从随堂练习到课程设计:操作系统核心算法实战解析
简介华南理工大学操作系统含课程设计随堂练习PDF聚焦操作系统引论章节适合正在学习操作系统基础课程的本科生、备考者及需要梳理核心概念的读者。资源以单份PDF文件形式提供压缩包内含1个PDF整体大小仅38KB轻量便携便于在手机或电脑上随时查看。当前已有117人学习浏览内容覆盖操作系统的基本概念、类型、历史发展、组成、功能、设计实现及应用趋势等摘要信息并收录第1章引论的13道选择题涉及实时操作系统处理外部事件的时限、操作系统的管理对象、虚拟计算机的定义、多道程序设计提高CPU与外部设备利用率、并发性的时间特征等高频考点每题均附参考答案方便自测和对照纠错。整体而言这份材料精炼浓缩了操作系统入门阶段的重要知识点可作为课堂笔记的补充或考前冲刺的速刷题库。1. 华南理工大学操作系统(含课程设计)随堂练习.pdf不是刷题册是浓缩考纲拿到「华南理工大学操作系统(含课程设计)随堂练习.pdf」这份文件的人大概分两种离考试还有两周、想靠它圈重点的在校生准备课程设计、想从里面刨项目思路的动手派。我的看法很直接这份随堂练习的价值不在刷完就稳而在它把操作系统最核心的五大模块——进程管理、内存管理、文件系统、设备管理和死锁——压缩成了可以逐题验证的题目形态。跟着它过一遍等于把六百页教材读薄成一张能动手的考纲。操作系统的任务说到底就是管好 CPU、内存和 I/O而这份练习恰好用计算题和设计题把「怎么管」问了一遍。下面这套路径就是我带学生复习和做课设时实际走的那套从拆题到写代码再到答辩前怎么用它自检。2. 拿到随堂练习先做三件事题型拆解、考点映射和知识盲区清单随堂练习和课后习题最大的区别是它是老师讲完马上要你反应的题答案往往就在最近一次课的板书里。所以它比课后题更接近考点也更零散。我一般不会拿到就从头刷到尾而是先干三件事归类、映射、立习惯。做完这三步这份 PDF 才真正变成你自己的操作系统笔记。2.1 先按五大模块给练习题归类别按页码刷把一份操作系统随堂练习拆开看题目其实就落在五个筐里进程与调度、同步与死锁、内存与虚拟存储、文件与磁盘、设备与 I/O。我见过不少同学拿到 PDF 直接按顺序做做到第四章发现第二章的进程状态又忘了再回头翻效率极低。正确做法是先花半小时通读给每道题标上一到两个模块标签比如「进程状态调度」「内存页表」「文件目录结构」之后按模块集中刷。这里有个容易被忽略的规律进程管理和内存管理通常占练习总量的六成以上而文件系统和设备管理多是概念题加少量计算题磁盘调度、位示图。你要是时间紧按这个比例分配精力比平均用力划算得多。归类的时候把题目序号写在草稿纸上同时标出「会做」「半懂」「完全没思路」三档。半懂和没思路的题才是你真正要补的盲区而不是那些已经会做的。做完归类你会发现自己对整门课的结构一下子清楚了这份归类过程本身就是一份比课本目录更好用的操作系统笔记。2.2 考点映射表一道练习对应一个可答辩的课程设计归类之后第二步是做一张考点映射表。随堂练习的价值在于每一道计算题本质上是课程设计里一个算法的手算样例。课程设计验收的时候老师最常问的一句话就是「你怎么证明你的程序是对的」。如果你能把练习里的标准答案变成程序的测试用例这个问题就迎刃而解。下面这张表是我按常见题型整理的映射关系你可以照着它把自己的练习标进去。练习题型对应考点可延伸的课程设计方向最容易丢分的位置进程状态转换与调度计算FCFS/SJF/RR进程生命周期、调度算法评价指标调度模拟器输出周转、带权周转、等待时间状态转换条件漏写「等待→就绪」PV 操作题生产者消费者、读者写者信号量互斥与同步多线程同步程序、哲学家就餐模拟P/V 顺序颠倒、信号量初始值设错银行家算法安全序列判断死锁避免银行家算法可视化工具可用资源向量更新错、Need 矩阵算错逻辑地址换算与页表查询分页存储、快表、有效访问时间地址转换命令行工具十六进制换算翻车、把块号当页号页面置换OPT/FIFO/LRU/Clock缺页率、Belady 异常置换算法对比程序初始是否计数约定不清、LRU 找错方向磁盘调度FCFS/SCAN/C-SCAN寻道时间优化磁盘臂调度模拟器磁头方向不回绕、起始位置忘标注这张表最关键的作用是把「会做题」和「能答辩」两件事打通。比如你练习里做对了一道 FCFS 调度题那你课程设计里调度器的输入输出格式就应该按这道题来定义进程号、到达时间、服务时间进去完成时间、周转时间、等待时间出来。这样练习里的每一道题都能变成一个可复现的验收点。答辩时老师随便指一道练习你现场把参数敲进去结果和标准答案一致这比任何口头解释都有说服力。2.3 动笔前先立三个习惯状态图、条件表、时间轴很多同学做计算题喜欢直接套公式结果一到变体题就懵。我一般会让学生动笔前先立三个习惯这三个习惯后期写代码时直接变成变量和数据结构一举两得。习惯一凡进程题先画状态图。不管题目问的是调度还是同步先把三态或五态图画出来标注每个转换事件的触发条件。比如「运行→等待」只能由 I/O 请求或事件等待触发「等待→就绪」只能由 I/O 完成或事件发生触发。这道工序看起来多花三十秒但能防止你漏掉转换边尤其防止把「等待→就绪」这条最容易漏的边丢掉。习惯二凡死锁题先列四个必要条件。互斥、请求保持、不可剥夺、循环等待逐条对照题目场景再判断题目考的是预防、避免还是检测。银行家算法属于避免破坏循环等待属于预防这两类题目的解题入口完全不同先列条件能帮你快速定位题型。习惯三凡调度题写时间轴。就是甘特图每完成一个进程就更新一次当前时间。FCFS 的时间轴是一条直线SJF 非抢占的时间轴要标出每次就绪队列变化的位置。这个时间轴写熟练了对应到代码里就是一个累加的time变量后面写调度模拟器时你会发现代码几乎就是时间轴的翻译。3. 从随堂练习到可运行代码调度、银行家、信号量的落地写法随堂练习里的计算题本质是手算一个算法课程设计要做的是把这些算法写成能跑的程序。常见做法是把每个核心算法做成一个小模拟器输入练习里的数据输出练习要求的指标。我一般用 Python 做原型因为数据结构直观、调试快答辩时还能现场改参数给老师看。下面三个例子覆盖了最常考、也最常被选作课程设计的三个方向。3.1 把调度计算题变成调度模拟器FCFS 与 SJF 一起写调度题是随堂练习的必考项也是课程设计里最好出效果的方向。这里给一个同时支持 FCFS 和非抢占 SJF 的调度模拟器输入是进程列表输出是每个进程的完成时间、周转时间和等待时间。def schedule(processes, modefcfs): # processes: [(pid, arrival, burst), ...] # pid进程号, arrival到达时间, burst服务时间 procs sorted(processes, keylambda p: p[1]) # 先按到达时间排 time 0 # 当前系统时间就是手算时的甘特图游标 done [] # 已完成的进程 ready [] # 就绪队列 idx 0 n len(procs) while len(done) n: # 把所有已到达的进程放进就绪队列 while idx n and procs[idx][1] time: ready.append(procs[idx]) idx 1 if not ready: # CPU 空闲直接跳到下一个进程的到达时刻 time procs[idx][1] continue if mode sjf: ready.sort(keylambda p: p[2]) # SJF从就绪队列挑最短服务时间 pid, arrival, burst ready.pop(0) time burst done.append((pid, arrival, burst, time, time - arrival, # 周转时间 time - arrival - burst)) # 等待时间 return done # 示例三道随堂练习风格的进程手算 FCFS 后核对输出 procs [(1, 0, 7), (2, 2, 4), (3, 4, 1)] for row in schedule(procs, fcfs): print(row)这段代码的核心逻辑有三处要说明。第一time就是你在草稿纸上画的时间轴每完成一个进程就累加一次burstCPU 空闲时直接跳到下一个到达时刻对应你手算时空闲段跳过不等的习惯。第二ready队列是 FCFS 和 SJF 的分水岭FCFS 按到达顺序弹出SJF 每次从就绪队列里挑服务时间最短的这就是非抢占 SJF 的手算过程。第三输出里的周转时间time - arrival和等待时间time - arrival - burst是课程设计验收的标准指标练习里的标准答案就是这两个数加一个平均带权周转时间。参数上要注意如果题目要求的是抢占式 SJF也叫 SRTF上面的代码就不够用了需要在每个新进程到达时比较剩余服务时间这个版本留给你的课设当扩展点。FCFS 模式下如果所有进程同时到达那按到达时间排序后就是按进程号顺序执行和手算结果一致。验证方法很简单把练习里手算好的甘特图拿出来逐行对照这段程序的输出。3.2 银行家算法安全序列从手算到程序银行家算法是死锁章节的压轴题手算时要反复试分配写程序时最怕的就是把试分配的资源状态改乱了。下面这个实现把安全检查单独做成一个函数注意看它在work上做临时累加而不是直接改available。def is_safe(available, allocation, need): # available: 当前可用资源向量例如 [3, 3, 2] # allocation[i]: 进程 i 已分配的资源 # need[i]: 进程 i 还需要的资源 work available[:] # 安全检查的临时工作副本不能改原始数据 finish [False] * len(allocation) safe_seq [] while len(safe_seq) len(allocation): found False for i in range(len(allocation)): if not finish[i] and all(need[i][j] work[j] for j in range(len(work))): # 进程 i 可以完成回收它占用的全部资源 for j in range(len(work)): work[j] allocation[i][j] finish[i] True safe_seq.append(i) found True if not found: # 一轮扫描找不到可完成的进程说明系统将进入不安全状态 return False, [] return True, safe_seq # 经典练习数据5 个进程3 类资源 available [3, 3, 2] allocation [[0, 1, 0], [2, 0, 0], [3, 0, 2], [2, 1, 1], [0, 0, 2]] need [[7, 4, 3], [1, 2, 2], [6, 0, 0], [0, 1, 1], [4, 3, 1]] ok, seq is_safe(available, allocation, need) print(safe if ok else unsafe, seq)这里最值得讲的是work available[:]这一行。手算安全序列时你是在草稿纸上复制一份可用资源来做试分配算完扔掉不会影响题目原条件。程序里如果不复制直接在available上累加第一次尝试成功后原始数据就变了后续判断全部失真这就是「手算安全、程序报错」最常见的根源。need[i][j] work[j]这个判断对应手算时那句「检查某进程的剩余需求是否都被当前可用资源满足」all函数把多类资源的一次性检查压缩成一行。如果课程设计要做到「请求资源」这一层流程是先检查Request[i] Need[i]再检查Request[i] Available然后做一次试分配把 Available、Allocation、Need 都更新最后调用is_safe判断试分配后是否仍安全。安全才正式分配否则回滚。这个回滚逻辑对应手算题里那句「若找不到安全序列则本次请求不被批准」。3.3 生产者消费者把 PV 练习变成能跑的多线程程序同步问题是随堂练习里最容易让人怀疑人生的部分因为 PV 操作光靠手算很难验证死锁不跑起来看不见。把练习里的信号量题改造成多线程程序是理解同步最快的路径。import threading import time buffer [] BUFFER_SIZE 5 empty threading.Semaphore(BUFFER_SIZE) # 空位个数初始等于缓冲区大小 full threading.Semaphore(0) # 已占用个数初始为 0 mutex threading.Lock() # 互斥锁保护 buffer 本身 def producer(item): empty.acquire() # 先申请空位对应 P(empty) mutex.acquire() # 再拿互斥锁对应 P(mutex) buffer.append(item) print(produce, item, buffer) mutex.release() # 先释放锁对应 V(mutex) full.release() # 增加已占用个数对应 V(full) def consumer(): full.acquire() # 先确认有数据对应 P(full) mutex.acquire() # 再拿互斥锁 item buffer.pop(0) print(consume, item, buffer) mutex.release() empty.release() # 释放一个空位对应 V(empty) return item if __name__ __main__: for i in range(10): threading.Thread(targetproducer, args(i,)).start() threading.Thread(targetconsumer).start()对照课本的 PV 写法你会发现代码就是P(empty); P(mutex); 写缓冲; V(mutex); V(full)的直译。两个信号量的初始值就是练习里要求的「设 emptyn, full0」这是整个程序正确性的前提。顺序上务必记住先资源信号量后互斥锁。empty和full管的是「缓冲区还有没有空位/数据」这个资源条件mutex管的是「同一时刻只能一个线程碰 buffer」。如果把mutex.acquire()提到empty.acquire()前面缓冲区满时生产者会拿着锁等空位消费者又进不去取数据直接死锁。这套模板可以扩展成读者写者问题、哲学家就餐问题只要把信号量个数和获取顺序按题目改就行。如果课程设计用 C 语言写Linux 下编译记得加-pthread链接选项Windows 下用 Win32 的CreateSemaphore也是同一个思路。跑起来后在终端观察打印顺序你会发现所谓「同步」就是让不同线程的打印要么都在临界区里要么被资源信号量卡在门外。4. 避坑随堂练习反复翻车的 5 类问题与排查思路下面五条是我辅导课设和批改作业时出现频率最高的翻车点每条按「现象 → 原因 → 解决」的顺序写。你可以直接对照自己的练习册和代码找问题。4.1 状态图漏了「等待→就绪」这条边现象画进程五态图每次都不完整做调度题时默认进程 I/O 完成后直接进入运行态导致时间轴算错。原因把「等待」当成了终点忘了进程从 I/O 或事件中醒来后要先回到就绪队列排队不能插队直接上 CPU。这里丢的不是一条边而是整个就绪队列存在的意义。解决把五态转换背成两进两出——就绪→运行由调度触发运行→就绪由时间片到触发运行→等待由 I/O 请求触发等待→就绪由 I/O 完成触发。每次画完图数一遍四条边少了哪条一眼就能看出来。这个习惯延伸到代码里就是就绪队列的入队操作必须发生在进程状态变成就绪之后而不是在 I/O 完成的那一刻直接让出 CPU。4.2 银行家算法手算安全、程序却报不安全现象随堂练习手算得出安全序列比如 (P1, P3, P2, P4, P0)把同样的数据写进代码程序却返回 unsafe。原因最常见的是在安全检查里直接修改了available第一轮 P1 试分配成功后原始数据就变了后面的判断条件不再反映真实系统状态。另一种是把allocation和need搞混need是「还需要的」allocation是「已经占着的」用错一个矩阵整个算法就废了。解决检查代码里是否对available做了副本再开始试分配就像上一章代码里的work available[:]。再打印出need矩阵和草稿纸逐格对比。我一般会在is_safe里临时加两行print把每轮选中的进程和当时的work向量打出来和手算草稿上的每次「可用资源更新」对齐能对上就说明逻辑没问题对不上就回头查资源回收那几行。4.3 地址换算题页号对了偏移量却错了现象逻辑地址0x2A3F页面大小 1KB算出页号是 10但偏移量一个版本算 575一个版本算 543对不上答案。还有一种翻车是把十六进制整体转十进制再去除页面大小除出来的页号总是差一位。原因分页地址换算的本质是位运算页面大小是2^10时页内偏移就是逻辑地址的低 10 位页号是高剩余位。很多同学把它当普通除法做忘了十六进制数里每一位对应 4 个二进制位低 10 位并不等于低两位十六进制要从二进制位边界去切。解决页面大小为 1KB 时先把逻辑地址写成十六进制低两位就是偏移量高位就是页号这个规则对所有 2 的幂次页面大小都成立。再用 Python 的int(2A3F, 16)转成十进制算一遍交叉验证。两道工序结果一致再写答案不一致就回头检查是哪一位切错了。这道题是随堂练习里靠粗心丢分最狠的地方没有之一。4.4 PV 题把 P 的顺序写反程序直接卡死现象生产者消费者多缓冲区程序跑起来输出一两行后就卡住终端既不往下打印也不报错像死机了一样。原因生产者的mutex.acquire()写在了empty.acquire()前面。缓冲区满时生产者拿着互斥锁等在empty上消费者想进临界区拿数据却被同一把锁挡住两边互相等这就是教科书式的死锁。单缓冲区题目里运气好可能不翻车缓冲区一多必现。解决严格按「先资源信号量、后互斥锁」的顺序写。排查时在每个acquire前后加一行打印信号量当前值卡住的位置一定在某个acquire上看它卡在哪个信号量就能定位是资源条件没满足还是锁被人占着。这个排查手段在课程设计答辩现场特别好用因为你能现场演示「卡住 → 定位 → 改顺序 → 跑通」的完整过程比空讲同步原理有说服力得多。4.5 页面置换答案总差一次缺页现象FIFO 或 LRU 手算的缺页次数和标准答案总是差 1 次有时多有时少。原因两个默认约定没对齐——初始页框是否算缺页以及访问串里重复访问的页是否计命中。大多数练习的默认规则是「页框初始为空第一次装入算缺页重复访问算命中」但题目不写的时候你按「已预先装满」算就会差一次。解决做题前先在草稿纸顶部写两行约定「初始空 算缺页页已在内存 命中不计数」。算完如果还是差 1 次别从头重算直接检查最后一次置换——最后一次访问的页如果已经在页框里却被你重复装入了一次缺页数就会多 1。这类题目对了约定正确率能立刻拉满。对应到课程设计代码里就是模拟器要暴露一个「初始状态」参数让用户明确选「空页框」还是「预填充」这样无论题目怎么出你都能对齐答案。5. 把随堂练习变成课程设计答辩的素材库一个值得坚持的验证习惯做完整份随堂练习最该做的不是把答案背下来而是把这些题变成课程设计代码的回归测试集。我这几年带课设最深的体会是老师答辩时问的不是「你会不会原理」而是「你怎么证明你写的东西是对的」。随堂练习恰好就是现成的测试数据因为每道题都有标准答案而你的代码输入这些数据输出必须和答案一致。具体做法很朴素为每个算法建一个tests目录把练习里的题目输入和标准答案存成断言。调度器就断言平均周转时间银行家算法就断言安全序列页面置换就断言缺页次数。下面这个片段是调度器的测试骨架你可以照着扩展。# tests/test_scheduler.py cases [ # 输入: (进程列表, 调度方式) 输出: 期望的平均周转时间 ([(1, 0, 7), (2, 2, 4), (3, 4, 1)], fcfs, 6.0), ([(1, 0, 7), (2, 2, 4), (3, 4, 1)], sjf, 5.0), ] def run_case(processes, mode, expected): result schedule(processes, mode) avg sum(r[4] for r in result) / len(result) # r[4] 是周转时间 assert abs(avg - expected) 1e-6, f{mode}: {avg} ! {expected} for processes, mode, expected in cases: run_case(processes, mode, expected)这样做的直接收益是课程设计做到最后你手里有一个「任何修改都不会破坏已验证结论」的保障。改调度逻辑、加新算法跑一遍测试集哪里坏了立刻知道。答辩前我还会专门测三类边界条件单进程且带较长空闲段、所有进程同一时刻到达、银行家算法里请求大于 Need这三种情况是老师最常随手考的场景。我自己当年做页面置换课设时把教材和练习里能找到的二十道置换题全做成了断言答辩时老师随手点了一道 LRU 的变体题我现场把参数输进程序一行输出就给出了和标准答案一致的缺页次数。那个瞬间我意识到随堂练习最好的用法不是考前突击而是平时就把每一道题喂给代码当裁判。这个习惯后来无论做操作系统还是做别的项目我都一直保留着希望帮到你。本文还有配套的精品资源点击获取