在这里插入图片描述一台计算机里往往同时存在浏览器、编辑器、后台服务、下载任务等大量执行流,但 CPU 核心数量始终有限。尤其在单核 CPU 上,同一时刻真正执行指令的只能是一个任务。那么,操作系统应该在什么时候收回 CPU,又该把 CPU 交给谁?

这就是**进程调度(CPU Scheduling)**要解决的问题。调度并不是简单地让进程“轮流运行”,它还牵涉任务状态、上下文保存、I/O 行为、响应速度、公平性以及实时截止时间等目标。

本文只讨论调度的基本概念、进程行为、调度时机、调度分类与设计目标;具体的批处理、交互式和实时调度算法留到后续内容再展开。

一、进程调度的基本概念与整体流程

1. 为什么需要进程调度?

CPU 是一种稀缺资源。即使系统拥有多个核心,可运行的进程和线程数量通常也远多于核心数:

P1、P2、P3、P4、……
           ↓
       有限个 CPU 核心

操作系统必须持续回答一个问题:

下一段 CPU 时间应该分配给哪个可运行任务?

负责做出这一决定的内核组件称为调度器(scheduler);它依据一定规则,从候选任务中选择下一个运行者的过程,就是调度。

2. CPU 实际调度的对象是什么?

教材常把“进程调度”和“CPU 调度”放在一起讲,但在现代操作系统中,更准确的说法通常是:CPU 直接调度的是内核可见的执行实体,往往就是内核级线程

可以先这样区分:

概念主要职责
进程资源容器,拥有地址空间、代码、数据、打开文件等资源
线程具体执行流,拥有自己的程序计数器、寄存器和栈
内核级线程能被内核直接管理、阻塞、唤醒和调度的线程

一个进程可以包含多个线程:

进程 P
├── 地址空间、代码、堆、打开文件
├── 线程 T1:自己的 PC、寄存器、栈
├── 线程 T2:自己的 PC、寄存器、栈
└── 线程 T3:自己的 PC、寄存器、栈

同一进程内的线程共享大部分资源,但每个线程必须保留独立的执行现场,才能分别停在不同的代码位置。多核机器上,内核级线程还可以真正并行:CPU0 运行 T1CPU1 同时运行 T2

除了内核级线程,还有由用户态运行时或线程库管理的用户级线程。用户级线程的切换可能不需要进入内核,因此开销较小;但内核未必能直接看见每一条用户线程。若它们的映射模型设计不当,一个底层执行实体发生阻塞时,可能连带影响同一进程中的其他用户线程。

因此可以先记住:

进程主要管理资源,线程主要负责执行;现代操作系统的 CPU 调度对象通常是内核级线程。

3. 进程或线程的“执行现场”

一个线程正在 CPU 上运行时,处理器寄存器中保存着许多当前状态,例如:

PC / IP:下一条要执行的指令地址
SP     :当前栈顶位置
R0、R1……:通用寄存器中的中间计算结果
状态寄存器、浮点寄存器、向量寄存器等

这些信息合起来称为上下文(context)。它像一张执行快照,描述任务执行到哪里、计算到什么状态,以及下一次应该从哪里继续。

假设 P1 执行到一半,操作系统决定让 P3 使用 CPU。如果直接覆盖寄存器,P1 下次恢复时就不知道自己上次执行到哪里,也会丢失中间计算结果。因此切换前必须保存旧任务的现场:

CPU 中的 P1 状态
        ↓ 保存
P1 的控制块

以后 P1 再次被调度时,再把这些内容恢复到 CPU:

P1 的控制块
        ↓ 恢复
CPU 寄存器
        ↓
从原位置继续执行

4. PCB 是什么?

**PCB(Process Control Block,进程控制块)**是操作系统为每个进程维护的一份档案。概念上,它会记录:

PCB
├── 进程 ID
├── 进程状态
├── 优先级、时间片等调度信息
├── 地址空间等内存管理信息
├── 打开的文件和其他资源
└── 用于恢复执行的上下文
    ├── PC
    ├── SP
    ├── 通用寄存器
    └── 其他处理器状态

