ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

操作系统调度、同步与死锁考点精讲:从课后习题到期末复习

操作系统调度、同步与死锁考点精讲:从课后习题到期末复习

简介:操作系统课程配套的课后习题解析文档,依据左万利、王英第四版《计算机操作系统教程》整理,面向高校相关专业学生、考研复习者与自学者,覆盖进程管理、处理机调度、进程同步与互斥、死锁避免等核心知识点。文档为Word格式,共1个文件,约4.21MB,便于打印或对照教材逐题使用;内容针对EDF与RMS实时调度、FCFS/SJB/HRN作业调度、读者写者问题、银行家算法等高频考点,详细给出Gantt图绘制、平均周转时间与带权周转时间计算、系统安全状态判定、资源请求合法性分析及死锁检测等解题过程,并配有典型计算表格与步骤说明。目前已有6334人学习下载,可帮助读者快速梳理解题思路、掌握算法执行过程与应试要点,是操作系统课程备考与日常练习的实用参考资料。

1. 从一道调度题说起:这份「计算机操作系统教程」课后习题答案,值不值得逐题过一遍

期末复习最怕的不是题难,而是答案本身有错。手边这本「计算机操作系统教程」的课后习题答案,覆盖第三章到第七章的经典考点——实时调度、作业调度、读者写者问题、银行家算法、死锁检测、页面置换、TLB 时间计算,每一题都给了解题过程和中间状态表。对考研和期末备考的人来说,这份答案的价值在于:它不是只给一个最终数字,而是把“为什么这么调度”“为什么安全序列是这个”“为什么缺页中断是 9 次”的推理链完整写出来了。适合两类人:一类是教材看完但不会做题的,另一类是做完题对不上答案、想知道自己卡在哪一步的。我按章节拆开讲,把最容易被跳过的推导细节和参数边界单独拎出来说。

2. 实时与作业调度:EDF/RMS 的判定边界,FCFS/SJF/HRN 的指标差距

2.1 可调度性判定:先算利用率,再谈算法好坏

第三章第 31 题给了一个周期性实时任务集:A 周期 30、处理时间 10;B 周期 40、处理时间 15;C 周期 50、处理时间 5。问 EDF(最早截止时间优先,动态优先级)和 RMS(单调速率调度,静态优先级)是否能调度,并要求画出 Gantt 图。

EDF 的判定条件比较宽松:只要总利用率小于等于 1,就存在可行调度。算一下:

10 / 30 + 15 / 40 + 5 / 50 = 0.333 + 0.375 + 0.1 = 0.808 < 1

所以 EDF 一定可以调度。而 RMS 的充分条件是总利用率不超过 n * (2^(1/n) - 1),n 是任务数。三个任务时这个界约等于 0.779,0.808 已经超过它,所以 RMS 不可调度。这是这道题最容易出错的地方:不是“感觉上 RMS 也能跑”,而是要先算利用率界。

提示:RMS 的 n * (2^(1/n) - 1) 只对周期等于截止时间的隐式截止时间模型成立。如果截止时间比周期短,这个界就失效,需要改用响应时间分析法。

答案里给的 EDF Gantt 图时间点是 0、10、25、30、40、55、60、70、80、95、105、110、120。拆开看前三个周期:0-10 跑 A,10-25 跑 B,25-30 跑 C;接下来 A 的截止时间 30 已过,新周期 A 截止 60,B 截止 40,C 截止 50,所以 30-40 跑 A、40-55 跑 B、55-60 跑 C。每一段都按“当前时刻截止时间最近的进程”来排,这就是 EDF 的动态决策逻辑。

2.2 作业调度计算:平均周转时间和带权周转时间怎么算才对

第 35 题给了四个作业,提交时间和执行时间分别是:J1 在 0 时刻提交、执行 2 小时;J2 在 20 分钟提交、执行 1 小时;J3 在 50 分钟提交、执行 0.5 小时;J4 在 70 分钟提交、执行 0.8 小时。要求分别用 FCFS、SJF、HRN 计算平均周转时间和平均带权周转时间。

答案里 FCFS 的执行顺序是 1-2-3-4。用一小段 Python 把计算过程固化下来,以后遇到同样题型改数据就能直接跑:

