ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

计算机操作系统课后答案汤小丹:从对答案到真懂原理的拆解路径

计算机操作系统课后答案汤小丹:从对答案到真懂原理的拆解路径 简介这份资源是汤小丹《计算机操作系统》教材的课后习题参考答案面向高校计算机专业学生及考研备考者用于课后巩固与期末复习。内容覆盖操作系统引论至文件管理等核心章节包含OS主要目标、多道批处理与分时系统、实时任务、并发共享虚拟异步四大特征以及处理机、内存、设备、文件管理的功能与任务等知识点并配有脱机/联机I/O、微内核OS等典型问答解析。资源包为1个doc文档大小约654KB结构紧凑便于打印或电子阅读。目前已有3900人学习下载适合需要系统梳理概念、对照教材查漏补缺的学习者使用。1. 计算机操作系统课后答案 汤小丹从“对答案”到“真懂原理”的拆解路径很多人拿到汤小丹版《计算机操作系统》的课后习题第一反应是找一份答案对一对把错题改过来就算完事。但真正做过操作系统课程设计、写过内核模块、调过调度器参数的人会告诉你课后答案的价值不在“对错”而在它逼你把进程调度、内存管理、文件系统这些黑匣子拆开看一遍。汤小丹这本教材的习题设计有个特点——计算题占比高PV操作、页面置换、磁盘调度、银行家算法几乎每章都有这些题如果只背答案考试一过就忘但如果顺着题目把算法逻辑跑一遍你收获的是能直接迁移到实际工程里的分析能力。这篇文章面向正在学这门课的学生、准备考研复试的考生以及想补操作系统基础的开发者把“课后答案”这件事从被动查阅变成主动推演给出可复现的解题路径和验证方法。2. 汤小丹习题的题型分布与解题底层逻辑2.1 为什么计算题不能只背答案汤小丹版教材的习题大致分四类概念辨析题、计算分析题、算法设计题、综合应用题。概念题考的是定义边界比如“进程和线程的区别”这种答案看似标准但如果你不理解“资源分配单元”和“调度单元”这两个维度的分离换个问法就答不上来。计算题才是这本教材的真正门槛——PV操作题需要你同时跟踪信号量值和进程状态页面置换题需要你手动模拟FIFO、LRU、OPT三种算法的每一步银行家算法需要你维护Available、Max、Allocation、Need四个矩阵的实时变化。我见过太多人把答案抄一遍就觉得自己会了结果考试时PV操作少写一个P就全盘皆输。根本原因在于这些题的本质是状态机推演你必须亲手把每一步的状态变化写出来才能建立肌肉记忆。常见做法是准备一张草稿纸每道计算题都画状态转移表把中间过程完整记录下来而不是直接跳到最终答案。2.2 三类核心题型的通用解题框架PV操作题的通用框架是先识别临界资源数量和互斥关系再确定信号量初值然后按照“互斥P在前V在后、同步先P后V”的原则排列操作顺序。具体步骤是第一步找出所有需要互斥访问的资源每个资源设一个mutex信号量初值1第二步找出所有需要同步的前驱后继关系每个关系设一个信号量初值0第三步把P操作放在临界区之前V操作放在临界区之后第四步检查是否有死锁可能必要时调整P操作顺序。页面置换题的框架是画一张表列是页面访问序列行是物理块每访问一个页面就更新表。FIFO看谁先进来谁先出去LRU看谁最久没被访问OPT看未来谁最晚被访问。关键是要把每次置换后的内存状态写清楚缺页次数自然就出来了。银行家算法题的框架是先算Need矩阵Need Max - Allocation然后找Need ≤ Available的进程假设它执行完释放资源更新Available重复直到所有进程都能执行完或者找不到可执行进程。如果找不到说明系统处于不安全状态。2.3 用Python模拟PV操作验证答案光靠手算容易出错我一般会用Python把PV操作模拟一遍验证自己手算的结果。下面是一个经典的生产者-消费者问题模拟代码import threading import time import queue # 信号量用threading.Semaphore模拟 mutex threading.Semaphore(1) # 互斥访问缓冲区 empty threading.Semaphore(5) # 空缓冲区数量初值5 full threading.Semaphore(0) # 满缓冲区数量初值0 buffer queue.Queue(5) # 缓冲区容量5 def producer(pid): for i in range(3): item fP{pid}-item{i} empty.acquire() # P(empty)等待空位 mutex.acquire() # P(mutex)进入临界区 buffer.put(item) print(f生产者{pid}放入{item}缓冲区大小{buffer.qsize()}) mutex.release() # V(mutex)退出临界区 full.release() # V(full)通知有数据 time.sleep(0.1) def consumer(cid): for i in range(3): full.acquire() # P(full)等待数据 mutex.acquire() # P(mutex)进入临界区 item buffer.get() print(f消费者{cid}取出{item}缓冲区大小{buffer.qsize()}) mutex.release() # V(mutex)退出临界区 empty.release() # V(empty)通知有空位 time.sleep(0.15) # 启动2个生产者和2个消费者 threads [] for i in range(2): threads.append(threading.Thread(targetproducer, args(i,))) threads.append(threading.Thread(targetconsumer, args(i,))) for t in threads: t.start() for t in threads: t.join()这段代码的逻辑说明empty信号量初值为5表示缓冲区有5个空位full初值为0表示没有数据mutex初值为1保证同一时刻只有一个线程操作缓冲区。生产者先执行empty.acquire()如果缓冲区满了就会阻塞然后执行mutex.acquire()进入临界区放入数据后先mutex.release()再full.release()。消费者顺序相反。参数调整建议把empty初值改成3观察生产者阻塞的时机变化把time.sleep去掉看是否出现竞态条件。这个模拟能帮你验证手算的PV操作顺序是否正确——如果手算结果和代码运行结果不一致大概率是P操作顺序写反了。3. 页面置换与磁盘调度手算代码双验证3.1 FIFO、LRU、OPT三种算法的逐步推演页面置换是汤小丹教材里计算量最大的题型之一。以经典的访问序列7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1为例物理块数为3三种算法的缺页次数差异很大。手算时建议画一张表每一列是一次访问每一行是一个物理块被置换出去的页面用删除线标出。FIFO的规则最简单谁先进内存谁先被换出。用队列维护每次缺页时淘汰队头页面。上面这个序列FIFO缺页15次。LRU的规则是淘汰最久未被访问的页面需要维护每个页面的最近访问时间。上面这个序列LRU缺页12次。OPT的规则是淘汰未来最长时间不会被访问的页面需要预知未来访问序列。上面这个序列OPT缺页9次。手算时最容易翻车的地方是LRU的“最近访问时间”更新时机。每次访问一个页面无论是否缺页都要更新该页面的时间戳。很多人只在缺页时才更新时间戳导致LRU退化成FIFO。血泪经验是在表格里给每个物理块加一列“最近访问序号”每次访问后都更新这样就不会漏。3.2 用Python实现三种置换算法并对比缺页率手算验证之后用代码跑一遍能更直观地看到差异def fifo(pages, frames): memory [] faults 0 for page in pages: if page not in memory: faults 1 if len(memory) frames: memory.append(page) else: memory.pop(0) # 淘汰最早进入的 memory.append(page) return faults def lru(pages, frames): memory [] recent {} # 记录每个页面的最近访问序号 faults 0 for i, page in enumerate(pages): if page not in memory: faults 1 if len(memory) frames: memory.append(page) else: # 淘汰最近访问序号最小的页面 victim min(memory, keylambda p: recent[p]) memory.remove(victim) memory.append(page) recent[page] i # 无论是否缺页都更新 return faults def opt(pages, frames): memory [] faults 0 for i, page in enumerate(pages): if page not in memory: faults 1 if len(memory) frames: memory.append(page) else: # 找未来最晚被访问的页面淘汰 farthest -1 victim None for p in memory: try: next_use pages[i1:].index(p) except ValueError: next_use float(inf) # 未来不再使用 if next_use farthest: farthest next_use victim p memory.remove(victim) memory.append(page) return faults pages [7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1] print(fFIFO缺页次数{fifo(pages, 3)}) print(fLRU缺页次数{lru(pages, 3)}) print(fOPT缺页次数{opt(pages, 3)})逻辑说明fifo用列表模拟队列pop(0)淘汰队头lru用字典记录每个页面的最近访问索引淘汰时选索引最小的opt在缺页时遍历当前内存中的页面计算每个页面在后续序列中下一次出现的距离淘汰距离最大的。参数调整把frames改成4观察三种算法的缺页次数差距是否缩小把访问序列改成局部性更强的序列如1,2,3,1,2,3,4,1,2,3观察LRU和OPT的差距变化。这个对比能帮你理解为什么实际系统中LRU比FIFO好但OPT无法实现只能作为理论下界。3.3 磁盘调度算法的计算要点磁盘调度常考FCFS、SSTF、SCAN、CSCAN四种。以磁头当前在100磁道请求序列为55,58,39,18,90,160,150,38,184为例FCFS就是按请求顺序服务总移动距离是各请求之间距离的累加。SSTF每次选离当前磁头最近的请求需要动态计算距离。SCAN朝一个方向扫到底再反向CSCAN扫到底后直接回到另一端。计算时注意两个细节一是SCAN和CSCAN的初始方向题目一般会说明“磁头正向移动”或“向磁道号增加方向移动”二是CSCAN回到另一端时如果题目问的是“移动距离”回程距离要算进去如果问的是“服务顺序”回程不产生服务。常见错误是把SCAN的折返点算错——SCAN是扫到该方向最远的请求就折返不是扫到磁盘边界。4. 银行家算法与死锁检测矩阵推演不翻车4.1 安全序列的逐步推导方法银行家算法的题目通常给三个矩阵Allocation已分配、Max最大需求、Available可用资源。第一步算Need矩阵Need[i][j] Max[i][j] - Allocation[i][j]。第二步找Need ≤ Available的进程假设它执行完并释放资源Available Allocation[i]。第三步重复第二步直到所有进程都进入安全序列或者找不到可执行进程。以经典例题为例系统有A、B、C三类资源Available [3,3,2]五个进程的Allocation和Max如下表进程Allocation (A,B,C)Max (A,B,C)Need (A,B,C)P00,1,07,5,37,4,3P12,0,03,2,21,2,2P23,0,29,0,26,0,0P32,1,12,2,20,1,1P40,0,24,3,34,3,1推导过程初始Available[3,3,2]找Need ≤ Available的进程。P1的Need[1,2,2] ≤ [3,3,2]P3的Need[0,1,1] ≤ [3,3,2]P4的Need[4,3,1]不满足P0和P2也不满足。先选P1执行完后Available [3,3,2] [2,0,0] [5,3,2]。再找P3满足执行完后Available [5,3,2] [2,1,1] [7,4,3]。再找P4满足执行完后Available [7,4,3] [0,0,2] [7,4,5]。再找P0满足执行完后Available [7,4,5] [0,1,0] [7,5,5]。最后P2满足。安全序列为P1→P3→P4→P0→P2。4.2 用代码自动搜索安全序列手推容易漏掉分支用回溯法可以找到所有安全序列def find_safe_sequences(available, allocation, need, n): 回溯搜索所有安全序列 results [] def backtrack(avail, finished, path): if len(path) n: results.append(path[:]) return for i in range(n): if not finished[i] and all(need[i][j] avail[j] for j in range(len(avail))): # 假设进程i执行完 new_avail [avail[j] allocation[i][j] for j in range(len(avail))] finished[i] True path.append(fP{i}) backtrack(new_avail, finished, path) path.pop() finished[i] False backtrack(available[:], [False]*n, []) return results 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]] seqs find_safe_sequences(available, allocation, need, 5) print(f共找到{len(seqs)}个安全序列) for s in seqs[:5]: print( → .join(s))逻辑说明backtrack函数尝试每个未完成的进程如果它的Need ≤ 当前Available就假设它执行完并释放资源递归搜索。参数说明available是初始可用资源allocation和need是二维列表。运行结果会输出所有安全序列你可以对照手算结果检查是否漏掉了某些分支。注意如果题目只要求判断是否安全找到一个安全序列即可如果要求所有安全序列必须用回溯。4.3 死锁检测与银行家算法的区别很多人把死锁检测和银行家算法混为一谈。银行家算法是预防死锁在分配资源之前先判断是否会导致不安全状态死锁检测是允许系统进入死锁状态然后定期检查是否存在循环等待。死锁检测用资源分配图化简法找到既不阻塞又非孤立的进程节点去掉它的所有边重复直到所有边都能去掉无死锁或者剩下无法化简的节点有死锁。考试时注意题目问的是“预防”还是“检测”两者的解题路径完全不同。5. 避坑与排查课后答案使用中的五个常见问题5.1 现象PV操作题手算结果和代码模拟不一致原因P操作的顺序写反了。在生产者-消费者问题中必须先执行资源信号量的P操作empty或full再执行互斥信号量的P操作mutex。如果反过来当缓冲区满时生产者先拿到mutex再等待empty消费者无法进入临界区释放空位直接死锁。解决记住口诀“先资源后互斥”用代码模拟验证如果程序卡住不输出大概率是P顺序问题。5.2 现象LRU页面置换手算缺页次数比标准答案多原因只在缺页时更新页面的最近访问时间导致命中时没有刷新时间戳LRU退化成了FIFO。解决在表格中给每个物理块维护“最近访问序号”列每次访问后无论是否缺页都更新该页面的序号。用代码验证时检查recent[page] i这行是否在if page not in memory外面。5.3 现象银行家算法找不到安全序列但答案说存在原因在搜索可执行进程时只检查了Need的第一个分量没有检查所有资源类型。比如Need[1,2,2]Available[3,3,2]第一个分量1≤3满足但必须所有分量都满足才能执行。解决用all(need[i][j] avail[j] for j in range(len(avail)))做全分量检查。另外注意Available更新时要加上Allocation而不是Need。5.4 现象磁盘调度SCAN算法的移动距离算多了原因把磁头折返后的回程距离重复计算了。SCAN算法中磁头从当前位置向一个方向移动服务沿途所有请求到达该方向最远请求后折返折返过程中服务反向的请求。移动距离是“去程距离 回程距离”但回程只算到最后一个请求不是算到磁盘边界。解决先确定折返点该方向最远的请求磁道号去程距离 |折返点 - 当前磁道|回程距离 |折返点 - 反向最远请求磁道|。5.5 现象概念题答案背了但换个问法就不会原因只记了结论没记推导过程。比如“为什么引入线程”这个问题答案可能写“提高并发性、减少切换开销”但如果不理解进程切换需要切换页表、刷新TLB而线程切换只需切换栈和寄存器换个问法“线程切换比进程切换快在哪里”就答不上来。解决每道概念题都追问一层“为什么”把答案背后的机制用自己的话复述一遍能画图就画图。6. 把课后题变成可迁移能力的进阶练法课后答案的终极用法不是对答案而是把每道计算题改造成一个小实验。我自己的习惯是每做完一章习题挑三道计算题用Python把算法实现一遍然后改参数观察行为变化。比如页面置换题把物理块数从3改成4、5画出缺页率随物理块数变化的曲线你会直观看到“Belady异常”——FIFO算法在某些访问序列下物理块增加反而缺页率上升。这个现象教材上只有一句话但亲手跑出来印象完全不一样。再比如PV操作题把生产者-消费者的缓冲区大小从5改成1观察信号量初值的变化把单生产者单消费者改成多生产者多消费者观察mutex是否仍然必要。这些改动会让你理解信号量初值不是随便设的它对应的是实际资源的数量。还有一个进阶练法是把银行家算法的安全序列搜索改成“找所有安全序列”然后统计不同资源分配策略下安全序列的数量。安全序列越多说明系统越灵活安全序列越少说明系统越脆弱。这个指标在实际的云资源调度里是有对应概念的——资源分配的灵活性直接影响调度器的吞吐量。我自己的教训是当年学这门课的时候把课后答案抄了三遍考试拿了高分但后来做操作系统相关的项目时连一个简单的线程池信号量都调不对。后来重新把PV操作题用代码跑了一遍才真正理解“信号量是资源计数器”这句话的含义。所以如果你现在正在学这门课别急着对答案先让代码跑起来。希望帮到你。本文还有配套的精品资源点击获取
返回列表