线程也有相应的线程控制结构,用于保存自己的执行上下文和调度信息。具体字段因操作系统实现而异,但核心目的相同:让内核能够暂停、管理并在以后恢复一个执行流。

5. 什么是上下文切换?

假设 CPU 原来运行 P1,调度器决定接下来运行 P3,完整过程大致为:

P1 正在运行
    ↓
发生调度
    ↓
保存 P1 的上下文
    ↓
调度器选择 P3
    ↓
读取并恢复 P3 的上下文
    ↓
CPU 开始执行 P3

这称为上下文切换(context switch)

需要注意,调度和上下文切换并不完全相同:

  • 调度回答“接下来选谁”;
  • 上下文切换负责“怎样从旧任务切换到新任务”。

有时调度器重新评估后仍选择当前任务,此时不一定发生完整的任务切换。

6. 进程调度主要解决三个问题

调度可以拆成三个连续问题:

  1. **什么时候调度?**何时需要或允许重新决定 CPU 的归属?
  2. **从哪里选择?**候选者通常来自就绪队列,而不是阻塞队列。
  3. **选择谁运行?**用调度算法在多个就绪任务之间进行取舍。

常见调度时机包括当前任务退出、发生阻塞、时间片耗尽,以及更高优先级任务变为就绪。调度器通常从就绪队列选择任务,因为阻塞任务仍在等待 I/O、锁或某个事件,即使得到 CPU 也无法继续执行。

“从多个就绪任务中选谁”正是调度算法研究的内容。先来先服务、时间片轮转、优先级调度、最短作业优先和多级反馈队列等算法,回答的都是同一个问题:

这么多已经可以运行的任务,CPU 应该先给谁?

7. 为什么调度不能太频繁?

一次任务切换并不是免费的,它可能涉及:

保存旧任务的上下文
更新运行队列和任务状态
切换内核栈或地址空间相关状态
恢复新任务的上下文
重新建立缓存和分支预测的有效状态

调度过少,某些任务可能长期占用 CPU,交互响应变差;调度过于频繁,大量时间又会浪费在内核管理和上下文切换上,有效计算时间反而下降。

因此,时间片大小和抢占频率本质上是在响应速度切换开销之间权衡。

8. 把整个调度流程串起来

多个进程 / 内核级线程
          ↓
      进入就绪队列
          ↓
      调度时机到来
          ↓
        调度器
          ↓
      按调度算法选择
          ↓
保存旧任务上下文 → 恢复新任务上下文
          ↓
       新任务占用 CPU

所以,进程调度可以概括为:

操作系统在合适的时机,从就绪任务中选择一个执行实体占用 CPU,并在需要时保存旧任务、恢复新任务执行现场的过程。

二、调度的介绍

早期批处理系统按照作业在磁带或卡片上的顺序逐个运行,调度规则相对简单。随着多道程序设计出现,内存中可以同时驻留多个任务:一个任务等待磁盘、网络或终端 I/O 时,CPU 不必空闲,可以转而执行另一个就绪任务。

多道程序提高了资源利用率,也使调度问题变得复杂。系统既可能存在强调吞吐量的批处理作业,也可能存在要求快速反馈的交互任务;还有些任务带有严格的完成时限。调度器要在它们之间分配 CPU 时间,平衡等待时间、吞吐量、响应性和优先级规则。

因此,不同使用场景需要不同调度策略,并不存在脱离系统目标、对所有工作负载都最优的单一算法。

三、进程行为

1. CPU 计算与 I/O 等待交替出现

绝大多数进程的执行并不是纯计算或纯 I/O,而是在二者之间交替:

运行一段 CPU 计算
        ↓
发起磁盘、网络等 I/O
        ↓
进程阻塞,等待 I/O 完成
        ↓
重新就绪并继续计算

连续使用 CPU 的一段时间常称为 CPU 突发(CPU burst)。根据 CPU 突发的长短和 I/O 请求频率,可以把进程粗略分成 CPU 密集型与 I/O 密集型。

2. CPU 密集型进程

**CPU 密集型(CPU-bound)**进程拥有较长的 CPU 突发,很少请求 I/O。视频编码、科学计算、压缩和大规模数值运算都属于典型例子:

CPU:计算 → 计算 → 计算 → 计算
I/O:偶尔发生

这类任务的性能瓶颈主要是计算能力。在单核机器上,给同一个纯 CPU 密集型任务盲目增加大量线程,通常不会使其更快,反而可能增加上下文切换开销;在多核机器上,只有把任务合理拆分,并让多个线程运行在不同核心上,才能获得真正的并行加速。

“线程数等于核心数”可以作为纯计算任务的初步估计,但并不是绝对公式。超线程、负载是否可并行、内存带宽以及系统中的其他任务都会影响最佳线程数。

3. I/O 密集型进程

**I/O 密集型(I/O-bound)**进程每次只使用很短一段 CPU,就要等待磁盘、网络、键盘、数据库或其他设备:

CPU:运行 5ms → 发起 I/O → 等待
CPU:运行 5ms → 发起 I/O → 等待

这里“密集”指 I/O 请求频繁,而不是占用 CPU 更多。它的性能瓶颈往往是外部设备或服务响应,因此 CPU 利用率可能并不高。

调度器若让刚完成 I/O 的进程较快获得一小段 CPU,它就能尽早处理结果并发起下一次 I/O;当它再次等待时,CPU 又可以去执行 CPU 密集型任务:

CPU:  B B A B B B A B
             ↓       ↓
磁盘:       A 读盘   A 读盘

这样 CPU 与磁盘、网络等设备能够重叠工作,提高整个系统的利用率。调度算法因此不应对所有任务简单地“一视同仁”,而要结合任务行为和系统目标进行权衡。

4. 小结

类型CPU 使用特点I/O 特点调度关注点
CPU 密集型CPU 突发较长I/O 较少减少不必要的切换,充分利用核心
I/O 密集型CPU 突发较短频繁等待 I/O尽快响应并发起下一次 I/O

区分二者的关键在于 CPU 突发的长度与 I/O 请求频率,而不是简单比较某次 I/O 本身耗时多久。

四、何时调度

1. 创建新进程

当前进程创建子进程后,父进程和子进程都可能处于可运行状态:

父进程 A 执行 fork()
          ↓
  A 可运行,B 也可运行
          ↓
调度器决定继续 A 还是先运行 B

这通常是一次可能发生的调度决策。当前任务还可以继续运行,因此系统可以继续运行父进程,也可以根据优先级和策略选择新进程或其他就绪任务。

2. 当前进程退出与空闲任务

当前任务正常结束或调用 exit 后,它已经不能继续使用 CPU,系统必须从就绪队列中挑选新任务:

CPU → A
A 退出
  ↓
从就绪队列 B、C、D 中选择一个

这属于必须重新调度的时机。

如果没有任何普通任务处于就绪状态,系统会运行空闲任务(idle task)。它具有三个重要特点:

  • 永远可以运行;
  • 不会像普通任务一样等待业务资源而阻塞;
  • 优先级最低,只在没有其他就绪任务时被选中。

现代系统中的 idle 不是为了让 CPU 无意义空转,而常常会执行等待中断、降低频率或进入低功耗状态等操作。任务管理器中的“系统空闲进程占用率”也主要表示 CPU 有多少时间没有执行普通工作,而不是它真的消耗了对应比例的计算资源。

3. 当前进程阻塞

运行中的任务若等待磁盘、网络、锁、信号量或某个事件,就会从运行态进入阻塞态:

A 请求磁盘 I/O
      ↓
A:运行态 → 阻塞态
      ↓
CPU 必须调度其他就绪任务

阻塞任务不是候选者,因为即使把 CPU 给它,它仍然无法继续执行。调度器通常从就绪队列选择下一个任务。

理论上,如果高优先级任务 A 正在等待 B 释放资源,优先运行 B 可能更有利;但通用调度器通常不会推导完整的任务依赖图,而主要依据任务状态、优先级和既定策略作出决定。优先级继承等同步机制则可以在特定场景下缓解这种依赖带来的问题。

4. I/O 完成中断

设备完成 I/O 后,会向 CPU 发送中断。中断处理程序把对应任务从阻塞态转为就绪态:

