1. 为什么这门课的习题值得花时间死磕
如果你正在学操作系统,大概率绕不开汤小丹老师这本《计算机操作系统(慕课版)》。这本书在高校里的覆盖率极高,考研408的复习也经常拿它当参考教材。但很多人学到第三章就开始卡壳——进程同步的PV操作、银行家算法、页面置换算法,课本上的例题看懂了,一到课后习题就懵。这不是你笨,而是操作系统这门课本身的特点决定的:它的知识点不是孤立的,每一道习题背后都牵扯着好几个概念的联动。
我见过太多人拿着习题答案直接抄,抄完觉得自己会了,考试换个数字就做不出来。问题的根源在于,操作系统的习题不是让你背答案的,它是让你验证自己有没有真正理解机制。比如信号量机制那道经典的生产者-消费者题,答案里写的是wait(mutex)在前还是wait(empty)在前,顺序换一下就是死锁。你光看答案知道"哦,要先wait(empty)再wait(mutex)",但为什么?如果不去想清楚信号量之间的依赖关系,下次遇到读者-写者问题照样写错。
所以这篇内容不是简单地把答案罗列出来给你抄。我会按章节拆解每类题型的解题逻辑,告诉你答案背后的推导过程,以及我在做题和讲题过程中总结出来的那些"课本上不会写但考试一定会考"的细节。适合正在学这门课的学生、准备考研的复习者,以及需要给这门课做辅导的助教。如果你只想找一份能直接抄的答案,那这篇可能不太适合你;但如果你想真正搞懂操作系统习题的套路,往下看。
2. 进程管理章节的习题类型与破题思路
2.1 PV操作题:从"背答案"到"推答案"的转变
进程同步这一块的习题,核心就一个东西:信号量。但信号量的题目千变万化,光靠背几道经典题的答案根本不够用。我的建议是,拿到一道PV操作题,先别急着写代码,按下面这个顺序理一遍:
第一步,找出所有进程和它们各自要做的动作。比如"生产者-消费者"问题里,生产者往缓冲区放东西,消费者从缓冲区取东西,这就是两个进程、两个核心动作。
第二步,确定共享资源有哪些,各自的容量是多少。缓冲区是共享资源,容量为N;互斥访问缓冲区的权限也是共享资源,容量为1。这一步决定了你要定义几个信号量。
第三步,判断哪些是同步关系,哪些是互斥关系。生产者必须等缓冲区有空位才能放,这是同步;消费者必须等缓冲区有数据才能取,这也是同步;生产者和消费者不能同时操作缓冲区,这是互斥。
第四步,根据关系定义信号量并赋初值。同步信号量初值通常是资源初始可用数量,互斥信号量初值固定为1。
第五步,在每个进程的动作前后加上对应的P操作和V操作。P操作申请资源,V操作释放资源。
这套流程走下来,大部分PV操作题都能推出来,不需要死记。我拿一道课本上的典型题演示一下。
题目:有一个仓库可以存放A和B两种产品,每次只能存入或取出一件产品。要求A产品数量与B产品数量之差在[-N, M]之间。请用PV操作描述入库和出库过程。
这道题的难点在于那个"差值范围"的约束。很多人看到这个条件就不知道信号量该怎么定义了。其实换个角度想:A比B最多多M个,意味着A最多领先B M个位置;B比A最多多N个,意味着B最多领先A N个位置。所以需要两个信号量来分别控制这两个方向的"领先额度"。
semaphore mutex = 1; // 互斥访问仓库 semaphore sa = M; // A领先B的剩余额度 semaphore sb = N; // B领先A的剩余额度 int countA = 0, countB = 0; // 存入A产品 void putA() { P(sa); // 申请A的领先额度 P(mutex); // 互斥进入 // 存入A countA++; V(mutex); V(sb); // 释放一个B的领先额度(因为A多了,B相对落后了) } // 存入B产品 void putB() { P(sb); P(mutex); // 存入B countB++; V(mutex); V(sa); }这里的关键理解是:sa和sb不是直接控制产品数量,而是控制两个方向上的"差值空间"。存入A会让A领先更多,所以消耗sa、释放sb;存入B则相反。这个思路一旦想通,类似的"差值约束"题目都能套。
注意:PV操作题里P操作的顺序绝对不能随便换。如果两个P操作都涉及互斥信号量,互斥的P一定要放在资源P的后面。否则可能出现一个进程占着互斥锁等资源,另一个进程占着资源等互斥锁,直接死锁。
2.2 银行家算法:手算表格的规范流程
银行家算法是死锁避免里的重点,也是考试高频考点。课本上的例题通常给一个5进程3资源的表格,然后问某个请求能不能分配。很多人觉得这题简单,不就是试算一下嘛。但实际操作中,最容易出错的地方是安全性检查的顺序和Work向量的更新。
我总结了一套手算流程,按这个走基本不会错:
- 列出Available、Max、Allocation、Need四个矩阵。Need = Max - Allocation,这个先算好。
- 把请求向量Request和Need比较。如果Request > Need,说明请求量超过了进程声明的最大需求,直接拒绝。
- 把Request和Available比较。如果Request > Available,说明当前资源不够,让进程等待。
- 试探性分配。Available -= Request,Allocation += Request,Need -= Request。
- 执行安全性检查。这一步是核心,找一个安全序列。
- 如果找到安全序列,正式分配;否则回滚,恢复原来的状态。
安全性检查的具体做法是:维护一个Work向量(初始等于Available)和一个Finish数组(初始全false)。每次找一个Need ≤ Work且Finish为false的进程,假设它执行完,Work += Allocation,Finish设为true。重复这个过程,如果所有进程都能Finish,说明存在安全序列。
这里有个细节很多人会忽略:安全序列可能不止一个,你只需要找到任意一个就行。但找的时候要按顺序从头扫,不要跳着找,否则容易漏掉。另外,Work向量在每一步都要更新,不能一直用初始值。
我见过一个常见的错误:有人在第5步检查时,把Work的初始值写成了原始的Available,而不是减去Request之后的Available。这个错误会导致安全性检查结果完全错误。记住,安全性检查是在"假设已经分配"的基础上做的,所以Work的起点必须是分配后的Available。
2.3 进程调度算法:计算题的时间轴画法
进程调度这块的习题主要是计算周转时间、带权周转时间、等待时间。算法本身不难,FCFS、SJF、RR、优先级调度,规则都很清晰。但手算的时候容易乱,尤其是RR(时间片轮转)算法,进程来回切换,时间轴一长就容易算错。
我的方法是:画一条时间轴,每个进程一行,用不同颜色的块表示运行、就绪、等待状态。虽然这里不能用图,但你可以自己在纸上画。具体步骤:
- 先按到达时间把所有进程排好。
- 从时刻0开始,看当前有哪些进程到达了。
- 根据算法规则选择下一个运行的进程。
- 记录它的开始时间、运行时长、完成时间。
- 更新就绪队列,继续下一步。
对于RR算法,关键是维护好就绪队列的顺序。新到达的进程排在队尾,被时间片打断的进程也排在队尾。这个顺序不能乱,否则整个时间轴就错了。
周转时间 = 完成时间 - 到达时间。带权周转时间 = 周转时间 / 服务时间。等待时间 = 周转时间 - 服务时间。这三个公式要记牢,但更重要的是理解它们的含义:周转时间衡量的是从提交到完成的总耗时,带权周转时间衡量的是相对于服务时间的效率。
提示:RR算法中,如果时间片足够大(大于所有进程的服务时间),它就退化成FCFS。如果时间片非常小,进程切换开销会变大,但响应时间会变短。考试里经常考"时间片取多大合适"这种概念题,答案通常是要大于一次上下文切换的开销,同时要保证大多数进程能在一个时间片内完成。
3. 内存管理习题的核心计算与易错点
3.1 页面置换算法:缺页率的精确计算
页面置换算法的习题通常给一个页面引用串和一个物理块数量,让你算缺页次数和缺页率。OPT、FIFO、LRU三种算法都要会手算。这里面的坑特别多,我逐个说。
OPT(最佳置换)算法:每次淘汰未来最长时间不会被访问的页面。这个算法理论上最优,但实际不可实现,只用于理论比较。手算的时候,你需要往后看引用串,找到每个已装入页面下一次出现的位置,淘汰最远的那个。如果某个页面后面再也不出现了,那它就是最佳淘汰对象。
FIFO(先进先出)算法:淘汰最早进入内存的页面。这个规则简单,但有个反直觉的现象叫Belady异常——增加物理块数反而可能导致缺页率上升。考试里经常考这个现象,让你举一个例子。经典的例子是引用串1 2 3 4 1 2 5 1 2 3 4 5,用3个物理块和4个物理块分别算,会发现4个块的缺页次数反而更多。
LRU(最近最久未使用)算法:淘汰最长时间没有被访问的页面。这个算法性能接近OPT,但实现开销大。手算的时候,你需要维护每个页面的"最近访问时间",每次淘汰时间最早的那个。
计算缺页率的时候,注意缺页率 = 缺页次数 / 总访问次数。总访问次数就是引用串的长度。有些题目会问"命中率",那就是1减去缺页率。
我总结了一个手算表格的模板,你可以直接套:
| 访问顺序 | 页面号 | 物理块1 | 物理块2 | 物理块3 | 是否缺页 |
|---|---|---|---|---|---|
| 1 | 1 | 1 | - | - | 是 |
| 2 | 2 | 1 | 2 | - | 是 |
| 3 | 3 | 1 | 2 | 3 | 是 |
| 4 | 4 | 4 | 2 | 3 | 是 |
| ... | ... | ... | ... | ... | ... |
每访问一个页面,就更新一次表格。缺页的时候标记"是",不缺页标记"否"。最后数一下"是"的个数就是缺页次数。
注意:FIFO算法中,如果物理块里还有空位,新页面直接放入空位,不算置换。只有物理块满了才需要淘汰。这个细节很多人会搞错,导致缺页次数算多。
3.2 分页与分段:地址转换的计算套路
分页存储管理里,逻辑地址到物理地址的转换是必考题。题目通常给页面大小、页表内容,然后给一个逻辑地址,让你算物理地址。这类题的套路很固定:
- 确定页面大小,算出页内偏移量的位数。比如页面大小为4KB,那页内偏移就是12位(因为2^12 = 4096)。
- 把逻辑地址拆成页号和页内偏移。页号 = 逻辑地址 / 页面大小,偏移 = 逻辑地址 % 页面大小。
- 查页表,找到页号对应的物理块号。
- 物理地址 = 物理块号 × 页面大小 + 页内偏移。
看起来简单,但有几个易错点。第一,页面大小可能是1KB、2KB、4KB,对应的偏移位数是10、11、12,别搞混。第二,页表里存的可能是物理块号,也可能是物理地址的起始地址,要看清楚题目怎么说的。第三,如果题目给的是十六进制地址,计算的时候要小心进制转换。
分段存储管理的地址转换类似,但段表里存的是段长和基址。逻辑地址 = 段号 + 段内偏移。转换时先检查段内偏移是否超过段长,如果超过就是越界错误。这个检查步骤不能漏,考试里经常考。
段页式管理是两者的结合,逻辑地址 = 段号 + 页号 + 页内偏移。转换过程要查两次表:先查段表找到页表地址,再查页表找到物理块号。计算量更大,但套路是一样的。
3.3 虚拟内存:页面分配与抖动问题
虚拟内存部分的习题经常考页面分配策略和抖动(Thrashing)的判断。页面分配有固定分配和可变分配两种,置换有全局置换和局部置换两种,组合起来有四种策略。考试里常问的是"工作集模型"和"缺页率与物理块数的关系"。
工作集模型的核心思想是:进程在一段时间内活跃的页面集合是相对稳定的。如果分配给进程的物理块数小于工作集大小,就会频繁缺页,导致抖动。题目通常会给一个引用串和一个窗口大小,让你算工作集。算法是:从当前时刻往前看窗口大小的引用,所有出现过的页面就是工作集。
抖动的原因是物理块数不够,解决办法是增加物理块数或者减少多道程序度。考试里经常出概念题,让你判断某个场景是不是抖动,或者问怎么解决。记住一个核心原则:抖动的本质是缺页率过高导致CPU利用率下降,而缺页率高的原因是物理块数不足。
4. 文件系统与I/O管理的习题处理
4.1 文件存储空间管理:位示图与索引结点的计算
文件系统这块的计算题主要集中在位示图和索引结点上。位示图用二进制位表示磁盘块的使用情况,0表示空闲,1表示占用。题目通常给一个字的大小(比如32位)和块号,让你算它在位示图中的位置。
计算公式:字号 = 块号 / 字长,位号 = 块号 % 字长。注意块号通常从0开始,字号和位号也从0开始。如果题目给的块号从1开始,记得先减1。
索引结点的计算更复杂一些。题目会给索引结点的结构,比如直接地址项有10个,一级间接、二级间接、三级间接各一个,每个地址项占4字节,磁盘块大小4KB。然后问你某个文件最大能有多大,或者给一个逻辑块号,问你怎么找到它。
这类题的解题关键是算清楚每个间接级别能覆盖多少块。一级间接:一个索引块能存4KB/4B = 1024个地址项,所以能覆盖1024个数据块。二级间接:1024 × 1024 = 1M个数据块。三级间接:1024^3个数据块。直接地址项覆盖10个块。所以最大文件大小 = (10 + 1024 + 1024^2 + 1024^3) × 4KB。
给逻辑块号找物理地址的时候,先判断它在哪个范围。如果逻辑块号小于10,直接查直接地址项。如果在10到1033之间,查一级间接。以此类推。这个判断过程要熟练,考试里时间紧,没空慢慢推。
4.2 磁盘调度算法:寻道时间的计算与比较
磁盘调度算法有FCFS、SSTF、SCAN、C-SCAN、LOOK、C-LOOK几种。题目通常给一个磁道请求序列和当前磁头位置,让你算总寻道长度或平均寻道长度。
FCFS就是按请求顺序来,总寻道长度 = 相邻两个请求之间距离的累加。SSTF每次选最近的请求,需要排序。SCAN是电梯算法,先往一个方向走到底再反向。C-SCAN是循环扫描,到了端点直接回到另一端。
计算的时候,先确定磁头的移动方向。SCAN和C-SCAN都需要知道当前方向,题目一般会说明。如果没说,通常默认往磁道号增大的方向。然后按方向排序请求,依次计算距离。
我见过一个常见的错误:在SCAN算法里,磁头走到端点后反向,但反向后的第一个请求不是最近的,而是按顺序来的。这个顺序不能乱,否则总寻道长度就错了。
提示:考试里经常比较不同算法的总寻道长度。一般来说,SSTF比FCFS好,SCAN比SSTF更稳定(不会饿死远端请求),C-SCAN比SCAN更公平(响应时间更均匀)。这些结论要记住,概念题会考。
4.3 I/O缓冲与设备管理:计算题的边界条件
I/O管理部分的习题相对少一些,但缓冲区的计算是个考点。单缓冲、双缓冲、缓冲池的处理时间计算,关键是要理解缓冲区的作用是缓解CPU和I/O设备之间的速度差异。
单缓冲的情况下,处理一块数据的时间 = max(CPU处理时间, I/O传输时间) + 缓冲区传递时间。双缓冲的情况下,如果CPU处理和I/O传输可以并行,处理时间接近max(CPU, I/O)。缓冲池更复杂,但核心思想是一样的。
设备管理里的计算题主要是SPOOLing系统的相关计算。SPOOLing用磁盘上的缓冲区模拟脱机输入输出,题目可能问输入井和输出井的容量、作业的周转时间等。这类题的关键是理清数据流向:输入设备 → 输入井 → 内存 → CPU → 输出井 → 输出设备。
5. 从习题答案到考试实战的转化技巧
5.1 答案看懂了但不会做题,问题出在哪
这是最普遍的问题。很多人看答案的时候觉得"哦,原来是这样",但合上答案自己写就卡住。根本原因是:你看的是答案的"结果",没有看答案的"推导过程"。答案里写P(empty); P(mutex);,你记住了这个顺序,但不知道为什么是这个顺序。下次题目换成"读者-写者"问题,信号量变了,顺序也变了,你就不会了。
解决办法是:每看一道题的答案,强迫自己回答三个问题。第一,这道题用了哪些信号量,每个信号量的含义是什么?第二,每个P操作和V操作分别对应什么物理意义?第三,如果我把某个P操作的顺序换一下,会发生什么?把这三个问题想清楚,这道题才算真正吃透。
我建议的做法是:先自己做一遍,做不出来再看答案。看答案的时候不要只看代码,要看文字解释。如果答案没有文字解释,就自己给自己讲一遍。讲不顺的地方就是你没理解的地方。
5.2 考前复习的优先级排序
操作系统这门课内容多,考前时间有限,不可能每个知识点都平均用力。根据我的经验,习题的优先级可以这样排:
第一优先级:PV操作、银行家算法、页面置换算法。这三类是必考的计算题,分值高,套路固定,练熟了就能拿分。
第二优先级:进程调度计算、地址转换、磁盘调度。这些也是计算题,但相对简单一些,套路更固定。
第三优先级:文件系统计算、I/O缓冲计算。这些出现的频率稍低,但一旦考到就是大题。
第四优先级:概念题和简答题。这些靠平时积累,考前突击效果有限,但可以把课本上的关键概念过一遍。
复习的时候,每类题找3-5道典型题练手,练到能独立写出完整过程为止。不要贪多,关键是练透。
5.3 那些答案里不会写但考试会扣分的细节
最后分享几个我在做题和讲题过程中总结的细节,这些在标准答案里通常不会特别强调,但考试里不注意就会扣分。
第一,PV操作的代码格式要规范。信号量定义要写清楚初值,P操作和V操作要写对名字(有些教材用wait/signal,有些用P/V,要跟课本一致)。代码块要标注清楚哪个是哪个进程。
第二,计算题要写中间步骤。比如算周转时间,不要只写最终结果,要把完成时间、到达时间、服务时间都列出来。阅卷老师看的是过程,结果错了但过程对,还能拿步骤分。
第三,单位要写清楚。地址转换题里,物理地址是多少KB、多少字节,要写明白。磁盘调度题里,寻道长度是多少个磁道,要标注。
第四,安全性检查要写出安全序列。银行家算法里,找到安全序列后要把序列写出来,比如"P1 → P3 → P0 → P2 → P4",这样阅卷老师一眼就能看出你做对了。
第五,页面置换要画出完整的表格。不要只写缺页次数,要把每次访问后的物理块状态都列出来。这样即使最后数字算错了,过程分也能拿到。
这些细节看起来琐碎,但考试里往往就是这些地方拉开差距。我见过太多人思路对了但格式不规范,最后扣了冤枉分。
操作系统这门课的习题,说到底考的不是记忆力,而是你对机制的理解程度。每道题都是一个具体的场景,你要做的是把课本上的原理应用到场景里。这个过程一开始会慢,但练多了就会形成条件反射。看到PV操作题就知道找同步互斥关系,看到页面置换就知道画表格,看到银行家算法就知道走安全性检查流程。到了这个程度,考试就是体力活了。