☰
进程调度算法课设从原理到实现:常见算法、模拟器与避坑指南
2026/10/14 4:40:07 网站建设 项目流程

简介:这是一份面向操作系统原理课程设计的学习资料,围绕基于ucore框架设计并实现优先级进程调度算法展开,适合高校计算机专业学生、准备操作系统课设或对Linux内核调度感兴趣的开发者参考。压缩包大小约3.28MB,具体文件总数未明确标注,从标题与描述可见主要包含源码、课设报告和演示PPT三大类,其中源码用于搭建调度实验环境,报告阐述设计思路与结论,PPT便于展示答辩。该资源目前已有288人学习浏览,属于较受关注的操作系统课设素材。资源内容针对优先级调度算法的局限性、ucore调度器框架和实验环境搭建(含git、gcc、gdb、make、qemu-system等工具)进行了完整呈现,读者可对照代码与报告理解进程调度的实现细节,并通过PPT快速掌握答辩要点,省去从零搭建环境的摸索成本。

1. 进程调度算法课设:到底是什么、难在哪、怎么拿高分

操作系统原理课设里,进程调度算法是被选得最多的题目,也是翻车比例最高的题目之一。最典型的场景是:代码明明跑出了结果,报告里贴了一堆运行截图,老师追问一句“你模拟的是抢占式还是非抢占式”,当场卡壳。真正决定这套课设分值的,不是某个算法的代码写得多漂亮,而是你能不能把进程从到达、排队、运行到完成的全过程量化,再让几个算法在同一组输入下公平对比。这里把从原理到落地的完整思路、代码骨架、参数选择和常见坑位过一遍,适合正在做课设、想拿高分又不想反复重写的同学。

2. 进程调度算法:先在原理上立住,再动手写代码

2.1 调度问题的本质:就绪队列、CPU 与决策时机

调度器要回答的问题只有一个:当前时刻,CPU 给哪个进程用。操作系统里,进程不会一直占用 CPU,它会在就绪、运行、阻塞、结束之间切换。调度器在每次切换发生时做决策,而课设模拟的是这整个决策过程,只是把真实的时钟中断换成了一个 while 循环。

很多同学写模拟器时一上来就写“选择下一个进程”的函数,却忘了先设计“什么时候需要做出选择”。这个决策时机就是调度的触发点。在课设里,常见的触发点有四个:进程新到达、当前进程运行完毕、当前进程时间片用尽、更高优先级进程抢占。哪个触发点被处理了、哪个被忽略了,直接决定模拟结果对不对。

我在给某开发者审代码时遇到过一件事:他实现了抢占式优先级调度,代码逻辑看起来没问题,可输出结果里优先级低的进程总是能跑完,优先级高的进程反而一直在等。查了半天,发现他只在进程完成时才做重新调度,也就是把抢占式写成了非抢占式。触发点没选对,算法名字再响也是白搭。

2.2 五类经典调度算法:一张表看懂差异

课设题目一般会限定范围,常见的是 FCFS、SJF、优先级调度、时间片轮转 RR,以及多级反馈队列 MFQ。它们的核心差异在三个维度:是否抢占、选择依据、是否考虑到达时间。

算法抢占性选择依据到达时间的处理方式
FCFS非抢占到达顺序按到达时间排序后依次执行
SJF非抢占/抢占服务时间最短在到达或完成时刻重新选择
优先级可抢占优先级最高新进程到达时判断是否抢占
RR抢占固定时间片轮转新进程加入队尾,按队列顺序执行
MFQ抢占多队列+时间片+反馈按队列级别逐级降级

FCFS 最容易理解,但也最容易暴露出“护花使者”问题:一个长进程先到达,后面所有短进程都得等。SJF 在非抢占形式下,每次只在当前进程跑完后从已到达的进程里挑服务时间最短的,平均周转时间最优,但存在饥饿风险,而且理论上的最优性依赖精确预知服务时间,这在真实系统中做不到。优先级调度强调给紧急任务让路,但低优先级进程可能长时间得不到 CPU,这就是教科书里说的“饥饿”。RR 解决了饥饿,代价是频繁切换带来的上下文切换开销,时间片选多大直接影响结果。MFQ 是综合方案,前台短进程在高优先级队列快速完成,长进程逐步降级,兼顾响应时间和吞吐量。

