操作系统这门课,进程调度几乎是绕不过去的一道坎,而课堂练习3.3往往就是第一次真正让你坐下来算数、画图、比较算法的那道题。它看起来只是几行表格加几个时间数字,实际上考的是你对进程调度这件事的整体理解:进程什么时候被选中、CPU什么时候被让出去、队列里的顺序怎么变、一组数据算下来谁优谁劣。很多同学卡在这里不是因为不会算减法,而是因为脑子里没有一个清晰的时间轴模型,导致每道题都要重新猜一遍规则。
这篇内容我想按着“先讲清题目在考什么、再把概念和时间量理顺、接着手算一遍经典算法、最后用代码把这套逻辑固化下来”的顺序展开。适合正在做这道练习的同学,也适合已经学过但一算就乱的读者。你会看到完整的推演过程、参数选择背后的理由、代码实现里几个容易写挂的细节,还有我在做题和写模拟器时踩过的坑。全文以 Python 模拟为主,但核心是思路,换成 C、Java 甚至 Excel 手工推演都不影响。
1. 这道课堂练习到底在考什么
1.1 把“进程调度”四个字拆开看
看到“课堂练习3.3:进程的调度”这个标题,第一反应不应该是去找公式,而是先问自己:调度这件事,到底在调什么?答案是把有限的 CPU 时间分配给多个都想要 CPU 的进程。一旦接受了这个前提,问题就变成三个必须回答的点:谁来排(就绪队列的组织方式)、按什么规则排(调度算法)、什么时候重新排(调度时机)。这三件事只要有一件没想清楚,整道题的结果就是错的。
课堂练习之所以把它单独拎出来,是因为它同时踩中了两条主线:一条是数据结构的应用(队列、优先队列),另一条是操作系统的核心机制(进程状态迁移)。很多教材把它放在进程管理这一章的中后段,前面刚讲完 PCB、进程状态图、原语,后面紧接着就是调度算法,这个位置绝不是随便放的。它要你做的,是把这些散点串成一条能跑起来的时间线。
所以拿到题目第一件事,我建议先把题干里的表格抄一遍,列出进程名、到达时间、服务时间(有的题叫运行时间或 CPU 突发时间)这三列。如果题干还给了优先级,那就再加一列。抄的过程本身就是一次信息核对,很多错题都源于看错了某一行的到达时间或者漏了一个进程。别嫌这一步土,我做过统计,同一批人里出错的原因,纯粹计算失误的比例其实低于看错数据的比例。
1.2 老师为什么偏偏在这里设卡:调度是操作系统的分岔路口
进程调度之所以成为设卡点,是因为它是一个典型的“多条路都能走,但结果差很多”的问题。同一个进程集合,用 FCFS 算出来的平均周转时间可能是 11.8,用 SJF 可能降到 9.6,用时间片轮转又可能反弹回 13 以上。这组数字的落差会逼着你思考:为什么短作业优先看起来更“聪明”?为什么时间片轮转反而变差了?它真的差吗?
这里其实藏着一个很重要的认知:评价指标不同,最优算法就不同。SJF 在平均等待时间上表现优秀,但它对长作业不公平,长作业可能一直排在后面。时间片轮转的平均周转时间看起来不占优,但它保证每个进程都能在有限时间内拿到 CPU,响应更均等。老师出这道题的真正意图,是让你体会到“没有免费的午餐”,每种调度策略都是在对某类指标做优化,同时牺牲另一类指标。
理解了这一层,你在答题时就不会只盯着一个平均数字下结论。我见过不少同学算出 SJF 平均周转最小,就直接写“SJF 是最好的算法”,这种结论在考试里很容易被扣分,因为它忽略了对长作业的公平性和实现成本。正确的表述应该是“在给定指标下 SJF 更优,但它需要预知服务时间且对长作业不利”。这句话是加分的。
1.3 动手前的准备:一张表把参数对齐
真正开始算之前,我习惯先做一张“参数对齐表”,把每个进程的关键信息固定下来。以一道常见的五进程题目为例:
| 进程 | 到达时间 | 服务时间 | 优先级(数字越小越高) |
|---|---|---|---|
| P1 | 0 | 5 | 3 |
| P2 | 1 | 3 | 1 |
| P3 | 2 | 8 | 4 |
| P4 | 3 | 2 | 2 |
| P5 | 4 | 4 | 5 |
这张表的作用是后续所有推演的唯一数据来源。我会在草稿纸旁边留出一块空白,专门画时间轴,标出 0、1、2、3……每个整数时刻上发生了什么:谁到达了、谁被选中、谁运行完。这个习惯能极大降低“算了后面忘了前面”的概率。
对齐数据时有两个细节特别容易忽略。第一是到达时间的边界:到达时间等于当前时刻的进程,算不算已经就绪?绝大多数教材的约定是“到达时间 ≤ 当前时刻”即视为就绪,也就是说 t=2 到达的 P3,在 t=2 这一刻就可以被调度。第二是同时到达的处理:如果有两个进程在同一时刻到达,题干通常会补充“按进程号顺序”或者“按先来先服务”,如果没有说明,我一般按进程编号小的优先,并在答题时注明这个假设——写清假设本身就是一种严谨。
2. 概念不清就必错:先把三个时间量彻底理顺
2.1 PCB、进程状态和调度队列之间的关系
调度的对象不是一段代码,而是一个 PCB(进程控制块)。教材里那张状态迁移图,三个基本状态是就绪、运行、阻塞,调度器真正操作的是就绪队列:从里面挑一个,让它从就绪态变成运行态;当它时间片用完或者被更高优先级抢占,又从运行态退回就绪态,重新进入队列。这个过程反复发生,就构成了整个调度的动态画面。
把这个画面记住,很多题目里的“坑”就自动失效了。比如有一类题会问“进程在等待 I/O 时会不会被调度”,答案是不会,因为它在阻塞态,根本不在就绪队列里。再比如“时间片用完的进程去了哪里”,答案是回到就绪队列尾部(在时间片轮转里),而不是直接结束。这些判断都依赖于你脑子里那幅状态迁移图是否清晰。
我在做题时会在草稿纸角落画一个小三角:就绪、运行、阻塞,然后用箭头标注每次调度对应的迁移。这个动作看起来多余,但当题目有六七个进程、频繁切换时,它能帮你避免把“阻塞”和“就绪”混为一谈。尤其是涉及 I/O 的题,一个进程可能多次进出就绪队列,没有这张小图很容易乱。
2.2 周转时间、等待时间、带权周转时间的算法与记忆法
这三个时间量是进程调度练习的评分核心,公式本身很简单,但一混就全错。我把它拆成一句话记忆:周转看头尾,等待扣掉跑的时间,带权就是周转除以服务。
周转时间 = 完成时间 − 到达时间。它衡量的是从进程到达到它彻底做完,一共花了多久,包含了它排队的全部时间。等待时间 = 周转时间 − 服务时间。它衡量的是这个进程在就绪队列里干等了多久,因为真正占用 CPU 的那部分时间是服务时间,不算等待。带权周转时间 = 周转时间 ÷ 服务时间,它是把周转时间按作业大小“归一化”后的结果。
带权周转时间为什么要引入?因为单纯的周转时间对长作业不公平。一个服务时间为 20 的进程周转 24,看起来很久,但相对它自身的规模只多等了 20%;一个服务时间为 1 的进程周转 5,绝对数字不大,但它是自身规模的 5 倍,用户体验极差。带权周转时间把这种“相对感受”量化了,所以平均带权周转时间比平均周转时间更能反映调度对不同规模作业的影响。
计算时最容易错的地方有两个。一是CPU 空闲时间段的处理:如果某个时刻没有进程就绪,CPU 是空闲的,下一