A 等待磁盘,CPU 正在运行 B
          ↓
磁盘完成 → I/O 中断
          ↓
A:阻塞态 → 就绪态

这会给系统一次重新评估的机会,但必须区分:

I/O 中断发生,不等于一定发生任务切换。

若当前运行的 B 仍然更适合继续,系统可能在处理中断后返回 B;若刚就绪的 A 优先级更高,或者策略强调 I/O 响应,系统也可能切换到 A

5. 时钟中断

如果系统完全依赖进程主动让出 CPU,一个无限循环程序就可能让其他任务永久得不到运行机会。硬件时钟周期性地产生时钟中断,让内核重新获得控制权:

A 正在运行
   ↓
时钟中断
   ↓
内核检查时间片、优先级和就绪队列

时钟中断是实现抢占式调度的重要基础,但也要记住:

时钟中断不等于必然发生上下文切换。

中断处理结束后,调度器仍可能选择 A 继续运行。

6. 非抢占式调度

在**非抢占式调度(non-preemptive scheduling)**中,任务一旦获得 CPU,通常会一直运行到自己退出、阻塞或主动让出 CPU。即使更高优先级任务已经就绪,严格的非抢占式系统也不会强制赶走当前任务。

A 获得 CPU
     ↓
A 一直运行
     ↓
A 请求 I/O 并阻塞
     ↓
调度 B

非抢占式调度实现简单、任务切换相对较少,但一个运行时间很长的任务可能导致其他任务响应缓慢。

时钟中断仍然可以进入内核处理计时等事务,但处理完后通常继续执行原任务。中断只是暂时打断当前任务,并不必然意味着调度切换。

7. 抢占式调度

**抢占式调度(preemptive scheduling)**允许操作系统在当前任务仍能继续运行时强制收回 CPU。最常见的触发方式是时间片耗尽:

A 运行一个时间片
       ↓
时钟中断,时间片耗尽
       ↓
CPU 控制权交回调度器
       ↓
重新选择最适合运行的任务

如果调度器选择 B,系统会保存 A 的上下文,把 A 放回就绪队列,再恢复 B 的上下文。这样可以防止一个任务长期垄断 CPU。

抢占也不一定只由时间片触发。在抢占式优先级调度中,低优先级任务 A 正在运行时,高优先级任务 B 一旦就绪,系统可能立即抢占 A,无需等待 A 的时间片耗尽。

最容易混淆的一点是:

时间片耗尽不是“必须换成另一个任务”,而是当前任务失去继续独占 CPU 的资格,调度器必须重新选择。

重新选择后,如果 A 仍然是当前最合适的任务,CPU 依然可能继续运行 A

现代桌面、移动设备和服务器操作系统普遍采用抢占式调度,以兼顾公平性和交互响应。

8. 调度时机总结

                    ┌→ 创建新任务:可能重新选择
                    ├→ 当前任务退出:必须调度
当前任务运行 ────────┼→ 当前任务阻塞:必须调度
                    ├→ 时间片耗尽:重新选择
                    ├→ 高优先级任务就绪:可能抢占
                    └→ I/O 完成:任务阻塞态转为就绪态
                              ↓
                           调度器
                              ↓
                       从就绪队列选任务
                       ┌──────┴──────┐
                       ↓             ↓
                  有普通任务      没有普通任务
                       ↓             ↓
                   运行任务       运行 idle

五、调度算法的分类

不同环境有不同目标,因此调度算法通常按使用场景分为三类:

系统类型典型特征调度侧重点
批处理系统大量作业、用户很少实时交互吞吐量、周转时间、资源利用率
交互式系统用户频繁输入并等待反馈响应时间、公平性、流畅体验
实时系统任务具有明确时间限制截止时间、可预测性

同一策略不可能把所有指标都做到最好。例如:

  • 较长的时间片减少上下文切换,却可能让交互任务感觉迟钝;
  • 较短的时间片改善响应速度,却会增加切换开销;
  • 严格优先级有利于紧急任务,但若没有老化等机制,低优先级任务可能饥饿。

这也是为什么调度算法必须结合系统环境讨论。

六、调度算法的目标