jobs = [ {"name": "J1", "submit": 0, "run": 120}, # 提交时间(分钟),执行时间(分钟) {"name": "J2", "submit": 20, "run": 60}, {"name": "J3", "submit": 50, "run": 30}, {"name": "J4", "submit": 70, "run": 48}, ] order = [0, 1, 2, 3] # FCFS: 按提交顺序 clock = 0 turnaround = [] for i in order: if clock < jobs[i]["submit"]: clock = jobs[i]["submit"] # 作业还没到,先等 clock += jobs[i]["run"] # 执行完 tt = clock - jobs[i]["submit"] # 完成时间 - 提交时间 turnaround.append(tt) print(f"{jobs[i]['name']} 周转 {tt} 分钟") avg = sum(turnaround) / len(turnaround) print(f"平均周转时间: {avg:.1f} 分钟") # 输出 157

逻辑说明:clock是 CPU 的当前时刻,作业没提交就先把时钟推到提交时刻,执行完再把时间加上执行长度。周转时间必须用“完成时刻减提交时刻”,不是“开始时刻减提交时刻”,这一步错了后面带权周转全错。参数说明:submit和run的单位统一成分钟,避免小时和分钟混用;如果你拿到的是小时单位,先换算再跑。上面的脚本跑出来平均周转就是 157 分钟,带权周转时间是每一项周转除以自己的执行时间再平均,得到约 3.23。

SJF 和 HRN 的结果也在这道题里:SJF 在 J1 执行完后,剩余作业按执行时间从短到长是 J3、J4、J2,平均周转约 146.5 分钟;HRN 按响应比(等待时间 + 执行时间)/ 执行时间动态选择,顺序是 1-3-2-4,平均约 149.5 分钟。注意 SJF 是“非抢占”的,J1 一到就要先跑完 2 小时,不能因为后面有短作业就中断它;HRN 是“每完成一个作业重新计算一次响应比”,不是一开始算完就固定。

3. 读者写者问题:信号量怎么写才不饿死,写者优先如何实现

3.1 经典解法:读者计数 + 互斥信号量,但要注意信号量的释放位置

第四章第 25 题给的是读者写者问题的一个“改进解法”,用三个信号量r_w_w、mutex、s,初始值都是 1,读者计数count初始为 0。这个解法的关键改进是加了信号量s,把读者的“进入临界区”和“判断是否为第一个读者”这两件事包起来,避免写者长期得不到执行。伪代码逻辑如下:

// 读者活动 P(s); P(mutex); count++; if (count == 1) P(r_w_w); V(mutex); V(s); // 读操作 P(mutex); count--; if (count == 0) V(r_w_w); V(mutex); // 写者活动 P(s); P(r_w_w); // 写操作 V(r_w_w); V(s);

逻辑说明:r_w_w是读者和写者之间真正的互斥锁,第一个读者进来时抢锁,最后一个读者离开时释放;mutex保护count的加减;s的作用是让读者在“检查自己是不是第一个读者”之前先排队,避免写者被后续不断到达的读者饿死。最容易写错的地方是V(s)的位置——必须在V(mutex)之后、进入读操作之前,不能挪到读操作结束之后。如果把V(s)放到读操作后面,相当于读者在整个读期间都占着s,写者一样被堵死,改进就白做了。

答案最后判断这个解法“不会出现饿死现象,是公平的解法”,理由就是s把读者和写者都纳入同一把排队锁,先到先得。这个结论有个前提:V(s)和P(r_w_w)之间不能插入其他可能让出 CPU 的操作,否则公平性会被打破。

3.2 写者优先:需要额外两组计数器和两级排队

第 26 题要求用 P、V 操作实现写者优先。这里不能直接在经典解法上加一个写者优先级标志,否则会出现“读者已经在读,写者来了却要等所有读者走完”的尴尬。常规做法是再加两个计数器:writecount记录等待或活跃的写者数,wmutex保护它;再加信号量z让所有读者按 FCFS 排队,x让一个读者和一个写者竞争文件访问权。核心思想是:一旦有写者在x上等待,后续读者全部被z挡住,不能再去抢x;而后续写者到达后,只要x被前一个写者释放,它们可以直接竞争读者与写者之间的互斥访问权。

三种策略的对比可以这样记:

策略关键信号量读者到达行为写者到达行为典型场景
读者优先mutex, r_w_w只要有读者在读,后续读者直接进必须等所有读者离开对读取延迟敏感的系统
公平解mutex, r_w_w, s按s排队,和写者公平竞争按s排队,不插队读写频率均衡
写者优先上述 + wmutex, z, x有写者等待时被z拦住后到写者可参与竞争写操作不能长期等待

笔试做这类题,阅卷老师看的是三个点:计数器是否正确初始化、P/V 是否成对出现、写者优先时读者是否真的被挡住。经常有人把z和x的作用搞混——z是读者排队用的,x是读者与第一个写者争用文件用的。只设x不设z,后续读者还是可以不排队直接冲到x,写者优先就实现不了。

4. 银行家算法与死锁检测:安全性序列的找法,资源请求的分支判断

4.1 安全状态判断:一个能跑通的银行家算法检测函数

第五章第 9 题给了一个五进程四资源的系统状态,Allocation和Need矩阵都是已知的,Available初始为 (1,6,2,3),问当前是否安全。答案是安全,存在安全序列<P0, P3, P4, P1, P2>。这个序列的找法不是猜,而是反复扫描:每次找一个Need[i] <= Work且没完成的进程,把它的Allocation加进Work,直到所有进程 Finish 为 true。

把这个过程写成脚本,以后遇到任何银行家算法题,把矩阵填进去就能验证:

allocation = [ [0, 0, 3, 2], [1, 0, 0, 0], [1, 3, 5, 4], [0, 3, 3, 2], [0, 0, 1, 4], ] need = [ [0, 0, 1, 2], [1, 7, 5, 0], [2, 3, 5, 6], [0, 6, 5, 2], [0, 6, 5, 6], ] available = [1, 6, 2, 3] n = len(allocation) finish = [False] * n safe_seq = [] for _ in range(n): for i in range(n): if not finish[i] and all(need[i][j] <= available[j] for j in range(4)): for j in range(4): available[j] += allocation[i][j] finish[i] = True safe_seq.append(f"P{i}") break print(safe_seq if all(finish) else "not safe") # 输出 ['P0', 'P3', 'P4', 'P1', 'P2']

逻辑说明:外层循环最多执行 n 次,每次只找一个可满足的进程;找到后立刻把资源并入Work。注意Need[i][j] <= available[j]是四维向量比较,必须用all(...)逐个分量判断,不能只比某一维。如果某次扫描一个进程都找不到,直接跳出并输出不安全。参数说明:allocation和need的行顺序要对应进程编号,available是动态数组,运行过程中会被改掉;如果还想用原始Available做后续判断,记得先复制一份。

4.2 请求合法性、可满足性、安全性三步判断

第 9 题第二问是 P2 提出请求Request[2]=(1,2,2,2),系统能不能分配。答案是不能,但过程要分三步走:

  1. 合法性检查:Request <= Need,即 (1,2,2,2) <= (2,3,5,6),成立。
  2. 可满足性检查:Request <= Available,即 (1,2,2,2) <= (1,6,2,3),成立,可以试分配。
  3. 预分配后跑安全性检测:分配后Available变成 (0,4,0,1),P2 的Allocation变成 (2,5,7,6),Need变成 (1,1,3,3)。此时没有任何进程的Need能放进Work=(0,4,0,1),所有 Finish 都是 false,系统进入不安全状态。

所以系统必须取消这次分配,恢复预分配前的状态,P2 继续等待。这一步是银行家算法最常考的变形——不是问“能不能满足请求”,而是问“分配后系统是否安全”,预分配后一定要跑一遍安全性检测再决定。

死锁检测和银行家算法的区别在于:死锁检测基于当前Request矩阵和Available,不做事前避免,而是允许系统进入不安全状态,然后周期性运行检测算法;如果存在一个进程序列能让所有进程 Finish 为 true,就没有死锁,反之则死了。第 10 题的三个请求状态变化里,前两个请求接受或记录后都能找到序列<P4,P1,P2,P3,P5,P6>(或 P1 开头的变化序列),说明未死锁;到Request[1]=(0,3,1)这一步,检测算法找不到完整序列,P1、P2、P3、P5 四个进程 Finish 为 false,死锁定位就完成了。做题时记住:检测算法只看两条,一是进程的Request能否被Available满足,二是满足后立即释放该进程全部Allocation并重新扫描。

5. 避坑:调度、信号量、银行家、页面置换里的六条踩坑记录

5.1 调度与同步计算里的翻车点

坑 1:RMS 利用率判定用错界。现象:算出总利用率 0.808 小于 1,直接判断 RMS 可调度,和答案相反。原因:RMS 的充分条件是利用率不超过 n * (2^(1/n) - 1),不是 1。解决:n=3 时先算 3 * (2^(1/3) - 1),约 0.779,再和总利用率比;0.808 > 0.779,所以不可调度。总利用率小于 1 只是 EDF 的充分条件,RMS 的更紧。

坑 2:周转时间把“开始执行时刻”当“完成时刻”。现象:算 FCFS 时四个作业周转是 120、140、90、98,和答案的 120、160、160、188 对不上。原因:J2 在 10:20 提交、13:00 完成,周转 160 分钟;有人却记成 12:00 开始、12:00 加 1 小时,用开始时刻减提交时刻。解决:完成时刻减提交时刻才是周转;带权周转再除执行时间。写代码或手算时都先把时刻轴画出来,标出每个作业的提交、开始、完成三个点。

坑 3:读者写者改进解法里把V(s)放错位置。现象:写完代码自己推演,发现第一个写者来了之后,后续读者还是能直接进来,写者被饿死。原因:V(s)被放在了读操作之后,读者在读书期间一直占着s,写者根本进不了队首。解决:V(s)必须在V(mutex)之后、读操作之前释放,让读者完成“排队 + 计数”就放开关卡。

5.2 死锁与分页判定里的忽略项

坑 4:银行家算法预分配后不恢复状态。现象:P2 请求判断完,接着算下一问,Available已经不是题目原始值,后面全错。原因:试分配后发现不安全,只写了一句“取消分配”,矩阵状态没在草稿上还原。解决:每次试分配前把Allocation、Need、Available三个矩阵抄在草稿一侧;一旦安全性检测失败,立刻把三个矩阵恢复成试分配前的样子。这个习惯在考试里能救回至少 5 分。

坑 5:OPT 页面置换从前往后找,而不是从后往前。现象:访问序列 4,3,2,1,4,3,5,4,3,2,1,5,页框数 3,手算 OPT 缺页次数和答案的 7 次不一致。原因:OPT 淘汰的是“未来最远才被访问”的页,有人却按 FIFO 的进入顺序淘汰,或按“当前往后第一个碰到的页”淘汰。解决:手算时对每次缺页,扫描当前时刻之后的所有访问,选最远的那个;如果某个页未来不会再访问,直接淘汰它。页框数为 4 时同理,答案是 6 次,比 FIFO 的 10 次少很多,好理解但难算对。

坑 6:TLB 时间计算把“缺页处理时间”的量级看错。现象:题干写缺页平均时间 108ns,有人当成 10^8ns 的简写,也有人当 108ns,最后总时间差出几个数量级。原因:排版压缩把 10^8 写成了 108,直接读字面值会翻车。解决:看到“1e8 ns 量级”要意识到这对应平均缺页处理耗时,计算时按 10^8 ns 处理;访问虚地址 15A5H 的总时间约 10 + 100 + 10^8 + 100 ns,约等于 10^8 ns 量级,而不是 10 ns 量级。另注意缺页中断处理后要回到原指令重新执行,这次重新执行的 TLB 命中和访存时间也要算进总时间,常被漏掉。

6. 把这份答案用成自测集:三遍法与手算检查单

6.1 三遍法:先独立做,再对答案,最后只看错题重做

第一遍不看答案,把第三章到第七章的题目当考试卷,限时 90 分钟。第二遍对答案,重点不是看数字对不对,而是看过程链完不完整——EDF 有没有算总利用率、银行家有没有跑安全性检测、页面置换的缺页标记有没有逐行列出来。第三遍只重做错题,每道错题在旁边写一行“错因”:利用率界记错、信号量位置放错、还是预分配后没恢复矩阵。这三遍下来,比单纯把答案从头到尾背一遍有用得多。

6.2 手算检查单:五个必查点

  • 调度题:检查总利用率公式的分母是不是周期,而不是执行时间;检查 Gantt 图每一段的长度是否等于对应任务执行时间。
  • 作业调度题:检查周转时间用的是“完成减提交”;带权周转的分子是周转、分母是执行时间。
  • 同步题:检查每个 P 操作是否都有对应的 V 操作;信号量初始值对不对;写者优先时读者是否被真正挡在排队处。
  • 银行家题:检查安全性序列里每个进程的 Need 是否都满足当前 Work;预分配后三个矩阵是否同步更新。
  • 页面置换题:检查 FIFO 看进入顺序、LRU 看最近使用、OPT 看未来使用,三种算法不要用同一套淘汰逻辑;缺页次数占位格子里每个页框的变化都要能说清楚。

这套检查单我贴在草稿纸角落,每次模拟考完对照着勾一遍。从那以后我做调度和死锁类题目,基本都能一次写对完整过程,不再因为漏步骤被扣分。希望帮到你——把这份答案当成自测集用,而不是抄写本,复习效率会明显不一样。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进