操作系统课设实战:调度、页置换、死锁避坑与答辩要点

发布时间:2026/9/28 15:13:40
操作系统课设实战:调度、页置换、死锁避坑与答辩要点
简介由张亚涛老师整理的操作系统课程设计实验报告面向高校计算机相关专业学生与操作系统初学者覆盖进程管理、内存管理、文件系统、设备管理、死锁处理、安全机制与性能调优等核心实验帮助学生把PV操作、页表、缺页异常、i节点、中断驱动I/O等抽象概念落到实处。压缩包共57个文件约3.14MB以36张实验截图和结果图为主配合8份Markdown实验报告及若干xml/json/suo等工程配置与说明文件直观呈现每个实验的代码实现、运行结果和关键操作。目前已有102人学习下载尤其适合操作系统实验课、课程设计与期末复习也可作为考研复试的参考资料。报告不仅给出各实验的设计思路与步骤还包含系统调用号、关键数据结构、makefile等细节截图读者可对照排查问题、复现实验快速形成从理论到实践的完整认知。1. 操作系统课程设计实验报告从理论课到能答辩的 30 天闭环操作系统课程设计最反直觉的一点是很多同学以为难在算法原理实际全部翻车在环境、输出和复现。拿到《张亚涛老师操作系统课程设计实验报告.zip》这类打包文件里面通常不是一篇文档而是一整套实验工程进程调度、页面置换、死锁避免、同步互斥外加跟代码对得上的运行日志和截图。反映到交付物上就是从源码能跑、报告能讲、答辩能圆回来。这篇文章写给正在赶课设的在校生也适合想用最短路径把操作系统核心机制落地验证的开发者。全篇按“选型 → 写码 → 调参 → 验证”的顺序展开照着一节一节做完四个实验大约需要两个周末时间紧就直接改每章的代码和参数跑自己的数据但答辩时每段逻辑都得讲得清。2. 进程调度实验先定“验证”还是“仿真”再选算法2.1 先定性质验证型程序才是课设的稳定路线动手写代码之前先回答一个问题这个实验是“验证算法”还是“仿真系统”验证指给定一组进程的到达时间、运行时间按调度算法推导出调度序列、完成时间、周转时间和带权周转时间仿真则要考虑就绪队列、IO 等待、上下文切换开销甚至进程随机到达。绝大多数课程设计考核的是前者但不少同学会把代码写成四不像加了随机到达又没做统计答辩时老师问“这组数据怎么来的”就直接卡住。我一般建议本科课设选验证路线。原因有二第一课程设计考核的核心是你对算法的理解验证型代码逻辑直观容易讲清楚第二验证型程序输入输出可控报告能稳定复现。仿真型程序往往因为随机性导致每次运行结果不同截图只能截一次“复现”就变成了说不清的事。这个选择定了后面所有参数设计都有依据。从检查角度看验证型程序还有一个隐性优势出了问题能一步步对着手算找错。仿真程序引入了上下文切换时间、IO 等待分布这类变量后出错时很难区分是算法问题还是仿真参数问题。课设时间有限没必要给自己加难度。2.2 算法选型FCFS RR 是性价比最高的组合常见调度算法和课设适配度如下表算法适合对比的指标课设难度主要坑位FCFS 先来先服务周转时间低到达时间非零时的空闲区间SJF 短作业优先带权周转时间中非抢占改抢占时条件写反RR 时间片轮转周转时间、切换开销中时间片过小导致切换频繁优先级调度等待时间高优先级反转和静默等待最常见的组合是 FCFS RR或者 SJF RR。RR 是分时系统的基础FCFS/SJF 能体现算法选择带来的指标差异。选两个算法还有一层原因报告对比表只放一组数据没什么可讨论两组以上才能说明“调度策略对系统指标的影响”。有些学校要求实现三个算法那就把 SJF 也加上SJF 的核心逻辑只是在插入就绪队列时按运行时间排序代码量增加不大。如果是第一次做课程设计我推荐 FCFS RR。FCFS 只考虑到达顺序代码最简单适合用来熟悉整个流程RR 引入时间片概念能引出“时间片大小对平均周转时间影响”这个参数分析答辩时老师最喜欢围绕这个问题追问。2.3 可改参数的 RR 调度器Python 最小版本先给一份能直接运行的 RR 调度器输入是进程序列和时间片大小输出包含完成时间、周转时间、带权周转时间和调度序列。代码保持最小结构方便你按自己的报告要求修改。# rr_scheduler.py —— 时间片轮转调度模拟 # 输入进程列表元素为 (到达时间, 运行时间) # 输出完成时间、周转时间、带权周转时间、调度序列 def rr_schedule(procs, quantum): # 按到达时间排序保证进程按到达顺序进入队列 procs sorted(procs, keylambda x: x[0]) n len(procs) remain [p[1] for p in procs] # 剩余运行时间 arrive [p[0] for p in procs] # 到达时间 finish [0] * n # 完成时间 cur 0 # 当前时刻 idx 0 # 下一个到达进程的下标 ready [] # 就绪队列存进程下标 seq [] # 调度序列便于画甘特图 while True: # 当前时刻前到达的进程全部进入就绪队列 while idx n and arrive[idx] cur: ready.append(idx) idx 1 # 就绪队列为空跳转到下一个进程到达时刻 if not ready: if idx n: cur arrive[idx] continue break pid ready.pop(0) # 取出队首进程 run min(quantum, remain[pid]) # 剩余不足一个时间片则一次跑完 cur run remain[pid] - run seq.append((pid, cur)) # 进程运行期间新到达的进程入队 while idx n and arrive[idx] cur: ready.append(idx) idx 1 if remain[pid] 0: ready.append(pid) # 未结束排到队尾 else: finish[pid] cur print(进程\t完成时间\t周转时间\t带权周转) for i in range(n): tat finish[i] - arrive[i] w tat / procs[i][1] print(fP{i}\t{finish[i]}\t\t{tat}\t\t{w:.2f}) print(调度序列, seq) if __name__ __main__: # 测试数据P0 时刻0到达运行4P1 时刻1到达运行3P2 时刻2到达运行5 procs [(0, 4), (1, 3), (2, 5)] rr_schedule(procs, quantum2)逻辑说明这段代码用两个while循环分别处理“新进程入队”和“进程跑完一个时间片是否续跑”。出队用ready.pop(0)进程没跑完就追加到队尾完美体现轮转特性。run min(quantum, remain[pid])处理剩余的边界情况避免把不需要的时间片继续计算进去。开头加一行sorted是为了防止你手动输入时没按到达时间排序。参数说明quantum是唯一需要调整的参数连续改成 1、2、4、8 跑同一组数据并记录平均周转时间报告里的“参数影响分析”就有着落了。procs列表的每一项是(到达时间, 运行时间)添加新数据时保持格式一致。如果你想对比 FCFS可以把quantum设成一个很大的值比如 99999这样每个进程一次跑完结果就是 FCFS 的表现不需要另写一份代码。2.4 时间片参数分析1、2、4、8 跑一轮用上面代码把quantum依次改成 1、2、4、8整理成一张表时间片平均周转时间最大完成时刻调度序列长度1数据A数据B长2数据C数据D中4数据E数据F短8数据G数据H更短一般得到的规律是时间片增大平均周转时间先下降后趋稳但短作业的响应时间会变差。原因是时间片变大后面到达的短作业要等更久才能再次获得 CPU。把这个“先降后稳”的趋势写进报告再解释一句“这就是时间片轮转作为分时策略的本质”这一段就完整了。如果跑出来的趋势和教科书写的不一样也别慌。这组进程序列本身就比较特殊比如长作业先到且连续占用 CPU把甘特图逐段画出来就能看出来。参数分析的价值在于解释现象而不是追求结论一定单调把过程讲清楚反而是报告里最能体现思考深度的部分。2.5 答辩要看的三样东西调度序列、结果表、对比结论调度实验的输出部分建议按这个顺序组织第一行是测试数据的参数第二行是调度序列第三行是结果表。答辩老师侧重的就是“调度序列能否对上结果表”。输出里必须包含到达时间、运行时间、完成时间、周转时间、带权周转时间五列缺任何一项都会被追问。提示调度序列放在结果表前面人来核对时最好读。如果代码里不方便直接打印就单独写一段生成甘特图的逻辑报告里放三行图体现的是工程严谨性比贴一大堆文字有效。3. 页面置换实验LRU 怎么调缺页率怎么验3.1 三种策略的本质差异对“未来”的预判能力不同页面置换实验的核心指标是缺页率即访问序列中未能命中内存的页面次数占比。FIFO、LRU、OPT 的差异本质上是“对未来的预判能力”不同FIFO 只看页面进入内存的顺序完全不考虑访问频率LRU 用“最近最久未使用”来近似模拟“未来最不可能被使用”OPT 则开了天眼能看到整个访问序列淘汰未来最晚才会被访问的页。记住这条主线代码和报告都不会跑偏。实现时有一个容易被忽略的约定初始加载阶段算不算缺页。很多教材把“页框为空时首次装入”也算作一次缺页也有教材默认所有页框已预置好页面、只统计访问序列执行期间的缺页。这个口径不统一会导致程序结果和手算结果对不上所以动笔前先在报告里写明你的定义。3.2 LRU 实现用列表顺序模拟访问时间LRU 标准实现是哈希表加双向链表但课设场景不需要那么复杂。直接用一个有序列表就能把原理讲清楚列表尾部是最近刚被访问的页面列表头部是最久未被访问的页面。# lru_page.py —— LRU 页面置换模拟 # 输入页面访问序列、物理块数 # 输出每次访问后的内存状态、是否缺页、缺页率 def lru(pages, frames): mem [] # 头部是最久未用尾部是最近访问 page_faults 0 fault_flags [] for page in pages: if page in mem: # 命中把该页提到“最近刚访问”的位置 mem.remove(page) mem.append(page) fault_flags.append(False) else: # 缺页淘汰最久未用的页放入新页 page_faults 1 if len(mem) frames: mem.pop(0) mem.append(page) fault_flags.append(True) print(f访问 {page}: 内存 {mem} 缺页{是 if fault_flags[-1] else 否}) total len(pages) fault_rate page_faults / total * 100 print(f缺页次数{page_faults}缺页率{fault_rate:.2f}%) return fault_flags if __name__ __main__: # 经典参考序列物理块数 3 pages [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1] lru(pages, frames3)逻辑说明mem.remove(page)加mem.append(page)的组合相当于把命中页面从原位置抽出来放到尾部。缺页时pop(0)淘汰头部也就是最久未使用的那一页。整个实现时间复杂度 O(n)但课设访问序列一般只有 20 项左右完全够用。参数说明把frames改成 4、5 再跑一遍记录缺页率的下降情况就是报告需要的“物理块数对缺页率影响”。pages可以替换成自定义序列但页面号要是整数且取值范围要大于frames否则每个页面都能常驻内存实验就失去意义了。3.3 FIFO 对照跑出 Belady 异常才算完整Belady 异常指物理块数增加后缺页次数反而增加的现象只出现在 FIFO 策略里。很多同学第一次跑出来以为是代码 bug其实算法特性就是这样。验证方法是拿经典序列1 2 3 4 1 2 5 1 2 3 4 5分别用frames3和frames4跑 FIFO经典结果是缺页次数 9 比 10块数增加反而多了一次缺页。把这个结果放进报告在讨论部分补一句“LRU 属于栈式算法不会出现 Belady 异常”这一组实验的深度立刻就有了。FIFO 的实现非常简单缺页时直接淘汰最先进入的页面不需要任何顺序调整。两段代码放在一起对比读者能直观看到 LRU 多出来的“最近使用记录”到底起了什么作用。3.4 同序列不同策略的缺页率对比表做对比实验时建议把三种策略放在同一个序列上跑输出一张统一形式的结果表置换策略frames3 缺页次数frames4 缺页次数是否出现 BeladyFIFO数值数值可能LRU数值数值否OPT数值数值否这张表的重点是“同序列、同口径、不同策略”。答辩时拿这张表说事逻辑链完整FIFO 有 Belady 现象LRU 没有原因是 LRU 的淘汰集合是栈式集合历史信息保留了更完整的“最近访问”记录。到这里页面置换实验的深度就不只是“会跑代码”了。4. 死锁避免与同步互斥银行家算法和生产者-消费者的实现要点4.1 银行家算法Need 矩阵算错安全序列必错银行家算法实现不难但有两个高频错误点一是把Max当Need用二是找到可执行进程后没有从头重新扫描。正确写法是安全性检测每轮从第一个进程开始扫描找到一个Need Work的进程就执行并回收资源然后再从头扫。下面这段代码演示了最标准的实现# banker.py —— 银行家算法安全性检测 # 输入available(可用资源), allocation(已分配), max_need(最大需求) # 输出是否存在安全序列以及安全序列内容 def safety_check(available, allocation, max_need): n len(allocation) m len(available) need [[max_need[i][j] - allocation[i][j] for j in range(m)] for i in range(n)] # Need Max - Allocation work available[:] finish [False] * n safe_seq [] for _ in range(n): found False for i in range(n): if finish[i]: continue # 判断剩余资源是否满足该进程全部需求 if all(need[i][j] work[j] for j in range(m)): for j in range(m): work[j] allocation[i][j] finish[i] True safe_seq.append(i) found True break if not found: return False, [] return True, safe_seq if __name__ __main__: # 经典参考数据5 个进程3 类资源 available [3, 3, 2] allocation [ [0, 1, 0], [2, 0, 0], [3, 0, 2], [2, 1, 1], [0, 0, 2], ] max_need [ [7, 5, 3], [3, 2, 2], [9, 0, 2], [2, 2, 2], [4, 3, 3], ] ok, seq safety_check(available, allocation, max_need) if ok: print(系统处于安全状态安全序列, seq) else: print(系统处于不安全状态无法找到安全序列)逻辑说明注意need矩阵是由Max减Allocation得到的这是银行家算法最容易被忽略的一步。外层for _ in range(n)控制最多执行 n 轮查找每轮都从 0 开始重新扫描全部进程避免上一轮执行顺序影响下一轮判断。found为假说明剩余进程没有一个能满足资源要求直接返回不安全。参数说明报告里除了跑经典数据建议构造一组需要三步以上才能找到安全序列的数据把每轮的work向量变化打印出来。这样能完整展示“回收资源、逐步推进”的过程答辩时这部分过程展示比只给一个“安全”结论有说服力得多。这个实验最常见的误用是把“死锁检测”当“死锁避免”写。银行家算法是避免核心是安全性检测死锁检测是允许死锁发生后找出并恢复。两者算法基础完全不同报告里如果概念混用老师容易直接判定不理解算法。4.2 生产者-消费者信号量初始值与加锁顺序生产者-消费者实验最容易出现“跑死”和“输出乱序”。核心三把信号量的初始值必须精确empty等于缓冲区容量full等于 0mutex等于 1。更重要的是加锁顺序P(empty)必须在P(mutex)之前V(full)必须在V(mutex)之后。# pc_semaphore.py —— 信号量实现生产者-消费者核心逻辑演示 import threading import time import random BUFFER_SIZE 5 mutex threading.Semaphore(1) # 互斥访问缓冲区 empty threading.Semaphore(BUFFER_SIZE) # 空位数量初始为容量 full threading.Semaphore(0) # 已占用数量初始为 0 buffer [] def producer(): for i in range(10): empty.acquire() # P(empty)申请一个空位 mutex.acquire() # P(mutex)进入临界区 buffer.append(i) print(f生产 {i}缓冲区 {len(buffer)}/{BUFFER_SIZE}, flushTrue) mutex.release() # V(mutex) full.release() # V(full)占用位 1 time.sleep(random.uniform(0.05, 0.2)) def consumer(): for i in range(10): full.acquire() # P(full)申请一个数据 mutex.acquire() item buffer.pop(0) print(f消费 {item}剩余 {len(buffer)}, flushTrue) mutex.release() empty.release() # V(empty)空位 1 time.sleep(random.uniform(0.05, 0.2)) if __name__ __main__: t1 threading.Thread(targetproducer) t2 threading.Thread(targetconsumer) t1.start() t2.start() t1.join() t2.join() print(全部执行完毕)逻辑说明P(empty)先于P(mutex)是经典顺序——先检查资源是否可用再检查缓冲区能否进入。flushTrue确保打印内容即时刷新避免两个线程的输出因为缓冲区积压而乱序。代码里sleep(random.uniform(...))用于模拟实际耗时如果后续要调试建议先把它改成固定值比如time.sleep(0.01)保证运行过程基本稳定可复现。信号量操作顺序写反会出现什么现象消费者先拿mutex再等full此时缓冲区为空消费者持锁等待数据生产者想往缓冲区放数据又拿不到mutex两边互等程序卡死。这是实验里最经典的死锁场景也是报告中值得单独写一段“信号量操作顺序的意义”的素材。参数说明把BUFFER_SIZE改小到 2、3缓冲区被占满和清空的频率明显增大能更直观看到“满时生产者阻塞、空时消费者阻塞”的切换。建议至少保留一组小缓冲区截图配合文字标注就是同步互斥实验最直观的论据。4.3 实验环境选择VMware 里跑 Ubuntu 的常见做法操作系统课设多数安排在 Linux 环境常见组合是 VMware 安装 Ubuntu Desktop 或 Server。虚拟机的价值在于快照回滚实验环境改坏了直接恢复快照不需要从头装系统。多线程实验在 Linux 上的行为比 Windows 稳定打印顺序更容易预测能减少环境相关的不确定因素。我的做法是两套环境并行本机写代码Ubuntu 虚拟机里跑正式测试。运行命令统一用python3 xxx.py测试数据放在同一个目录。答辩现场重跑只需要敲一条命令避免现场花五分钟配环境这种尴尬。如果你用的不是 Ubuntu麒麟、统信 UOS 这类国产系统在执行 Python 脚本层面没有差异注意把 Python3 环境和权限确认好就行。4.4 另一种等价的锁实现不用信号量也能过有些学校允许用threading.Lock代替Semaphore。等价写法是mutex用Lock()empty和full用Condition或Semaphore都行。mutex换Lock后要注意acquire不能重复调用否则同一线程会把自己锁死。信号量和锁在互斥用途上基本等价但信号量可以表示“可用资源数量”这是Lock做不到的。如果只是做对比实验建议两个版本都写信号量版用于讲同步计数逻辑Lock 版用于讲互斥访问。报告里写“两种实现对比”比单纯贴一种更有层次。5. 避坑清单操作系统课设报告被打回的 5 个真实原因以下几条是操作系统课程设计最常见的翻车点每条按“现象 → 原因 → 解决”给出。5.1 现象运行截图和代码在答辩现场对不上报告里贴的截图是一份数据现场重跑出来是另一份数据。这是被打回的第一大原因答辩老师只要用报告里的测试数据试一次就会发现。原因大部分是写报告时用了旧版本代码的运行结果。代码改过输入格式或者输出内容但没有重新生成截图。解决定稿前把每段代码的入口参数、运行命令、输出结果三者一起核对。运行命令建议重定向到文本文件保存python3 rr_scheduler.py rr_output.txt python3 lru_page.py lru_output.txt截图和文本文件一起放进报告附录保证答辩现场用同一份数据重跑时结果完全一致。5.2 现象多线程实验打印混乱甚至直接死锁现象是生产者-消费者的输出行里“生产”“消费”交叉乱跳偶尔程序卡住不再输出任何内容。原因一是mutex锁没有包住整个临界区导致缓冲区数据竞争二是信号量顺序写反。比如在full.acquire()之前先拿了mutex此时缓冲区为空消费者持锁等待数据生产者拿不到锁两边互等程序挂死。解决先检查信号量顺序再检查临界区范围。把生产者和消费者的 acquire/release 配成对逐行画出谁先谁后死锁位置立刻暴露。排查时给每个线程打印加上线程 ID能定位到具体卡在哪一行。5.3 现象缺页率结果和手算对不上理论课手算缺页率是某个值程序跑出来不一样翻来覆去检查算法也没找到 bug。原因缺页计数的口径不一致。“初始加载阶段算不算缺页”“物理块初始是否为空”“访问序列是否提前预热”不同教材定义不完全一致。程序实现和手算用不同口径结果当然对不上。解决在报告开头明确写清楚定义“本实验缺页指访问序列执行过程中发生的缺页中断初始加载阶段计入缺页次数物理块数为 3 时先加载前三页算三次缺页。”把口径写进代码注释和报告方法部分结果自然可复现。注意写代码时也要照这个口径实现别报告写一套、代码跑另一套。5.4 现象测试数据只有一组答辩时换数据就翻车报告从头到尾只有一组数据的输出代码也确实只测了这一组。答辩时老师随手给一组新数据程序可能直接报错或输出异常。原因不是代码不能用而是没有验证“算法具备通用性”。课程设计考核的重点是验证实现是否正确而验证的唯一证据就是在多组数据上结果一致。解决至少准备两组数据。一组是教科书经典数据用于和手算对照另一组是随机数据程序内用random并按固定种子生成既体现通用性又能复现import random random.seed(42) pages [random.randint(0, 9) for _ in range(30)]固定种子的作用是保证每次运行生成的序列一致。报告里放一张两个数据集的对比表说明算法在经典和随机场景下的缺页率差异这部分内容通常都是加分项。5.5 现象代码能跑但没有任何注释和中间输出代码功能正常但没有注释和关键步骤标识老师指出“这部分实现看不出来是 LRU”时只能点头。原因赶进度只写了实现没写过程和设计说明。答辩要求讲清楚“为什么这样设计”没有注释和中间打印现场讲不到三句就卡壳。解决从写代码第一天就给关键段落加注释重点四处输入数据结构、算法核心循环、边界条件分支、输出格式化。注释不需要长写清楚“这段为什么存在”即可。同时保留中间状态的打印开关比如 LRU 每次访问后的内存状态、RR 每次调度后的当前时刻。有这些辅助输出答辩时你顺着日志讲比对着空代码干讲顺畅得多。6. 验证实验报告是否合格的三个技巧把“跑通了”变成“可复现”6.1 技巧一构造最小破坏性测试用例验证实验代码有没有问题不是跑最长最复杂的用例而是跑最小破坏性用例。比如页面置换里用一个连续访问同一页的序列1 1 1 1 1如果程序输出缺页率 0 且内存状态始终只有一页说明“重复访问”处理正确。再比如调度实验构造 5 个同时到达的进程看 RR 和 FCFS 的结果差异是否合理。最小用例把边界逼出来比大数据更有说服力。把这个过程写进报告的“测试方法”部分能直接体现你的工程意识。6.2 技巧二把中间状态逐行打印和手算对齐模拟程序如果只输出结论在答辩时就是个黑匣子老师很难判断你是真懂还是碰巧跑通。我在每个模拟实验里都会保留一个打印中间状态的开关LRU 打印每次访问后的内存状态RR 打印每次调度后的当前时刻银行家算法打印每轮 work 向量变化。运行时一档一档看手动把前几步和程序输出对齐前面对上了后面基本不会错。6.3 技巧三录屏保留完整复现过程最后一步是录制一段 3 到 5 分钟的视频内容包括进入实验目录、依次运行两个程序、展示相同输入下输出一致、结尾展示与报告一致的截图文件。这段录屏既是留给自己的复现记录也是答辩出现意外时最能说明问题的材料。很多老师不会主动要求看但你在“附加验证材料”里提到它观感会比光说不练强很多。我在做操作系统课设时有条血泪经验有一回报告写完了答辩前一天改了一个参数忘记更新截图现场被老师当场点出对不上只能硬着头皮重讲。从那以后我养成了习惯——任何实验报告定稿前先把所有运行命令重新执行一遍输出文件按日期归档然后再截图写报告。算法可能不复杂但工程上的闭环和细节才是课设真正想训练的东西。希望这篇文章能帮到你把每份实验报告都做成经得起现场复现的成果答辩顺利。本文还有配套的精品资源点击获取