这张表建议直接放进课设报告的“算法设计”章节里。很多报告拿不到高分,不是因为代码写错,而是原理章节抄教材抄得太明显。把对比表按自己的理解重新组织一遍,老师一眼就能看出你读懂了。

提示:表里的“抢占性”不是越抢占越好。抢占式算法的平均响应时间更好,但上下文切换次数更多;非抢占式实现简单,适用批处理场景。报告里要能说出你选某个算法的理由,而不是默认全部做成抢占式。

2.3 评估指标:周转时间、带权周转时间与等待时间

实现完算法之后,能不能证明它好,靠的是量化指标。操作系统课设里必用的三个指标是:周转时间、带权周转时间、平均等待时间。

周转时间等于进程从到达时刻到完成时刻的差值,对应一个进程从提交到出结果的完整时长。带权周转时间是周转时间除以服务时间,它衡量的是“等待成本”占了多少比重,短进程的带权周转时间通常更大,说明它相对更吃亏。等待时间则等于周转时间减去服务时间,也就是进程在就绪队列里排队的总时长。这三个指标只有第一项直接来自模拟输出,后面两项都是用周转时间倒推的,所以代码里只要把完成时间和到达时间记录准确,其余都是计算问题。我在报告里习惯把三个指标并列成一张表,再额外加一列“平均”,老师需要的数据就齐了。

2.4 选型思路:按课设要求一步步缩小范围

我做这个课设的通用流程是:先读题,把题目里要求实现的算法圈出来;再定输入数据格式,统一到同一个结构里;然后写一个能跑通任意算法的模拟框架,最后逐个填算法。这样每个算法只改“选择下一个进程”那一小段,其余代码完全复用。

如果题目写成“设计并实现至少三种进程调度算法并对比分析”,我一般会选 FCFS、SJF、RR 或优先级做组合。这三个组合能覆盖非抢占、抢占、时间片三个维度,报告里的对比分析有足够素材。如果题目明确要求“模拟多级反馈队列”,那就要把重心放在参数设计上,比如队列数量、各级时间片大小、降级策略,这些参数对结果的影响远大于算法本身的代码实现。

3. 搭一个可扩展的调度模拟器:数据结构与时间推进是地基

3.1 PCB 结构体:字段怎么设计才够用又不冗余

模拟器的地基是进程控制块 PCB。课设里不需要完整的线程 ID、内存映像、文件描述符表,但必须包含调度相关字段。我一般这样定义:

typedef struct pcb { int pid; // 进程编号,从1开始递增 int arrive_time; // 到达时刻,模拟输入给出 int need_time; // 需要的总CPU时间,即服务时间 int remain_time; // 剩余CPU时间,初始等于need_time int priority; // 优先级,数值越小优先级越高 int start_time; // 第一次获得CPU的时刻 int finish_time; // 完成时刻 int run_time; // 累计运行时间,用于时间片轮转 struct pcb *next; // 就绪队列链表指针 } PCB;

这里有一个很容易踩的坑:很多实现把“运行时间”和“剩余时间”混用一个字段。时间片轮转里的“累计已运行时间”和抢占式调度里的“剩余所需时间”语义完全不同——前者用来判断一个进程被调度了多少次,后者用来判断它是否执行完毕。如果只保留一个字段,RR 算法里就没办法区分“已经跑了 2 个时间片还剩 8 个”和“总共需要 8 个已经跑完 2 个”。所以我在一开始就把 need_time、remain_time、run_time 三个字段全部定义好,宁可不用,也不能缺。

还有一个取舍:priority 字段的数值方向。不同教材定义不一样,有的按数值越大优先级越高,有的相反。我建议在程序开头用注释写清楚,并且所有比较操作统一用一个宏或者一个比较函数,否则写优先级调度时很容易把大于号小于号搞反。这个错误很隐蔽,因为大部分输入数据下“反着比较”也能跑出结果,只是调度顺序全反了。

3.2 时间推进机制:while 循环里怎么模拟时钟

模拟器的核心是一个推进时钟的循环。真实操作系统靠定时器中断触发调度,模拟器里则用一个不断增加的 current_time 变量代替。我常用的框架是:

int current_time = 0; // 模拟时钟 int finished = 0; // 已完成的进程数 PCB *ready_queue = NULL; // 就绪队列头指针 while (finished < total) { // 步骤1:把到达时刻等于 current_time 的进程插入就绪队列 add_new_arrivals(proc_list, total, current_time, &ready_queue); // 步骤2:从就绪队列中选择一个进程运行 // 这个位置替换成不同调度算法的选择逻辑 // 步骤3:推进时间,更新进程的剩余时间/运行时间 // 步骤4:判断进程是否完成,更新完成时间 }

这个框架的关键在于“步骤1 和步骤2 的执行顺序”。必须先处理新到达的进程,再做调度决策,否则会出现一个进程已经到达却“错过”本轮调度的情况,尤其在抢占式算法里会直接导致结果偏大。步骤3 的时间推进量也在这一步确定:非抢占式算法一次推进到当前运行进程完成或下一个进程到达;时间片轮转一次最多推进一个时间片。

注意:current_time 可以设计成“只在事件发生时跳跃”。事件就是新进程到达和进程完成。这样循环次数少、结果精确,不用一个时间片一个时间片地遍历整个时间轴。缺点是代码里要小心处理“事件同时发生”的情况,比如某进程在第 5 时刻完成,另一个进程也第 5 时刻到达时,先处理哪个会影响后续结果。

3.3 输入数据格式:让所有算法公平对比的第一道关

模拟器的输入一般是进程列表,每行一个进程的到达时间、服务时间、优先级。我习惯用文件读入,因为课设报告里“输入数据”需要固定下来,多次运行才能复现。文件格式可以简单一点:

// input.txt 每行: 进程名 到达时间 服务时间 优先级 P1 0 7 3 P2 2 4 5 P3 3 1 1 P4 5 3 2

选这组数据的理由有两层。第一层,它包含了同时到达和错峰到达的情况:P1 在时刻 0 先到,P2 在时刻 2 到达时 P1 还没跑完,P3 和 P4 分别在 3 和 5 到达,覆盖了“进程执行中才到新进程”的抢占判断场景。第二层,服务时间差异明显:P3 只要 1 个时间单位,是测试 SJF 和 RR 对短进程响应能力的好素材。这组数据在跑 FCFS 和 SJF 时结果差异足够明显,报告里写对比分析不会显得勉强。

文件读入的代码不复杂,重点是鲁棒性:用 fscanf 逐行解析时,要处理行尾注释和空行;在程序里给进程数量加一个上限检查,防止读取越界。这些细节不会在演示时被直接看到,但数据解析出错会让整个模拟结果作废,属于那种“查很久才发现是读文件的问题”的隐蔽毛病。

4. 逐个实现调度算法:代码骨架与参数边界

4.1 先来先服务和短作业优先:排序之后的两个细节

FCFS 是非抢占式算法里最简单的。实现上先按到达时间排序,然后依次执行,当前时间小于进程到达时间时先跳到到达时间。

// FCFS:按到达时间排序后依次执行 void fcfs(PCB *proc, int n) { sort_by_arrive(proc, n); // 按到达时间升序排序 int current = 0; for (int i = 0; i < n; i++) { if (current < proc[i].arrive_time) current = proc[i].arrive_time; // 等待进程到达 proc[i].start_time = current; proc[i].finish_time = current + proc[i].need_time; current = proc[i].finish_time; printf("P%d: 开始=%d 完成=%d 周转=%d\n", proc[i].pid, proc[i].start_time, proc[i].finish_time, proc[i].finish_time - proc[i].arrive_time); } }

这里最容易被忽略的是 sort_by_arrive。很多同学把进程列表按 pid 或输入顺序直接拿来跑,得到的“FCFS 结果”实际上不是先来先服务,而是“输入顺序服务”。如果输入文件里进程不是严格按到达时间排列,结果就全错了。排序之后还要保持进程 pid 和原始数据的对应关系,我建议在结构体里直接存 pid,排序移动整个结构体,而不是排序一个索引数组再映射。

SJF 非抢占式则不一样:它不是在开始前排一次序,而是在每个完成时刻,从“已经到达但还没运行”的进程里选服务时间最短的。因此排序不能提前一次性做完,而要在模拟循环里,每次完成事件后重新扫描未完成的进程。

// SJF 非抢占:每次完成时从已到达进程里选服务时间最短的 void sjf_nonpreemptive(PCB *proc, int n) { int current = 0, done = 0; while (done < n) { int idx = -1; for (int i = 0; i < n; i++) { if (proc[i].remain_time > 0 && proc[i].arrive_time <= current) { if (idx == -1 || proc[i].need_time < proc[idx].need_time) idx = i; } } if (idx == -1) { // 没有进程已到达,时钟跳到最早到达时刻 int next = INT_MAX; for (int i = 0; i < n; i++) if (proc[i].remain_time > 0 && proc[i].arrive_time < next) next = proc[i].arrive_time; current = next; continue; } proc[idx].start_time = current; proc[idx].finish_time = current + proc[idx].need_time; current = proc[idx].finish_time; proc[idx].remain_time = 0; done++; } }

这段代码的判断顺序值得注意:先判断“是否已到达”,再比较“服务时间最短”。如果把到达判断放在比较之后,就会出现还没到的进程被选中的逻辑错误。另外 idx == -1 分支处理的是 CPU 空闲时段——所有进程都还没到达,这时不能干等,要把 current 直接跳到下一个最早到达时刻,否则模拟会无限循环。这个分支在 FCFS 里用 if 判断一个进程就够了,在 SJF 里需要扫描全部进程找最早的到达者。

4.2 抢占式优先级调度:重新调度的时机决定一切

抢占式优先级调度的核心是:一个高优先级进程到达时,如果当前运行的进程优先级更低,就立即打断它。这里的关键不是“打断”怎么写,而是“什么时候检查有没有新进程到达”。实现时可以复用 3.2 的模拟框架,在步骤2 里从就绪队列中优先挑 priority 最小的进程。

C 语言的优先级比较要特别留意字段方向。如果设计成数值越小优先级越高,那么选择逻辑是“找 priority 最小值”;如果数值越大越高,则要找最大值。把方向搞反后,结果只是“反着排”,不会报错,所以尤其危险。建议在结构体注释里写明,并且把选择逻辑抽成一个函数,将来改方向只改函数内部。

抢占式实现里还有一个容易反复改不对的细节:被抢占的进程已经运行了一部分,它的 start_time 应该记录第一次运行时的时刻,而不是每次被重新调度的时刻。否则周转时间计算会丢失等待时间,最终统计指标偏小。finish_time 同理,只在真正完成的那一次赋值。这个约束建议在代码注释里写清楚,调试时逐条核对。

4.3 时间片轮转:队列旋转和边界处理

RR 算法的模拟要保持一个动态的就绪队列,每一次调度从队首取进程,运行一个时间片后,如果还没完成就放回队尾。核心代码如下:

// RR:时间片为 quantum,进程用完时间片未完成则放回队尾 while (finished < total) { enqueue_new_arrivals(&ready_queue, proc_list, total, current_time); if (ready_queue == NULL) { current_time = next_arrive_time(proc_list, total, current_time); continue; } PCB *p = dequeue(&ready_queue); // 队首出队 if (p->start_time == -1) // 首次运行,记录开始时间 p->start_time = current_time; if (p->remain_time <= quantum) { // 剩余时间小于等于一个时间片 current_time += p->remain_time; // 一次跑完 p->remain_time = 0; p->finish_time = current_time; finished++; } else { // 用满整个时间片,未完成 current_time += quantum; p->remain_time -= quantum; enqueue(&ready_queue, p); // 放回队尾 } }

边界条件有两个。第一个:remain_time 恰好等于 quantum 时,进程应该在本轮完成,不要把它放回队尾再空转一个循环;如果用 <= 判断,这个边界就自然处理对了。第二个:current_time 在队列为空时也要前进,否则空等会让循环卡死。next_arrive_time 从进程列表里找出下一个还没到的进程的到达时刻,与 SJF 里的空闲处理思路一致。

时间片大小的选择会直接改变模拟结果。时间片太小,上下文切换频繁,短进程能快速完成;时间片太大,RR 退化成 FCFS,短进程反而可能被长进程阻塞。我第一次做这个课设时,用了 quantum = 1,结果在输入数据里加了一个服务时间 100 的大进程,整个周转时间表很难看,后来把 quantum 调到 4,短进程的响应时间才有明显改善。报告里的对比分析应该覆盖至少两组 quantum,说明它对指标的影响。

4.4 多级反馈队列:三层队列的反馈参数是灵魂

多级反馈队列 MFQ 是实现起来最费心思的算法,因为它有两个层面:每层队列用 RR 或 FCFS 调度,进程根据占用 CPU 的时间在队列之间移动。一个常见的三层设计是:队列1 时间片 4,队列2 时间片 8,队列3 按 FCFS;新进程先进入队列1,时间片用尽未完成则降到队列2,队列2 用尽再降到队列3。

代码上,MFQ 并不需要真的维护三个独立的链表,可以维护一个进程的“当前所属队列编号”字段,再在调度时从优先级最高的非空队列取进程。这样实现起来比三链表简单,逻辑也直观。

// MFQ 调度选择逻辑:从编号最小且非空的队列取进程 PCB *pick_next(PCB *queues[3], int start_q) { for (int q = start_q; q < 3; q++) { if (queues[q] != NULL) return dequeue(&queues[q]); } return NULL; }

这里 start_q 是调度器当前扫描的起始队列。降级规则要写清楚:进程在一个队列里累计占用了该队列的全部时间片仍没完成,就移动到下一级队列。有一种常见实现错误是让进程每次都从队列1 开始重新排队,这样低优先级队列永远不会有进程,MFQ 就退化成了单层 RR。另有一种错误是只降级不升级,长进程全部堆在最底层,底层的 FCFS 反而变得很慢。MFQ 的参数(队列数、每层时间片、是否在抢占时重置)没有标准答案,报告里应该明确写出自己选的参数和理由。我做课设时选了 3 层、时间片 4/8/16,并在报告里对比了“时间片翻倍”和“时间片固定”两种策略下进程的完成时间差异,这一节成了报告里最加分的部分。

5. 课设避坑:从编译到验收的 5 个常见问题排查

5.1 到达时间没有参与调度,所有算法结果都差一点

现象:实现 SJF 和 RR 时,结果和手算对不上,表现是某些进程的开始时间小于它的到达时间,或者 FCFS 的结果和输入顺序完全相同,换一种输入顺序结果就变。

原因:模拟框架里进程数组没有按到达时间处理,调度逻辑直接按数组下标顺序遍历,把“进程在这个顺序”当成了“进程在这个时刻到达”。这是最基础但最高频的错误。

解决:在读取输入后统一按 arrive_time 排序,或者每次遍历都在判断 p->arrive_time <= current_time 后再处理。我一般选择后者,因为它在所有算法里都通用,不用依赖排序的稳定性。

5.2 时间片边界处理用错了符号,RR 最后一个时间片多算一段

现象:RR 跑出来的进程完成时间比手算大一个时间片,或者进程明明在第 10 时刻只剩 3 个时间单位,却等到第 14 才完成。

原因:代码用 if (remain_time >= quantum) 判断“是否放回队尾”,导致剩余时间恰好等于时间片时也被放回队尾,空转了一个时间片后才在下一轮完成。

解决:改成 if (remain_time <= quantum) 直接完成,用 else 处理剩余时间大于时间片的情况。这个符号错误在代码审查里很难被看出来,因为单步调试时只有一两个进程触发。

5.3 新进程到达检查放在调度之后,抢占式算法晚一个时间片

现象:抢占式优先级调度的输出里,高优先级进程的 start_time 比其到达时间晚,且晚出的时间正好是最低调度粒度,通常是 1 个时间单位。

原因:模拟循环里先做调度决策、后把新到达进程加入就绪队列。高优先级进程到达的瞬间,当前进程还在运行,下一个循环才检查到达,相当于白白多运行了一个时间片。

解决:把“加入新到达进程”放在循环体的最前面,再执行调度。同时把到达时间相同的进程按顺序全部加入,避免漏掉。

5.4 随机数没固定种子,每次运行结果不同,无法复现

现象:同一份代码、同一份输入,两次运行输出不一样;写报告截图时也拿不到一致的数据。

原因:模拟器里用了 rand() 生成优先级或服务时间,却没有在 main 里设置 srand,导致每次运行产生不同序列。

解决:固定用 srand(42) 或某个固定数字初始化随机种子,并把输入数据也固定成文件。这样既能复现,又能保证报告里展示的结果就是代码真正跑出来的数据。也可以在程序里支持一个“手动输入模式”,方便演示。

5.5 报告数据和代码输出不一致,验收时被追问

现象:报告表格里的周转时间和演示时现场跑的代码输出对不上,差几个时间单位。

原因:报告是改过参数或换过输入数据后重新生成的,但代码的默认输入还是旧版本。比如报告里说时间片是 4,演示时代码里写的是 quantum = 5。

解决:报告里的每一张数据表都对应一个固定的输入文件和固定参数。我在课设文档里列出“输入文件:input.txt,时间片:4,优先级方向:数值小优先”,并在代码的 main 函数里把默认参数用宏写死,保证双击运行就是报告那一版。这个习惯后来帮我避免了好几次现场翻车。

6. 让课设报告和 PPT 不拖后腿:验证指标与加分技巧

课设报告的重点不是贴代码,而是证明你的代码是对的。我常用的验证方法之一是构造一组“手算已知结果”的输入,比如 4 个进程、到达时间为 0/2/3/5,手工算出 FCFS 和 SJF 的周转时间,再和程序输出比对。只要这几个数值完全一致,整个模拟框架的可信度就上来了,剩余算法只需要抽查边界情况。PPT 里放一张“手算 vs 程序输出”对照表,胜过十页代码截图。

报告结构我会按“实验目的 → 数据结构设计 → 算法流程与代码 → 运行结果 → 对比分析 → 总结”来组织,其中对比分析是拿分重点。用同一组输入跑三个算法,把每个进程的到达、完成、周转、带权周转列成一张大表,再补一段话解释为什么 SJF 的短进程带权周转时间最小、为什么 RR 时间片变大后短进程响应变慢。这段话不需要长篇大论,把因果关系说清楚就够了。

PPT 的要点是“过程图比代码块更值钱”。建议画一张时间线甘特图,横轴是时间,进程用不同颜色的条表示,哪些时刻进程在运行、哪些时刻在等待一目了然。很多老师看到代码标题页会走神,看到甘特图反而会多问两句。我有一个小教训:有一次课堂展示时间超了,我把重点放在讲多级反馈队列参数变化对结果的影响上,结果后面时间不够,只能匆匆结束,最后老师问“RR 时间片增大,平均周转时间为什么上升”我竟然没答上来。后来所有数据都准备“两套对比”:一套默认参数,一套对比参数,并提前把差异的解释写好。这个习惯在答辩时非常救命。希望这次梳理能帮到你,把课设做成一份能随时现场验证的系统,而不是一份只能靠截图撑场的演示。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询