一、进程调度基础概念
1.1 什么是进程调度
在 Linux 系统中,CPU 核心数量远少于进程数量,操作系统必须决定哪个进程在什么时候使用 CPU ,这个决策过程就是进程调度。调度器(Scheduler)是内核的核心组件,它从就绪队列中挑选最合适的进程分配 CPU 时间片,实现多任务并发执行。
调度的核心目标:
- CPU 利用率:让 CPU 尽量不空闲
- 吞吐量:单位时间完成的进程数
- 周转时间:进程从提交到完成的总时间
- 响应时间:用户交互到得到反馈的时间
- 公平性:每个进程都能分到 CPU 时间
1.2 Linux 进程的五种状态
Linux 中进程在其生命周期内会在不同状态间切换,调度只在就绪态的进程中选择:
plaintext
创建 → 就绪 ←→ 运行 → 终止
↖ ↙
阻塞
表格
| 状态 | 内核标识 | 说明 |
|---|---|---|
| 运行态 | TASK_RUNNING |
正在 CPU 上执行,或在就绪队列中等待调度 |
| 可中断阻塞 | TASK_INTERRUPTIBLE |
等待事件 / 资源,收到信号可唤醒 |
| 不可中断阻塞 | TASK_UNINTERRUPTIBLE |
等待硬件资源(如磁盘 IO),不响应信号 |
| 停止态 | TASK_STOPPED |
被暂停(如收到 SIGSTOP) |
| 僵尸态 | TASK_ZOMBIE |
进程已结束,但父进程未回收资源 |
使用
ps aux命令时,STAT列的R表示运行 / 就绪,S表示可中断睡眠,D表示不可中断睡眠,Z表示僵尸。
二、Linux 内核调度器演进
2.1 调度器发展历史
- Linux 2.4:O (n) 调度器,每次调度遍历所有进程,进程越多越慢
- Linux 2.6.0 - 2.6.22:O (1) 调度器,基于优先级哈希队列,常数时间选择
- Linux 2.6.23 至今:CFS 完全公平调度器(Completely Fair Scheduler),默认调度器
2.2 CFS 完全公平调度器(核心)
CFS 是 Linux 默认的普通进程调度器,核心思想是给每个进程公平分配 CPU 时间。
核心概念:虚拟运行时间 vruntime
- 每个进程都维护一个
vruntime(虚拟运行时间),记录进程已经运行了多久 - 优先级高的进程,vruntime 增长慢;优先级低的进程,vruntime 增长快
- CFS 总是选择 vruntime 最小的进程运行
红黑树数据结构
CFS 使用红黑树(rbtree)管理就绪队列:
- 键:vruntime 值
- 值:进程描述符
task_struct - 最左侧节点就是 vruntime 最小的进程,即下一个要运行的进程
plaintext
红黑树(就绪队列)
┌───┐
│ 20│
└───┘
/ \
┌───┐ ┌───┐
│ 10│ │ 35│
└───┘ └───┘
/ \
┌───┐ ┌───┐
│ 5 │ │ 15│ ← 最左节点=下一个运行的进程
└───┘ └───┘
时间片计算
CFS 没有固定时间片,而是根据当前就绪进程数动态划分:
plaintext
目标调度周期 = 20ms(默认)
每个进程时间片 = 目标调度周期 / 就绪进程数
例如 4 个进程就绪,每个分到 5ms;10 个进程就绪,每个分到 2ms。
2.3 实时调度
Linux 还支持两种实时调度策略,优先级高于 CFS:
- SCHED_FIFO:先进先出,高优先级一直运行直到主动放弃
- SCHED_RR:时间片轮转,同优先级轮流运行
实时进程优先级范围 1-99,数字越大优先级越高;普通进程 nice 值 -20 到 +19,对应优先级 100-139。
三、上下文切换完整机制
3.1 什么是上下文切换
上下文切换(Context Switch)是 CPU 从一个进程切换到另一个进程时,保存旧进程运行现场 + 加载新进程现场的操作。切换完全在内核态完成,对用户进程透明。
3.2 进程描述符 task_struct
Linux 中每个进程对应一个 task_struct 结构体(即 PCB),保存了进程的全部上下文信息,核心字段包括:
c
运行
struct task_struct {
volatile long state; // 进程状态
void *stack; // 内核栈指针
pid_t pid; // 进程ID
int prio; // 优先级
unsigned long vruntime; // 虚拟运行时间
struct mm_struct *mm; // 内存管理(页表、虚拟地址空间)
struct files_struct *files;// 打开的文件描述符表
struct signal_struct *signal; // 信号处理
struct pt_regs *regs; // CPU寄存器快照
// ... 上百个字段
};
3.3 上下文切换的完整步骤
当调度器决定切换进程时,执行以下操作:
-
触发中断 / 异常进入内核态
- 时间片耗尽 → 时钟中断
- 系统调用 → 软中断
- IO 完成 → 硬件中断
- 内核通过
swapgs指令切换到内核栈
-
保存旧进程的硬件上下文
- 将 CPU 所有寄存器(rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp、rip、rflags 等)压入旧进程的内核栈
- 保存栈指针到
task_struct->thread.sp
-
切换内核栈
- 将当前栈指针指向新进程的内核栈
- 加载新进程的
task_struct到current全局变量
-
切换地址空间
- 调用
switch_mm(),将新进程的页表基址写入 CR3 寄存器 - TLB(转译后备缓冲器)失效刷新
- 这是开销最大的一步,因为 TLB 失效会导致大量内存访问变慢
- 调用
-
切换浮点 / 扩展寄存器
- 保存旧进程的 FPU/SSE/AVX 寄存器状态
- 加载新进程的浮点寄存器状态
-
恢复新进程的通用寄存器
- 从新进程内核栈中弹出寄存器值
- 执行
iret指令返回用户态,CPU 从新进程的断点继续执行
3.4 上下文切换的开销
上下文切换不是免费的,典型开销在 几微秒到几十微秒 之间,主要消耗在:
- 寄存器保存与恢复(纳秒级,很快)
- 页表切换与 TLB 刷新(微秒级,主要开销)
- 缓存失效:CPU 的 L1/L2/L3 缓存都是旧进程的数据,新进程需要重新预热
观测方法 :使用
vmstat 1查看cs列(每秒上下文切换次数);使用pidstat -w查看每个进程的切换次数。
四、调度触发时机与抢占
4.1 主动调度
进程主动放弃 CPU:
- 调用
sleep()、wait()等系统调用进入阻塞 - 等待信号量、互斥锁
- 进程正常退出
4.2 被动调度(抢占)
Linux 是抢占式内核,在以下时机强制调度:
- 时钟中断中发现时间片耗尽 :更新当前进程 vruntime,若超过阈值则设置
TIF_NEED_RESCHED标志 - 中断 / 异常返回用户态前 :检查
TIF_NEED_RESCHED标志,若置位则调用schedule() - 唤醒高优先级进程时:新唤醒的进程 vruntime 更小,触发抢占
4.3 用户态抢占与内核态抢占
- 用户态抢占:中断返回用户态时可以调度,所有 Linux 版本都支持
- 内核态抢占 :内核执行过程中也可以被抢占(2.6 之后支持,需开启
CONFIG_PREEMPT)
五、Linux 调度实操举例
例 1:查看进程调度信息
bash
运行
# 查看进程调度策略和优先级
chrt -p 1234
# 查看进程详细调度统计
cat /proc/1234/sched
例 2:修改进程 nice 值(优先级)
bash
运行
# 以 nice=10 启动程序
nice -n 10 ./myprogram
# 修改运行中进程的 nice 值
renice -5 -p 1234
例 3:设置实时调度策略
bash
运行
# 设置为 FIFO 实时调度,优先级 50
chrt -f -p 50 1234
例 4:观测上下文切换
bash
运行
# 每秒查看系统整体上下文切换
vmstat 1
# 查看指定进程的自愿/非自愿切换次数
pidstat -w -p 1234 1
六、进程调度知识体系图
plaintext
Linux 进程调度与切换
│
┌───────────────────┼───────────────────┐
│ │ │
调度基础 调度器实现 上下文切换
│ │ │
┌─────┴─────┐ ┌────┴────┐ ┌────┴────┐
│ │ │ │ │ │
进程状态 调度目标 CFS公平调度 实时调度 保存现场 恢复现场
│ │ │ │ │ │
R/S/D/Z CPU利用率 vruntime SCHED_FIFO 寄存器 内核栈切换
响应时间 红黑树 SCHED_RR 页表CR3 TLB刷新
公平性 动态时间片 优先级1-99 FPU状态 缓存失效
│
触发时机
│
┌─────┴─────┐
主动调度 抢占调度
sleep/exit 时间片到
高优先级唤醒
中断返回检查
谢谢