1. 所有系统都关心的目标

公平

不能让某个任务无限期占用 CPU,也不能让另一些任务长期得不到运行机会。公平并不一定意味着所有任务严格平均分配 CPU;如果系统明确定义了优先级,那么按照规则让高优先级任务获得更多 CPU 仍可视为公平,关键是避免无期限饥饿。

策略强制执行

操作系统既然规定了某种调度规则,调度器就应确保规则真正生效。例如实时任务优先、后台任务只使用空闲资源、不同用户受到配额限制等,都需要由调度机制落实。

平衡

调度目标不应只盯着 CPU 利用率。适时运行 I/O 密集型任务,可以让它尽快发起 I/O;它等待设备时,CPU 再去执行计算任务,从而让 CPU、磁盘和网络等资源同时工作。

2. 批处理系统的目标

批处理任务通常不要求即时交互,因此更关注整体处理效率:

  • 吞吐量:单位时间完成多少个作业;
  • 周转时间:作业从提交到最终完成的总时间;
  • CPU 利用率:有工作可做时,避免因调度不合理而让 CPU 无意义闲置。

周转时间容易与实际运行时间混淆。例如任务在 10:00 提交、10:03 开始运行、10:08 完成:

周转时间 = 10:08 - 10:00 = 8 分钟

它包含排队等待、CPU 执行和 I/O 等待,而不只是任务真正使用 CPU 的 5 分钟。

3. 交互式系统的目标

交互式系统最在意用户是否感觉“立即有反应”。

响应时间是从用户发出请求到系统首次给出反馈的时间,不等于任务彻底完成的时间。例如点击“导出视频”后 0.1 秒显示进度条,5 分钟后导出完成:前者是响应时间,后者才是完成时间。

均衡性强调体验符合用户预期:点击菜单、移动鼠标这类轻量操作应当快速响应;压缩大文件或视频渲染允许更久。即使系统平均响应时间不错,偶尔出现数秒卡顿也会严重破坏交互体验。

4. 实时系统的目标

实时系统最核心的要求不是“平均速度越快越好”,而是在截止时间(deadline)之前完成

例如传感器每 10ms 产生一份数据,系统需要在下一份数据到来前完成当前处理:

耗时 5ms  → 满足截止时间
耗时 9ms  → 满足截止时间
耗时 11ms → 超过截止时间
  • 硬实时系统:错过截止时间不可接受,如飞行控制、汽车安全和工业设备;
  • 软实时系统:偶尔超时可以容忍,但体验或质量会下降,如视频、语音和游戏。

实时系统还强调可预测性。每一帧都在 15~17ms 完成,通常比大多数帧只需 8ms、偶尔一帧耗时 70ms 更符合实时体验。平均时间不差,并不代表最坏情况满足要求。

系统类型主要目标核心关注点
所有系统公平、策略、平衡避免饥饿、落实规则、提高整体利用率
批处理系统吞吐量、周转时间、CPU 利用率一批作业的整体处理效率
交互式系统响应时间、均衡性用户操作能否快速、稳定地得到反馈
实时系统截止时间、可预测性能否按时完成以及最坏情况是否稳定

总结

进程调度的本质,是操作系统在有限 CPU 上安排多个可运行执行流。理解它可以抓住以下主线:

  • 现代系统通常直接调度内核级线程;进程提供资源,线程承担执行;
  • 上下文保存与恢复让任务能够暂停后从原处继续,切换本身存在成本;
  • 调度器从就绪队列选择任务,常见触发包括创建、退出、阻塞、I/O 完成、时钟中断和高优先级任务到来;
  • 抢占式调度允许内核强制收回 CPU,时间片到期代表重新评估,而不是必然换人;
  • CPU 密集型与 I/O 密集型任务行为不同,合理安排可以提高 CPU 与 I/O 设备的整体利用率;
  • 批处理、交互式和实时系统分别侧重效率、响应和按时完成,没有脱离场景的“最佳算法”。

后续学习具体算法时,无论是先来先服务、最短作业优先、时间片轮转还是优先级调度,都可以回到三个问题:何时触发调度、候选任务从哪里来、系统究竟想优化什么。


如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