Linux 进程调度与上下文切换深度详解

一、进程调度基础概念

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 调度器发展历史

  1. Linux 2.4:O (n) 调度器,每次调度遍历所有进程,进程越多越慢
  2. Linux 2.6.0 - 2.6.22:O (1) 调度器,基于优先级哈希队列,常数时间选择
  3. 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 上下文切换的完整步骤

当调度器决定切换进程时,执行以下操作:

  1. 触发中断 / 异常进入内核态

    • 时间片耗尽 → 时钟中断
    • 系统调用 → 软中断
    • IO 完成 → 硬件中断
    • 内核通过 swapgs 指令切换到内核栈
  2. 保存旧进程的硬件上下文

    • 将 CPU 所有寄存器(rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp、rip、rflags 等)压入旧进程的内核栈
    • 保存栈指针到 task_struct->thread.sp
  3. 切换内核栈

    • 将当前栈指针指向新进程的内核栈
    • 加载新进程的 task_structcurrent 全局变量
  4. 切换地址空间

    • 调用 switch_mm(),将新进程的页表基址写入 CR3 寄存器
    • TLB(转译后备缓冲器)失效刷新
    • 这是开销最大的一步,因为 TLB 失效会导致大量内存访问变慢
  5. 切换浮点 / 扩展寄存器

    • 保存旧进程的 FPU/SSE/AVX 寄存器状态
    • 加载新进程的浮点寄存器状态
  6. 恢复新进程的通用寄存器

    • 从新进程内核栈中弹出寄存器值
    • 执行 iret 指令返回用户态,CPU 从新进程的断点继续执行

3.4 上下文切换的开销

上下文切换不是免费的,典型开销在 几微秒到几十微秒 之间,主要消耗在:

  • 寄存器保存与恢复(纳秒级,很快)
  • 页表切换与 TLB 刷新(微秒级,主要开销)
  • 缓存失效:CPU 的 L1/L2/L3 缓存都是旧进程的数据,新进程需要重新预热

观测方法 :使用 vmstat 1 查看 cs 列(每秒上下文切换次数);使用 pidstat -w 查看每个进程的切换次数。


四、调度触发时机与抢占

4.1 主动调度

进程主动放弃 CPU:

  • 调用 sleep()wait() 等系统调用进入阻塞
  • 等待信号量、互斥锁
  • 进程正常退出

4.2 被动调度(抢占)

Linux 是抢占式内核,在以下时机强制调度:

  1. 时钟中断中发现时间片耗尽 :更新当前进程 vruntime,若超过阈值则设置 TIF_NEED_RESCHED 标志
  2. 中断 / 异常返回用户态前 :检查 TIF_NEED_RESCHED 标志,若置位则调用 schedule()
  3. 唤醒高优先级进程时:新唤醒的进程 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   时间片到
              高优先级唤醒
              中断返回检查

谢谢