上一篇 我们搞清了"进程是什么"(身份证、地址空间、状态机),这一篇我们聚焦 "进程如何被管理、如何被分配CPU时间" ------生命周期流转的完整过程,以及调度器选谁运行的底层逻辑。
前言:调度解决什么问题?
假设系统中有 10 个进程同时处于就绪态,但 CPU 只有 1 个(或有限个)核心。
调度器的任务就是:决定下一个运行谁,以及运行多久。
这是一个"资源分配"问题,核心矛盾在于:
- 公平性:谁都不能被饿死
- 效率:CPU 要尽可能满负荷运转
- 响应时间:交互式应用(如键盘输入)不能卡顿
- 吞吐量:批处理任务要尽可能多完成
一、进程的完整生命周期(五态模型)
上一篇我们讲了"三态模型"(就绪/运行/阻塞),但真实的进程生命周期比这更丰富。
(一) 五态模型

| 状态 | 含义 | 典型触发条件 |
|---|---|---|
| 创建态(New) | 进程正在被创建,内核已分配PCB但资源尚未就绪 | fork() 调用后、exec() 之前 |
| 就绪态(Ready) | 万事俱备,只等CPU | 创建完成 / 阻塞解除 / 时间片用完 |
| 运行态(Running) | 正在CPU上执行指令 | 被调度器选中 |
| 阻塞态(Blocked) | 因等待某事件而主动放弃CPU | read()、wait()、申请锁失败 |
| 终止态(Terminated) | 进程已结束执行,但PCB尚未被回收 | exit() 调用 / 收到终止信号 |
(二) 补充概念:挂起态(Suspend)
在内存资源紧张时,操作系统可以将某个进程的整个地址空间换出到磁盘 ,该进程进入挂起态。
- 就绪挂起:进程在外存,但随时可以调入内存执行
- 阻塞挂起:进程在外存,且在等待某个事件
引入挂起态的核心原因:内存不够用了,把暂时不跑的进程"冷冻"到磁盘上腾出空间。
(三) 状态转换的完整规则
| 转换 | 触发条件 | 谁触发 |
|---|---|---|
| 创建 → 就绪 | 资源分配完成,进程初始化完毕 | 内核 |
| 就绪 → 运行 | 调度器选中该进程 | 调度器 |
| 运行 → 就绪 | 时间片用完 / 被高优先级进程抢占 | 时钟中断 / 调度器 |
| 运行 → 阻塞 | 请求I/O、等待事件、申请锁 | 进程自身(系统调用) |
| 阻塞 → 就绪 | I/O完成、事件发生、锁可用 | 硬件中断 / 其他进程 |
| 运行 → 终止 | exit()、致命错误、被信号杀死 |
进程自身 / 内核 |
| 终止 → 消亡 | 父进程调用wait()回收资源 |
父进程 |
二、 调度算法
第一阶段:批处理时代的"公平排队"
① 先来先服务(FCFS, First Come First Served)
按到达顺序排队,非抢占。
- 优点:简单、公平、无饥饿
- 缺点:护航效应------一个长作业堵住后面所有短作业,平均等待时间极差
② 短作业优先(SJF, Shortest Job First)
预估运行时间,最短的先执行。
- 优点:平均等待时间最优(数学上已证明)
- 缺点:
-
- 需要预知运行时间(不可能精确获知)
- 长作业饥饿------短作业源源不断到来,长作业永远得不到执行
③ 高响应比优先(HRRN)
引入动态优先级 :优先级 = (等待时间 + 服务时间) / 服务时间
- 短作业优先,但长作业等待久了优先级也会提升
- 妥协方案,兼顾公平与效率
第二阶段:交互式时代的"分时轮转"
批处理时代的算法都是非抢占的,一个进程必须运行完毕才能让出CPU。这在分时系统(如Unix、Windows)中是不可接受的------用户点一下鼠标不能等几秒才响应。
④ 时间片轮转(RR, Round Robin)
每个进程分配一个固定时间片(典型值 10ms~100ms),用完即排队到队尾。
- 优点:响应快,交互体验好
- 缺点:
-
- 时间片过大 → 退化为 FCFS,交互响应变差
- 时间片过小 → 上下文切换开销占比飙升(系统把大部分时间花在"切换"而非"执行"上)
时间片大小选择的黄金法则:上下文切换开销 < 时间片的 1%。
第三阶段:兼顾公平与响应的"多级反馈队列"
⑤ 多级反馈队列(MFQ, Multilevel Feedback Queue)
这是 "兼顾响应性、公平性、效率" 的经典设计,广泛应用于现代操作系统的基础框架中。
核心机制:
markdown
┌─────────────────────────────────────────────────────────────┐
│ 就绪队列 0(优先级最高)→ 时间片 8ms → 先来先服务 │
│ 就绪队列 1(优先级中) → 时间片 16ms → 先来先服务 │
│ 就绪队列 2(优先级最低)→ 时间片 32ms → 轮转调度 │
└─────────────────────────────────────────────────────────────┘
规则:
1. 新进程入队 0(最高优先级)
2. 队列 0 中按 FCFS 执行,时间片内完成则退出;未完成则降级到队列 1
3. 队列 0 为空时,才执行队列 1;队列 1 为空时,才执行队列 2
4. 只要队列 0 中有新进程到达,立即抢占低优先级队列正在运行的进程
为什么 MFQ 是经典之作?
- 交互式进程(如编辑器) :时间片内就能完成,始终停留在高优先级队列 → 响应极快
- CPU密集型长进程 :逐渐降级到低优先级队列,获得更长的时间片 → 减少上下文切换开销
- 公平性 :低优先级队列中的进程最终也能得到执行,不会饥饿
- 无需预知运行时间:内核自动根据行为调整 → 这就是"反馈"的含义
Linux 的 CFS 虽然放弃了"多级队列"的结构,但 MFQ 的设计思想(区分I/O型与CPU型进程、动态调整优先级)深刻影响了 CFS。
三、Linux CFS(完全公平调度器):现代调度器的标杆
(一) 为什么需要 CFS?
传统 Unix 调度器(O(1)调度器)依赖复杂的优先级计算和多个运行队列,代码晦涩,且对"交互式进程"和"CPU密集型进程"的区分不够精准。
CFS(Completely Fair Scheduler,完全公平调度器)在 Linux 2.6.23 引入,核心思想简单到令人惊讶:
"给每个进程分配一个比例,确保它们获得"公平"的CPU时间份额。"
(二) 核心概念:vruntime(虚拟运行时间)
这是 CFS 的精髓。
- vruntime 记录了一个进程"已经消耗了多少CPU时间",单位是纳秒
- 但 vruntime 的增长速度是按优先级加权的:
-
- 普通进程(nice=0):vruntime ≈ 实际运行时间
- 高优先级(nice=-20):vruntime 增长更慢(同样的物理时间,vruntime加得更少)
- 低优先级(nice=+19):vruntime 增长更快
CFS 的调度决策极其简单:永远选择 vruntime 最小的进程运行。
这就像一群人在排队领饭,每个人领完后会在"我已经吃了多少"的计数器上加一笔。CFS 总是让吃得最少的人先去吃------这样长期来看,所有人都吃得一样多,但"权重高"的人每口饭加得少,相当于能多吃几口。
(三) 数据结构:红黑树(Red-Black Tree)
CFS 如何快速找到 vruntime 最小的进程?
答案:用 红黑树 组织所有就绪进程,以 vruntime 为键值。
- 查找最小节点:O(log N),即使系统有 10000 个进程也能飞快完成
- 插入/删除:O(log N),进程状态变化时高效更新
- 自平衡:树的高度始终可控,不会退化
scss
[进程C vruntime=50]
/ \
[进程A vruntime=30] [进程D vruntime=80]
/ \
[进程E vruntime=10] [进程B vruntime=40]
(← 最左节点,vruntime最小,下一个运行)
(四) nice 值的含义
nice 值 是用户控制进程优先级的接口,范围 -20 ~ +19:
- nice 值越低 → 优先级越高 → 获得更多CPU时间
- nice 值越高 → 优先级越低 → 获得更少CPU时间
- 默认 nice = 0(普通优先级)
只有 root 可以调低 nice 值(提高优先级),普通用户只能调高 nice 值(降低优先级) 。
| nice 值 | 优先级 | 适用场景 |
|---|---|---|
| -20 ~ -10 | 极高 | 系统关键进程(不建议普通程序使用) |
| -5 ~ -1 | 较高 | 对延迟敏感的服务(如数据库) |
| 0 | 普通 | 大多数应用程序 |
| 5 ~ 10 | 较低 | 后台批处理任务(如日志压缩、备份) |
| 15 ~ 19 | 极低 | 非紧急的后台任务,保证不影响交互体验 |
在 CFS 中,nice 值与 vruntime 增长速率的关系:
约每降低一个 nice 值,CPU 时间份额增加约 10%。
即:nice=0 的进程获得 1 份时间;nice=-1 获得约 1.1 份;nice=+1 获得约 0.9 份。
两个 nice 值相差 1 的进程,长期 CPU 分配比例约为 1.1 : 1。
(五) CFS 的抢占时机
CFS 不是"时间片"驱动的,而是通过 sched_latency_ns(调度延迟)决定每个进程能运行多久:
- 目标调度延迟默认约 6ms(可配置)
- 如果有 N 个就绪进程,每个进程一次最多运行
目标延迟 / N - 当一个进程运行时间超过它的"应得份额"时,CFS 会触发抢占
效果 :进程数少时,每个进程分到更长时间片(减少切换开销);进程数多时,每个进程分到更短时间片(保证响应性)。自适应,无需人工配置。
四、总结:
| 调度算法 | 核心思想 | 适用场景 | 核心问题 |
|---|---|---|---|
| FCFS | 先来先服务 | 批处理系统 | 护航效应 |
| SJF | 最短先执行 | 理想化模型 | 需预知时间、长作业饥饿 |
| 时间片轮转 | 固定时间片轮流 | 分时交互系统 | 时间片大小难调优 |
| 多级反馈队列 | 多级优先级 + 降级 + 抢占 | 通用操作系统基础框架 | 参数复杂 |
| CFS | vruntime最小者优先 + 红黑树 | Linux(通用现代系统) | 几乎无短板 |
- 调度的本质不是"选谁",而是"如何量化公平" ------CFS 用 vruntime 给出了一个优雅的数学答案
- 多级反馈队列是"自适应"的先驱------进程的行为(I/O密集型 vs CPU密集型)决定了它在哪一级队列,无需管理员手动配置
- nice 值不是"绝对优先级",而是"权重比例" ------CFS 中的 nice 控制的是 CPU 时间份额的相对比例,而非谁先谁后
- "抢占"与"切换"是两码事 ------调度器决定哪个进程该获得CPU,但真正让CPU放弃当前进程的,是时钟中断 或其他硬件中断
本文是「进程管理」系列的第二篇,欢迎关注,持续更新。如有任何疑问或指正,请在评论区留言讨论。