资讯动态

操作系统课程设计:用C++实现进程调度算法模拟器(FCFS/SJF/RR)

发布时间:2026/10/26 16:22:09 来源:尧图企业网站定制
简介一份面向操作系统课程设计与大作业场景的C进程调度模拟实现源码包覆盖动态优先级、先来先服务、时间片轮转三种经典调度算法适合计算机相关专业学生理解进程调度原理、完成课程报告或进行算法对比实验。压缩包共25个文件以源程序文件、工程配置文件、解决方案文件、可执行程序为主另含调试符号、构建日志、中间文件及说明文档整体仅有1.88MB结构紧凑。源码基于Visual Studio开发下载后用VS直接打开即可运行便于查看三种算法各自如何管理就绪队列、分配处理器并输出调度时刻表。已有4793人学习使用对正在准备操作系统课设或需要参考调度算法实现的读者具有较高参考价值也可作为复习进程管理章节的辅助素材。1. 操作系统课程设计里的进程调度模拟这道大作业到底在考什么如果你正在为操作系统课程设计的大作业选题发愁进程调度算法的模拟实现大概是被学长学姐推荐最多的一个方向。它不依赖真实硬件不要求你把Linux内核调起来只需要用C把FCFS、SJF、RR、优先级调度这些算法跑通最后输出一份可对比的调度结果。听起来不难但每年都有大量作业挂在同一批问题上代码写完了不知道结果对不对调度顺序讲不清楚进程状态管理一复杂就崩答辩时被老师追问一个“时间片到了谁先切换”就卡壳。这篇文章会从调度算法的选型逻辑讲起带你把PCB设计、核心调度循环、指标计算和文本甘特图输出完整过一遍。整份方案不依赖任何第三方库C11以上就能编译在Visual Studio或者Linux下用g都能跑。你既能拿来做课程设计源码提交也能在报告里把每个设计决策讲明白。文章结尾还会给出四组必测的边界输入用来验证你的模拟器在不同调度策略下是否真的正确——这一步才是课设拿高分的关键。2. 进程调度算法的选型哪些算法组合能在课设里讲出深度2.1 五种经典调度算法的对比表与选型逻辑进程调度算法是这道大作业的心脏。常见的做法是至少实现三种以上算法并保证它们共用同一套进程池和模拟框架。下面的表基本覆盖了课设最常见的算法组合算法核心决策依据抢占性主要指标优势明显缺陷FCFS按到达时间排队非抢占实现最简单、公平平均等待时间可能很高SJF非抢占到达后选服务时间最短非抢占平均周转时间理论最优长作业可能饥饿SRTF抢占式SJF每次新进程到达时比较剩余时间抢占平均周转时间更优切换开销大、长作业饥饿更严重RR就绪队列轮转抢占时间片到期响应时间可控时间片大小严重影响性能多级反馈队列按优先级分队列高队列时间片短抢占兼顾响应时间和吞吐参数调整复杂如果你是第一次做这个题我的建议是选FCFS、SJF、RR加上优先级调度这四种作为基础课设组合。四个算法能覆盖非抢占和抢占两类场景报告里的对比实验也能做出明确差异——比如FCFS的平均等待时间明显高于非抢占SJFRR的响应时间比FCFS好但上下文切换次数多。如果只做两个算法答辩时深度不够容易被问“你为什么不考虑多级队列”。如果贪多把六七个算法全塞进去光是调试抢占式调度的时间就够你通宵几次。优先级调度可以做得简单一点静态优先级数字越小优先级越高老化机制作为扩展点在报告里提一笔即可。2.2 统一调度器框架怎么让多种算法共用一套代码这四个算法看起来差异很大但从实现角度其实可以统一在一个调度器框架下。关键在于想清楚一个核心问题**谁来决定进程切换的时机**FCFS和SJF是“当前进程运行完才切换”RR和SRTF是“额外的事件触发切换”。我在课设里采用的做法是给每种算法实现两个回调函数shouldPreempt负责判断“是否有必要打断当前进程”pickNext负责从就绪队列里选出下一个要运行的进程。这样一个调度器核心循环就能写得很干净用当前时间推进进程到达时插入就绪队列运行态进程执行到“完成或时间片耗尽”然后判断是否切换。RR算法里shouldPreempt判断当前运行时长是否达到时间片SRTF里它判断就绪队列里是否出现了剩余时间更短的进程。这样四种算法不需要各自写一套主循环后面加新算法也只是新增两个函数。更重要的是答辩时你能直接指出这种设计的好处调度策略与模拟时钟解耦代码复用于是有了实打实的证据。3. 用C搭出模拟内核PCB结构与时间轴推进3.1 PCB的字段设计进程控制块应该装哪些东西进程控制块是整个模拟器的数据结构核心。课程设计里不需要仿照真实内核做得很复杂但关键字段一个不能少。我设计PCB时会区分两类字段进程的“静态属性”和“动态运行状态”。静态属性包括进程ID、到达时间、服务时间、初始优先级这些在读取输入时就固定下来动态状态包括剩余服务时间、当前优先级、进入就绪队列的时间、开始运行时间、完成时间、累计运行时间。struct PCB { int pid; // 进程ID double arriveTime; // 到达时间, 课设数据建议用整数或一位小数 double serviceTime; // 服务时间(总CPU时间) double remainTime; // 剩余服务时间, 初始等于serviceTime int priority; // 静态优先级, 数字越小优先级越高 double startTime; // 第一次获得CPU的时间, -1表示还没运行过 double finishTime; // 完成时间 double runTime; // 累计已运行时间 ProcessState state; // NEW / READY / RUNNING / TERMINATED int queueID; // 预留: 多级队列扩展用 };这个结构里容易踩坑的是时间字段的精度。如果你用double存时间就绪队列排序时比较到达时间要带上epsilon容错否则两个到达时间相同的进程先后顺序会因为浮点误差出现乱序。我一般用double存储但在比较函数里写fabs(a.arriveTime - b.arriveTime) 1e-9来判定相等。另外一个容易被忽略的是startTime的初始化。它必须与“进程是否已经开始运行”关联起来不是简单初始化为0。课设输出平均等待时间时要计算等待时间等待时间 第一次开始运行时间 - 到达时间所以这个字段没记准后面所有统计指标都是错的。3.2 模拟时钟的推进方式逐片扫描还是事件驱动时间推进是模拟器的第二个核心问题。新手最常见的写法是for循环里时间从0开始每个单位时间扫描一次所有进程。这种写法的缺点是当单位时间比较小时比如0.1秒一个时钟片而进程的服务时间又很大运行到几百个循环才能完成一道题数据一多就非常慢。我采用事件驱动的推进方式每轮循环里时间直接跳到下一个“有意义”的时刻。所谓有意义时刻就是有进程到达、或正在运行的进程完成、或RR时间片到期的时刻。// 调度器主循环伪代码 while (!allTerminated) { // 1. 把当前时刻之前到达的进程放入就绪队列 while (nextArrivalIndex processes.size() processes[nextArrivalIndex].arriveTime currentTime EPS) { readyQueue.push(processes[nextArrivalIndex]); nextArrivalIndex; } // 2. 如果CPU空闲且就绪队列非空, 直接选一个进程开始运行 if (currentProcess nullptr !readyQueue.empty()) { currentProcess pickNext(readyQueue); if (currentProcess-startTime 0) currentProcess-startTime currentTime; } // 3. 如果没有进程可运行, 把时间跳到下一个到达事件 if (currentProcess nullptr) { if (nextArrivalIndex processes.size()) { currentTime processes[nextArrivalIndex].arriveTime; } else { break; // 所有进程都已到达, 理论不会走到这里 } continue; } // 4. 计算下一个触发时刻: 完成、时间片到期、新进程到达(抢占算法需要考虑) double nextArrival (nextArrivalIndex processes.size()) ? processes[nextArrivalIndex].arriveTime : INF; double nextFinish currentTime currentProcess-remainTime; double nextPreempt currentTime timeSlice - currentProcess-runTime; // 5. 按算法类型决定时间推进到哪个时刻, 更新状态 advanceToNextEvent(/* ... */); }不要小看第3步那个“直接跳到下个到达时间”。如果不做这一步CPU空闲但当前时间还在逐个时钟片推进输出结果时你会看到一段冗长的“CPU空闲段”计算结果没问题但运行效率很差。事件驱动的好处是模拟器可以在毫秒级完成几百个进程的调度运算你在报告里可以自信地写“采用事件驱动而非逐周期扫描时间复杂度与事件数相关而非与总时长相关”。3.3 状态切换的四个关键动作调度器里所有算法最终都落在一组固定操作上当前进程被移出CPU、新进程被加载、进程状态更新、统计字段记录。这四个动作无论如何组合都不能绕开。我建议不要在每个算法里分别实现状态切换逻辑而是封装成switchProcess()函数这样能避免一个隐蔽问题进程完成和进程被抢占的收尾逻辑不一样。bool switchProcess(PCB* oldProc, PCB* newProc, double currentTime) { if (oldProc ! nullptr) { oldProc-runTime currentTime - oldProc-lastSwitchTime; if (fabs(oldProc-runTime - oldProc-serviceTime) EPS) { // 进程完成 oldProc-finishTime currentTime; oldProc-state TERMINATED; } else { // 进程被换出, 回到就绪 oldProc-state READY; readyQueue.push(oldProc); } } if (newProc ! nullptr) { newProc-lastSwitchTime currentTime; if (newProc-startTime 0) newProc-startTime currentTime; newProc-state RUNNING; } return true; }这个函数里有几个字段需要和3.1的PCB设计对应lastSwitchTime记录进程最近一次获得CPU的时刻。需注意进程被换出那一刻runTime需要累加上本次运行区间但remainTime也要同步更新我习惯用remainTime serviceTime - runTime而不是单独维护避免两个字段不一致。如果两个字段只用一个剩余时间判断出错会直接导致SRTF调度结果混乱。4. 算法实现的关键代码从FCFS到RR的调度顺序怎么算对4.1 FCFS与SJF的pickNext实现就绪队列的比较规则做过课设的应该都有经验FCFS和SJF的实现差异只在就绪队列的排序规则。FCFS按到达时间排到达时间相同的按pid排SJF按服务时间排服务时间相同的按到达时间排。有了3.2节统一调度器实现两个算法只是写两个比较函数的问题。// FCFS: 先到先得, 队首就是最早到达的 struct FCFSCompare { bool operator()(const PCB* a, const PCB* b) const { if (fabs(a-arriveTime - b-arriveTime) EPS) return a-arriveTime b-arriveTime; // 小顶堆: 到达时间小的在前 return a-pid b-pid; } }; // SJF非抢占: 在已到达进程中选服务时间最短的 struct SJFCompare { bool operator()(const PCB* a, const PCB* b) const { if (fabs(a-serviceTime - b-serviceTime) EPS) return a-serviceTime b-serviceTime; if (fabs(a-arriveTime - b-arriveTime) EPS) return a-arriveTime b-arriveTime; return a-pid b-pid; } };这几个比较函数就是整个模拟器的“调度策略插件”。把比较函数作为模板参数或者函数对象传进就绪队列main里声明不同的优先队列容器就能跑出不同算法。请注意这里的细节优先队列默认是“最大元素在顶端”所以比较函数的返回值含义需要仔细推理。我在代码里写的是“如果a应排在b后面则返回true”而operator的写法会让到达时间最小的进程待在队首——建议每个算法写完后先用一个小数据用例打印调度结果确定顺序符合预期再去跑大用例。非抢占SJF里的一个隐藏问题是就绪队列里的进程是“当前时刻已经到达”的而这个队列是动态变化的。也就是说CPU空闲时pickNext选出的进程与CPU运行时新到达的进程两者不会触发重新选择——这正是非抢占的定义。很多课设实现把SJF写成了伪抢占式每当新进程到达就重新比较这是错的。你的报告里需要明确区分这一点答辩时老师特别喜欢追问。4.2 RR轮转调度的实现时间片推进与队列尾部插入RR的代码量也不大但坑在细节。时间片到了之后当前进程如果没完成要被放到就绪队列尾部而不是重新参与比较。另外RR的模拟时钟推进到“时间片到期”这个事件点时要处理一个边界新进程在当前时间片结束前到达应等本轮时间片用完再切换还是立即切换理论上标准RR是等当前时间片用完再切换。但实际模拟中这个规则在课程设计报告里应当写明。我的实现里RR的版本采用了标准答案的写法每次调度时间为min(remainTime, timeSlice)到期时换人。新进程到达不抢占。// 在调度主循环的advanceToNextEvent中, RR分支如下: double duration std::min(currentProcess-remainTime, timeSlice); double nextEventTime currentTime duration; bool canRunFullSlice true; // 如果时间片未用完前有进程到达, 当前进程仍然继续运行, 但下一个事件时刻不变 // 推进时钟并更新运行状态 currentProcess-remainTime - duration; currentProcess-runTime duration; currentTime nextEventTime; // 更新完状态后, 如果运行中进程已完成 if (currentProcess-remainTime EPS) { markTerminated(currentProcess, currentTime); currentProcess nullptr; } else { // 时间片耗尽但未完成, 放入就绪队列尾部 readyQueue.push(currentProcess); currentProcess nullptr; }这段代码逻辑并不复杂真正容易踩坑的地方是第4行的nextEventTime——如果currentTime本身已经因为新进程到达而变过这里的计算不能直接用全局currentTime而应该用currentProcess-lastSwitchTime作为时间片起始点。另一个容易翻车的地方是运行时间的浮点累计如果在多次迭代里反复对remainTime做减法浮点误差会累积到让剩余时间变成0.000001这种诡异值。解决方式是当remainTime timeSlice * 1e-6时直接按进程完成处理而不是死等它减到0。4.3 指标计算周转时间和带权周转时间怎么统计做完调度模拟后输出指标是课设报告的硬通货。操作系统教材里定义的平均周转时间、平均带权周转时间是反映调度算法性能的两个最核心指标。计算它们需要一个前置条件每个进程的完成时间被正确记录。FinishTime在PCB里已经定义一轮调度跑完遍历所有进程统计即可。double calcAverageTurnaroundTime(const std::vectorPCB procs) { double sum 0.0; for (const auto p : procs) { sum p.finishTime - p.arriveTime; } return sum / procs.size(); } double calcAverageWeightedTurnaroundTime(const std::vectorPCB procs) { double sum 0.0; for (const auto p : procs) { sum (p.finishTime - p.arriveTime) / p.serviceTime; } return sum / procs.size(); }这两个函数本身不难但要注意输出格式。工程上习惯用%.2f保留两位小数因为课设报告的三线表里精确到0.01就足够说明问题。另一个隐藏点是——带权周转时间的定义分母是服务时间如果服务时间可以为0除零会让你程序崩溃。课设输入数据里建议限定服务时间大于0代码里加一行防御性校验在读取阶段就将非法数据报错。5. 进程调度模拟的避坑记录四条耗时一整晚的典型问题5.1 坑一SRTF抢占条件写反长进程永远抢占不到CPU现象用SRTF跑一组数据短进程一直运行长进程的完成时间特别大甚至输出结果和其他参考实现完全对不上。原因抢占式SJF的切换条件不是“新到达的进程服务时间比当前进程剩余时间短”而是“比当前进程的剩余服务时间短”。我调试时发现有些实现直接比较新进程的serviceTime和当前进程的serviceTime而不是比较剩余时间。这会让运行到一半的长进程因为“原始服务时间长”被反复换出产生饥饿。解决确保比较用的字段是remainTime而不是serviceTime。代码里在shouldPreempt函数中使用newProc-serviceTime currentProc-remainTime - EPS判断是否抢占。同时注意边界如果新进程的serviceTime与当前进程remainTime相等标准SRTF会维持当前进程继续运行。5.2 坑二时间片小到极端时RR的上下文切换次数异常高现象给RR设置时间片为0.1进程服务时间平均5跑完后输出结果里的切换次数达到几千次报告里画出的甘特图密密麻麻无法阅读。原因时间片远小于服务时间时任何进程都不可能在一个时间片内完成每次时间片到期都会触发切换。这在模拟器逻辑上没错但课程设计报告里说服力很差——甘特图根本没法展示。解决课设数据范围设置时间片为服务时间的四分之一到二分之一比较合理。比如服务时间5时间片设为1或2。同时建议在实验结果中额外输出总切换次数用数字论证“时间片过大响应变差过小开销增加”。这个维度是答辩加分项。5.3 坑三就绪队列用了全局优先级比较导致到达时间改变后排序不更新现象FCFS先跑结果正常但切换成SJF后进程完成的顺序频繁出现“后到达的短作业排到了先到达的长作业前面但输出顺序却是按pid排列”。原因就绪队列内部用优先队列做堆排序但模拟过程中如果修改了进程的某个比较字段比如在静态数组里更新了状态而堆里的元素是PCB指针排序依据不会随之更新。更常见的错误是——把进程插入队列和提取队列的动作写对了但比较函数里引用的字段在后续被修改导致堆结构混乱。解决调度器的设计原则上是“进程一旦进入就绪队列其参与排序的字段在本次等待期间不再改变”。SRTF的实现之所以要特殊处理是因为它需要在进程被抢占后重新进入队列这时remainTime已经变了必须重新push一次而不是原位修改。我的做法是从CPU上换下的进程无论什么原因都执行一次readyQueue.pop再push确保堆内部顺序被重建。5.4 坑四输出甘特图时进程ID对齐混乱现象多行输出的甘特图在pid为两位数时错位横轴的刻度对不上看起来杂乱无章。想在报告里截图但输出一眼看去就是“没用心做”的既视感。原因格式化输出时没有统一字段宽度用空格拼接而不是格式化控制符。解决用C的iomanip库统一宽度比如每格宽度为8个字符内容是[PID:%2d]这种固定格式。甘特图输出其实是课设报告里最有视觉冲击力的部分值得花点时间把格式调好。我自己习惯是输出一行时间刻度一行布局条一行进程ID三行一组时间用setw(5)固定占位这样即使模拟到50个进程也能轻松看清调度顺序。6. 边界测试与答辩验证四组输入数据验证调度器的正确性模拟器写完只是第一步第二步是做验证。课程设计答辩时老师几乎必然会问“你怎么确认你的模拟结果是对的”。单纯跑通默认数据并不够你需要准备几组能暴露问题的边界输入。第一组全部进程同时到达输入5个进程到达时间全为0服务时间分别为2, 4, 6, 8, 10。这时FCFS和SJF的调度顺序应该完全一致因为就绪队列是空的先到达的进程立即运行SJF没有机会重排。如果这组数据下你的SJF结果与FCFS不同说明算法里存在bug很可能是抢占逻辑写错了或者就绪队列初始状态处理错误。RR结果则应该呈现严格的轮转顺序每个进程各跑一个时间片后循环。第二组进程按服务时间降序到达到达序列为(进程A: 到达0, 服务10)、(进程B: 到达1, 服务2)、(进程C: 到达2, 服务1)。这组数据专门测试SRTF的抢占行为。正确答案是A运行到1B到达后抢占29B运行到2完成C到达后抢占17C运行到3完成最后A继续运行到时间10。如果SRTF结果中A一口气跑到结束说明抢占条件没触发。第三组CPU空闲窗口设计一个时间段内没有进程到达的输入。比如进程1到达时间0服务时间3进程2到达时间5服务时间2。正确的输出中时间区间[3,5]应显示CPU空闲标记。这组数据会暴露模拟器是否在CPU空闲时浪费循环逐片推进的缺陷——如果你的程序运行时间随总模拟时长线性增长说明事件驱动没做对。第四组时间片恰好等于服务时间给RR设置时间片为4某个进程服务时间也是4。进程应在一个时间片内完整结束并且不能在结束时被重复放入就绪队列。这个边界条件测试的是“remaining time减到0”与“时间片到期”两个事件同时发生时事件处理的优先级。我的实现里先检查完成再检查切片这样进程不会出现“已经结束了还在就绪队列里等待下一轮”这种荒谬输出。做完验证把四组数据的运行结果截图放进报告附录答辩时直接说“我用了这四组边界输入做测试分别覆盖了同时到达、抢占边界、CPU空闲窗口和零剩余时间场景”这个表述本身就是学术严谨性的体现。我的习惯是每改动一次调度器代码就把这四组数据重新跑一遍确保没有引入回归。这类模拟器最怕的状态黑匣子在进程状态切换时悄悄吞掉了一次调度事件而边界测试往往是暴露它的唯一手段。希望这篇笔记能帮你把这份大作业从“能跑”推进到“能讲”——不只是在报告里写出算法名字而是能在答辩现场用数据和设计逻辑说服老师。祝你的操作系统课设顺利收场。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