ARTICLE DETAIL

资讯详情

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

C++实现时间片轮转与SJF进程调度模拟:原理、代码与课程设计实战

C++实现时间片轮转与SJF进程调度模拟:原理、代码与课程设计实战 项目标题: 基于时间片轮转和SJF的进程调度系统的模拟设计2操作系统C(设计源文件万字报告讲解)支持资料、图片参考_相关定制_文章底部可以扫码掐指一算又到了操作系统课程设计的高峰期。每年这个时候总有不少同学被进程调度算法折磨得睡不着觉——时间片轮转、SJF、优先级调度这些概念在课本上读得明明白白一上手写代码就变成“脑子会了手不会”。今天我就以过来人的身份把这个经典题目彻底拆开揉碎讲清楚用 C 写一个进程调度模拟系统到底要做什么、怎么做、为什么这样做以及报告和答辩环节怎么准备才不会被老师问倒。这个项目本质上是用 C 模拟操作系统内核的进程调度行为——不是去改内核也不是真的去调度 CPU而是把进程抽象成数据结构把调度算法实现成逻辑代码在一个可控的模拟环境里观察程序的执行过程。你可以把它理解成一个“沙盘推演”先定义一批进程到达时间、服务时间然后分别用时间片轮转Round Robin和短作业优先SJF两种算法去“模拟运行”它们最后统计出平均周转时间、平均等待时间这些硬指标用数据说话说明两种算法的优劣和适用场景。这套东西适合谁操作系统课程正在学进程管理这一章的同学马上要交课程设计的在校生以及想补一补调度算法底层逻辑的自学者。读完这篇文章你不仅能把代码写出来、跑出结果还能搞懂背后的原理和报告答辩的套路。1. 项目解读这个课程设计到底要你做什么1.1 从标题拆出真实需求很多同学拿到题目第一反应是去搜代码但我觉得第一步应该是把题目翻译成人话。标题里三个关键词要拆开看时间片轮转RR每个进程轮流占用 CPU 一小段时间用完就排到队尾循环往复。SJFShortest Job First每次从就绪队列里挑“预计运行时间最短”的进程先执行。模拟设计不是实现真实调度器而是在程序里模拟调度过程输入进程参数输出调度序列和统计结果。再看交付物设计源文件 万字报告 讲解。这三件套说明老师要的不只是代码而是完整的“设计—实现—分析—表达”闭环。代码是干活的报告是说明白的讲解是讲清楚的。有同学只顾着写代码报告随便凑两页答辩时支支吾吾最后分不高——问题就出在没理解这个项目的完整要求。1.2 为什么选 C 做模拟器有人会问Python 写起来不是更简单吗确实Python 写模拟器会更快但操作系统课程设计用 C 有它的道理第一C 提供了完整的 STL 容器queue、vector、priority_queue这些正好对应调度算法里的数据结构需求写代码很顺手。第二C 更贴近底层思维用类去抽象 PCB进程控制块、用指针去传递进程对象这种思维方式跟操作系统底层实现是同一个套路。第三课程考核场景下C 是大多数学校 OS 实验的首选语言参考代码多、调试经验足、老师也熟悉。当然如果你私心想用 Python 写原理完全一样只是《万字报告》里可能要额外解释一下为什么选 Python。我这里按 C 来讲因为这是题目默认的语境。1.3 整体架构怎么搭才不乱模拟器程序不能一上来就堆代码。我建议按照“数据层—调度层—统计层—展示层”四层来组织工程数据层定义进程结构体或类维护进程列表负责读取或生成测试数据。调度层实现 RR 和 SJF 两种调度算法输入就绪队列输出调度事件序列。统计层根据调度事件计算周转时间、等待时间、响应时间、CPU 利用率等指标。展示层把调度过程打印成甘特图或者时间线表格把统计结果汇总成报告表格。这样分层的好处是每种算法只需要实现一个调度接口新增算法时不用动其他模块统计逻辑独立算法跑完自动算指标展示部分想从控制台打印改成 CSV 输出或者 GUI 都很容易。我自己做的时候先是把结构体定义和公共函数写好再分别实现两种算法最后才做统计和输出这样调试时可以一个模块一个模块地验证。2. 核心算法RR 和 SJF 的原理、对比与设计取舍2.1 时间片轮转公平背后的复杂度时间片轮转的基本逻辑太简单了把进程排成一队按顺序每人分一个固定长度的时间片quantum用完就排到末尾。可就是这么简单的逻辑写代码时有一个隐藏细节特别容易出错——时间片和到达时间的关系。举个例子P1 服务时间 5P2 服务时间 3时间片设 2两个进程都在 0 时刻到达。调度过程是P1 跑 2 个时间单位还剩 3切换 P2 跑 2还剩 1再切回 P1 跑 2还剩 1再切 P2 跑 1 完成最后 P1 跑 1 完成。这个逻辑看起来顺但如果 P2 是时刻 3 才到达呢前 3 个时间单位里 CPU 只能给 P1而且 P1 第一次跑 2 个时间单位结束后就绪队列是空的此时不能“傻等”要检查是否有新进程到达同时判断当前进程是否运行完毕——这两个判断的先后顺序直接影响结果正确性。我见过的不少错误代码就是忽略了“CPU 空闲区间”和“进程到达时间的匹配”。正确的思路是维护一个全局时钟每次调度前把当前时刻之前到达的进程“释放”进就绪队列然后才从队列里取进程运行。时间片的设定也有讲究——太小会导致频繁切换系统开销大太大会退化成 FCFS先来先服务。我的经验是模拟时时间片取平均服务时间的 1/3 到 1/2 比较有对比价值。2.2 SJF理论最优背后的工程代价SJF 的思想一句话每次选“剩余时间最短”或“总服务时间最短”的进程运行。它有一个著名的性质在非抢占式且所有进程同时到达的情况下SJF 能保证平均等待时间最小。听起来很美但工程上有一个极其致命的问题——饥饿。想象一个场景一个需要运行 100 的大进程排在队列里后面源源不断来一批服务时间只有 1、2 的小进程。每次调度都选最短的大进程永远轮不上。这在真实的操作系统中是不可能接受的。所以教材里提到 SJF 的改进——老化技术Aging——在模拟器里也很值得实现等待时间每增加一定量就把进程的“优先级”或“等效长度”降低让长作业最终也能获得 CPU。实现 SJF 时选数据结构有讲究非抢占式 SJF 用priority_queue按服务时间排序就行抢占式 SRTF最短剩余时间优先则需要每次进程到达时重新评估剩余时间最短的进程这时priority_queue不好动态改我的做法是每次调度时从就绪队列扫描一遍选剩余时间最小的——模拟器规模不大扫描代价完全可接受还避免了复杂的数据结构操作。2.3 为什么把两个算法放同一个系统里对比单独实现一个算法没什么挑战这个项目的核心价值就在“对比”。同一个进程集合分别扔进 RR 和 SJF 里跑出来的平均周转时间、平均等待时间、响应时间会有明显差异指标RRSJF非抢占说明平均周转时间一般高于 SJF短周转时间 完成时刻 - 到达时刻平均等待时间一般高于 SJF最短等待时间 周转时间 - 服务时间响应时间很短可能很长响应时间 首次运行时刻 - 到达时刻公平性好差RR 人人有份SJF 可能饿死长进程抢占开销有切换开销小RR 频繁切换SJF 切换次数少这个表格是报告里最核心的图景也是答辩时老师最爱让你展开讲的内容。我的建议是设计测试用例时故意构造几组不同特征的数据——一组同时到达、一组陆续到达、一组长短混合这样两种算法的差异就能立体地呈现出来。3. 数据结构与核心实现要点3.1 PCB 设计把进程的一切装进一个类进程控制块是调度算法的操作对象字段设计直接决定代码好不好写。我的 PCB 类长这样struct PCB { int pid; // 进程编号 int arriveTime; // 到达时间 int serviceTime;// 总服务时间 int remainTime; // 剩余服务时间 int startTime; // 首次运行时间 int finishTime; // 完成时间 int waitTime; // 累计等待时间 int status; // 0-就绪 1-运行 2-完成 };serviceTime和remainTime分开存是必须的SJF 非抢占模式下需要按serviceTime排序但抢占地SRTF要看remainTimeRR 每次时间片耗尽要把remainTime减掉时间片长度。startTime用来算响应时间finishTime用来算周转时间waitTime则是每次进程被调度时累计当前时刻 - 上次运行结束时刻。我的一个教训是不要在 PCB 里放动态字符串之类的复杂成员这个结构体会被频繁拷贝、排序、入队保持轻量很重要。3.2 模拟时钟驱动还是事件驱动写模拟器前要想清楚用哪种时间推进方式。两种流派时钟驱动time-driven全局时间t从 0 开始每次 1检查当前时刻有没有进程到达更新队列状态再决定是否调度切换。实现直观但 CPU 空闲时还在逐秒空转效率低。事件驱动event-driven不挨秒过直接从队列里取下一个要运行的进程计算出它运行到某个时间点会触发什么事件时间片耗尽、运行完成、新进程到达直接跳到该时间点。效率高但逻辑更复杂。我的建议是初学者用时钟驱动因为逻辑更直观调试时打印每个时刻的状态变化一眼能看出问题在哪。性能完全够用——模拟几十个进程就算每秒循环一次也就循环几百次无压力。等代码跑通了再想挑战自己可以改成事件驱动版本这个优化点在报告里也是加分项。3.3 两种算法的调度主流程伪代码 关键细节RR 调度主流程queuePCB* readyQueue; // 就绪队列 int currentTime 0; while (!allFinished) { // 1. 把 currentTime 之前到达的新进程加到队尾 for (进程列表里的每个进程) if (进程到达且未入队且 currentTime arriveTime) readyQueue.push(进程); // 2. 队空则 CPU 空闲推进时间 if (readyQueue.empty()) { currentTime; continue; } // 3. 取队首运行 PCB* p readyQueue.front(); readyQueue.pop(); if (p-startTime -1) p-startTime currentTime; // 首次运行 int runTime min(timeSlice, p-remainTime); p-remainTime - runTime; currentTime runTime; if (p-remainTime 0) { p-finishTime currentTime; // 统计周转、等待 } else { readyQueue.push(p); // 时间片用完没跑完排到队尾 } // 注意进程运行期间可能又有新进程到达要在下轮循环开始前释放 }这里有个必须强调的细节每次循环第一步把已到达的进程释放入队但如果队列取出的进程需要连续运行多个时间片比如时间片设为 4但当前进程运行 2 就完成了运行期间到达的新进程会在下一次外部循环时被释放。为了保证逻辑严谨有些实现把“运行期间检查新进程到达”嵌入到每个时间片内部——这也是一开始就上难度的地方建议先把外部循环版本跑通再优化。SJF 调度主流程非抢占式// 每次调度时扫描就绪队列找 serviceTime 最小的进程 PCB* selectShortestJob(vectorPCB* readyList) { PCB* best nullptr; for (auto p : readyList) if (!best || p-serviceTime best-serviceTime) best p; return best; }非抢占 SJF 的逻辑比 RR 还简单进程一旦运行就一直运行到结束除非新进程到达时我们采用“抢占式 SRTF”。我这里提一句题目要求“SJF”默认是非抢占式但如果你的报告里能主动做“非抢占 SJF 可抢占 SRTF 对比”老师会觉得你有深度。SRTF 的实现很像 RR只是每次运行后要检查新的短作业是否到达如果有剩余时间更短的就切换。4. 从零到一完整实操过程记录4.1 环境准备与工程结构开发环境不用花哨本地装gMinGW 或者 Linux 带 GCC 都行配一个 VS Code 或 CLion 就够了。Windows 下用 Dev-C 也不是不行但那个调试体验确实差点意思。如果你的 VSCode 还没配好 C 环境花二十分钟把g路径配进环境变量装好 C/C 扩展插件 教程网上大把 。工程文件结构我建议这样组织process_scheduler/ ├── main.cpp // 入口读取数据调用调度器 ├── pcb.h // PCB 结构体定义 ├── scheduler.h // RR SJF 调度函数声明 ├── scheduler.cpp // 调度算法实现 ├── stats.h // 统计计算接口 ├── stats.cpp // 统计实现 └── test_data.txt // 测试数据进程编号 到达时间 服务时间分文件的好处是报告里可以逐模块描述答辩时也可以指着代码讲清晰。不过如果你嫌麻烦全部塞进main.cpp也不是不行我们当年也有人这么干只是代码一长自己都找不到函数在哪。4.2 测试用例设计让数据会说话这一步非常关键因为模拟器的“说服力”全靠测试数据。我设计了三组数据测试组 A所有进程同时到达0 时刻5 个进程P1(0,8) P2(0,4) P3(0,9) P4(0,5) P5(0,2)测试组 B陆续到达长短混合P1(0,7) P2(2,4) P3(4,1) P4(6,3) P5(8,6)测试组 C极端大小对比P1(0,20) P2(1,1) P3(2,1) P4(3,1) P5(4,1) P6(5,1)组 C 专门用来展示 SJF 的饥饿问题——P1 这大胖子在非抢占 SJF 里可能排到最后才运行。这些数据要同时喂给 RR 和 SJF 两套调度流程保持输入一致输出对比才公平。时间片我设了 2、3、4 三档RR 每组数据都要跑三次看不同时间片对结果的影响。4.3 运行结果与指标统计真实示例下面是我运行一组测试数据P1(0,8)、P2(0,4)、P3(0,9)、P4(0,5)、P5(0,2)时间片3得到的输出为了方便阅读做了整理RR 调度序列时间片3时刻 0~3: P1 运行队列: P2 P3 P4 P5 时刻 3~6: P2 运行队列: P3 P4 P5 P1 时刻 6~8: P3 运行还差 7队列: P4 P5 P1 P3 时刻 8~11: P4 运行还差 2队列: P5 P1 P3 P4 时刻 11~13: P5 运行 2 完成队列: P1 P3 P4 ... 最终P1完成时刻 24P2完成时刻 15P3完成时刻 27P4完成时刻 21P5完成时刻 13SJF 调度序列非抢占按照服务时间排序P5(2) → P2(4) → P4(5) → P1(8) → P3(9) 调度顺序就是 P5 → P2 → P4 → P1 → P3 最终完成时刻P52P26P411P119P328然后算指标指标RR (q3)SJF平均周转时间(2415272113)/5 20.0(26111928)/5 13.2平均等待时间平均周转 - 平均服务 20 - 5.6 14.413.2 - 5.6 7.6平均响应时间首次运行-到达RR 各进程大约 0,3,6,9,12 → 平均 6.0按顺序依次 0,2,6,11,19 → 平均 7.6很明显SJF 在这组数据上平均周转和平均等待全面胜出但响应时间差一些。特别是最后一个 P3它在 SJF 下要等 19 个时间单位才第一次运行这对交互式系统是灾难。而 RR 的响应时间分布均匀没有人会等太久。这就是为什么“批处理系统偏爱 SJF分时系统偏爱 RR”的原因——数据摆出来结论就水到渠成。4.4 统计时要小心的定义统计代码看似简单但“周转时间”“等待时间”的定义如果搞错结果全崩。周转时间 完成时刻 - 到达时刻不是从 0 开始减等待时间 周转时间 - 服务时间前提是没有抢占如果有抢占还要累加被中断的时间响应时间 首次运行时刻 - 到达时刻很多同学报错数据就是因为忘了进程不一定在 0 时刻到达导致周转时间被算大了。建议在代码里用结构体数组保存每个进程的startTime、finishTime统计时统一从这些字段推导不要手工算。5. 常见问题与调试技巧实录5.1 调度序列算不对的四个高频原因根据我自己的经验和帮学弟学妹调代码的经历结果不对90%是下面这四个原因之一记录症状可能原因排查方法周转时间异常大到达时间处理错了把从 0 开始当成了从 CPU 空闲开始打印每个进程的finishTime和arriveTime手工验证一次RR 轮转顺序不对时间片耗尽后没把进程放回队尾或者放到了队首加断点检查readyQueue在每次调度后的内容和顺序CPU 空闲区间丢失队列为空时没有推进currentTime导致永远卡死加一个if (readyQueue.empty()) { currentTime; continue; }分支多个进程同时到达次序乱释放入队和取队首的顺序写反记住先释放、再取队首同时到达的进程按照数组顺序入队即可5.2 SJF 饥饿与边界情况的防守SJF 模拟器还有一个很容易被忽略的点当就绪队列空但还有进程没到达时算法不能选择任何进程要直接跳到下一个进程的到达时间。我见过有人用while循环让时间 1 空转硬等——能跑但如果有一个进程 100 时刻才到达前面空转了 100 次效率难看虽然结果对。更优雅的方法是if (readyList.empty()) { // 找到下一个未到达进程的 arriveTime直接跳过去 int nextArrive INT_MAX; for (auto p : allProcesses) if (p-status 就绪 p-arriveTime nextArrive) nextArrive p-arriveTime; currentTime max(currentTime, nextArrive); continue; }这样的“时间跳跃”处理报告里也能写一句“采用事件驱动思想避免无谓的空转时间”。5.3 C 实现里的几个真实踩坑点坑一priority_queue的比较器方向搞反。C 的优先队列默认是“大顶堆”——优先级最大的在队首。如果你要按serviceTime最小优先必须自定义比较器struct CompareByServiceTime { bool operator()(PCB* a, PCB* b) { return a-serviceTime b-serviceTime; // 注意是 生成最小堆 } }; priority_queuePCB*, vectorPCB*, CompareByServiceTime pq;这个的写法让很多人懵记住priority_queue里的比较器返回 true 表示“a 的优先级低于 b”因此想让最短作业在队首就得让a-serviceTime b-serviceTime返回 true 时把 a 往后排。我当年因为这个 bug 查了一晚上输出全是降序排列。坑二用queuePCB而不是queuePCB*导致拷贝开销大还改不了原数据。进队列一定要存指针否则队列里是拷贝副本更新remainTime时根本改不到原始进程对象统计结果全是错的。坑三忘记处理服务时间剩余为 0 的进程。比如时间片 3进程剩 2 就完成了运行时间应该取min(timeSlice, remainTime)不能机械地跑满 3否则remainTime变成负数后面的统计全乱。坑四浮点输出精度。平均周转时间如果要求保留两位小数用std::fixed std::setprecision(2)别裸用cout默认精度会被老师挑刺的。6. 万字报告与答辩讲解的备战策略6.1 报告结构怎么搭才像“万字”很多同学写报告苦于没话讲其实只是没掌握扩写的节奏。我的建议是七章结构每章重点锁定目录参考绪论背景、目的、意义——重点写“为什么要模拟调度算法”可以从真实操作系统调度器复杂性切入。相关知识基础进程调度基本概念、状态模型、RR/SJF 算法描述——每个术语配合公式周转、等待展开。需求分析功能需求、性能需求、运行环境——画出输入输出功能的文字描述。概要设计模块划分、模块关系、数据结构设计——把 PCB 类图、模拟器流程讲清楚。详细设计与实现重点章节逐模块贴代码、逐函数解读——这里是凑字数的主要阵地。测试与分析设计多组测试用例贴运行结果做对比分析——大量表格和截图是王道。总结与展望写“算法优劣总结”“模拟器局限性”“后续改进方向”如实现多级反馈队列。“万字报告”没那么玄按这个结构填充每个章节里加入关键代码片段、运行截图、对比表格再配上原理讲解8000~10000字自然就有了。核心原则是不要废话但要完整不要贴整段代码而无解释。每个函数至少让读者知道“输入什么、做什么、输出什么”。6.2 答辩高频问题与应对思路老师提问基本集中在“原理理解”和“实现细节”两类。我整理几个高频问题问为什么 SJF 平均等待时间最短能不能从数学上解释一下答在非抢占且同时到达的情况下可以交换论证——如果两个作业 A 和 B 相邻A 先 B 后拿 A 和 B 互换对比总等待时间变化可以证明按服务时间升序排列时总等待时间最小。这就是短作业优先“最优性”的直觉来源。问时间片对你的结果影响大吗答大。时间片偏小上下文切换开销上升但响应时间改善时间片偏大算法向 FCFS 退化。我的实验里 3 和 4 的结果差异明显报告中有一组时间片扫描数据。问你的模拟器和真实操作系统进程调度有什么区别答真实内核要考虑中断处理、I/O 阻塞、进程优先级动态变化、多核负载均衡等模拟器做了大量简化忽略了切换开销、假设进程纯 CPU 计算、没有 I/O 等待。模拟器用于理解算法主线不能直接搬到真实内核。问如果进程有优先级怎么办答可以在 PCB 里加priority字段把调度器核心从“按服务时间选”改成“先按优先级选、再同优先级按时间片轮转”甚至可以模拟多级反馈队列 MLFQ——这是一个非常好的加分扩展点。问你遇到过最难调试的 bug 是什么答可以如实回答priority_queue比较器方向写反导致排序反了或者进程释放入队和取队首顺序问题——真实经历比背稿更有说服力老师也更喜欢听你踩坑的故事。6.3 讲解环节怎么把 5 分钟讲出彩如果有讲解环节我的建议是不要照读报告要用“故事线”第一步用 30 秒说背景操作系统的进程调度就像餐厅排号RR 是每桌固定吃 3 分钟就换下一桌SJF 是先让吃得快的客人先吃两种方案各有代价。第二步用 1 分钟展示系统输入测试数据跑一遍 RR再跑一遍 SJF观察调度序列的差异。第三步用 1 分钟念两句指标SJF 平均周转低但响应差RR 反过来——然后引出“场景决定算法选择”的结论。第四步用 30 秒说一个你踩过的坑比如时间片和剩余时间的边界处理证明你真的做过。这套流程讲下来即使代码有小瑕疵老师也会觉得你理解了项目本质。结尾一点真心话这就是我做完这个项目后最想跟你分享的东西。回头看看这个课程设计真正的难点从来不是“敲代码”而是把一个看似简单的算法写成能在边界条件下正确运行的模拟器并且用数据和图表说服别人你的系统是有效的。我在调试过程中印象最深的一个 bug就是RR的时间片设成 3有一个进程剩余时间只剩 1我却让它硬跑了 3 个时间单位结果后面所有进程的完成时间整体偏移——这个错误让我明白一个道理模拟器的每一个时间推进都必须基于“真实时间轴”而不是想当然的整数跳。最后给你一个实用小建议写代码的时候每实现完一个功能模块就立刻做一个小实验验证它别攒到最后再联调。你看着自己写的 RR 调度器在控制台里一行行打印出 P1、P2、P3 的轮转序列真的会感觉自己离操作系统内核近了一大步。这个项目做完你对“进程调度”的理解绝对比光看十遍课本都要深。祝你的报告一稿通过答辩发挥出色。
返回列表