Linux 调度器底层原理深度解析

引言

调度是操作系统最核心的功能之一,它决定了哪个进程/线程在什么时候获得 CPU 执行权。本文将深入剖析 Linux 调度器的底层原理,从基础概念到内核实现,帮助你全面理解调度机制。

1. 调度基础概念

1.1 什么是调度

调度 (Scheduling) 是操作系统决定哪个进程/线程获得 CPU 执行权的过程。在任一时刻,单核 CPU 只能运行一个进程,调度器的任务就是在多个可运行进程之间分配 CPU 时间。

就绪队列                                    
┌───┐ ┌───┐ ┌───┐ ┌───┐                  
│ P1│ │ P2│ │ P3│ │ P4│  ──→  调度器  ──→  CPU
└───┘ └───┘ └───┘ └───┘                  

1.2 调度目标

调度器需要平衡多个目标:

目标说明
CPU 利用率尽量让 CPU 保持忙碌
吞吐量单位时间完成的进程数
响应时间从提交到首次响应的时间
等待时间在就绪队列中等待的时间
公平性每个进程都能获得 CPU

1.3 进程状态

Linux 进程状态与调度密切相关:

状态符号说明
TASK_RUNNINGR运行中或在运行队列中等待
TASK_INTERRUPTIBLES可中断睡眠,等待事件
TASK_UNINTERRUPTIBLED不可中断睡眠,等待 I/O
TASK_STOPPEDT被信号停止
TASK_TRACEDt被调试器停止
EXIT_ZOMBIEZ僵尸状态
        创建 ──────────→ 就绪 (R) ←──────┐
                        │ 调度          │ 时间片用完
                        ↓               │ 或被抢占
                      运行 (R) ─────────┘
                        │
          ┌─────────────┼─────────────┐
          ↓             ↓             ↓
       睡眠 (S)      阻塞 (D)       终止

1.4 调度时机

调度发生在以下时机:

主动让出 CPU:

  • 调用 sleep(), wait(), pause()
  • 调用 sched_yield()
  • 等待 I/O 完成
  • 等待锁或信号量

被动让出 CPU:

  • 时间片用完(时钟中断)
  • 更高优先级进程就绪
  • 被信号中断

1.5 上下文切换

上下文切换是保存当前进程状态、恢复另一个进程状态的过程:

// 简化的上下文切换流程
void context_switch(struct task_struct *prev, struct task_struct *next) {
    // 1. 保存 prev 的 CPU 寄存器
    save_registers(prev);
    
    // 2. 切换内存映射 (页表)
    switch_mm(prev->mm, next->mm);
    
    // 3. 恢复 next 的 CPU 寄存器
    restore_registers(next);
}

上下文切换开销:

  • 直接开销:保存/恢复寄存器、切换页表
  • 间接开销:CPU 缓存失效、TLB 刷新
  • 典型耗时:几微秒到几十微秒

1.6 调度器发展历史

版本调度器特点
Linux 2.4O(n)遍历所有进程,进程多时性能差
Linux 2.6.0O(1)常数时间选择,但交互性不够好
Linux 2.6.23+CFS基于虚拟运行时间,更公平
Linux 6.6+EEVDFCFS 改进版,更好的延迟保证

2. Linux 调度策略

Linux 提供多种调度策略,适用于不同场景:

2.1 调度策略概览

策略类型优先级时间片适用场景
SCHED_DEADLINE实时最高CBS周期实时任务
SCHED_FIFO实时1-99硬实时
SCHED_RR实时1-99软实时
SCHED_OTHER普通nice动态普通进程
SCHED_BATCH普通nice动态批处理
SCHED_IDLE普通最低动态空闲时运行

优先级顺序:

SCHED_DEADLINE > SCHED_FIFO/RR > SCHED_OTHER > SCHED_BATCH > SCHED_IDLE

2.2 SCHED_OTHER (CFS)

默认调度策略,由 CFS(完全公平调度器)实现:

// 查看当前调度策略
int policy = sched_getscheduler(0);
// 返回 SCHED_OTHER (0)

特点:

  • 基于虚拟运行时间的公平调度
  • 通过 nice 值调整优先级(-20 到 19)
  • 适用于大多数普通应用

2.3 SCHED_FIFO

先进先出实时调度:

struct sched_param param;
param.sched_priority = 50;  // 1-99
sched_setscheduler(0, SCHED_FIFO, &param);

特点:

  • 无时间片限制
  • 一直运行直到阻塞、让出或被更高优先级抢占
  • ⚠️ 可能导致系统无响应

2.4 SCHED_RR

实时轮转调度:

struct sched_param param;
param.sched_priority = 30;
sched_setscheduler(0, SCHED_RR, &param);

// 获取时间片
struct timespec ts;
sched_rr_get_interval(0, &ts);

特点:

  • 类似 FIFO,但有时间片
  • 时间片用完后移到同优先级队列末尾

2.5 SCHED_DEADLINE

基于截止时间的调度(Linux 3.14+):

struct sched_attr attr;
attr.size = sizeof(attr);
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 10000000;   // 10ms
attr.sched_deadline = 50000000;  // 50ms
attr.sched_period = 100000000;   // 100ms

syscall(SYS_sched_setattr, 0, &attr, 0);

参数含义:

|<──────── period ────────>|
|                          |
|<─ runtime ─>|<─deadline─>|
┌─────────────┐            │
│   执行时间   │            │
└─────────────┘            │
↑                          ↑
开始                       截止

2.6 命令行工具

# 查看进程调度策略
chrt -p <pid>

# 设置调度策略
sudo chrt -f 50 ./program      # SCHED_FIFO
sudo chrt -r 30 ./program      # SCHED_RR
sudo chrt -o 0 ./program       # SCHED_OTHER
sudo chrt -b 0 ./program       # SCHED_BATCH
sudo chrt -i 0 ./program       # SCHED_IDLE

3. CFS 调度器详解

CFS(Completely Fair Scheduler)是 Linux 2.6.23 引入的默认调度器。

3.1 核心思想

理想情况: N 个进程同时运行,每个获得 1/N 的 CPU。

现实: 单核一次只能运行一个进程。

解决方案: 跟踪每个进程的"虚拟运行时间"(vruntime),总是选择 vruntime 最小的进程运行。

3.2 虚拟运行时间 (vruntime)

vruntime = 实际运行时间 × (基准权重 / 进程权重)

计算示例:

进程nice权重实际运行vruntime
A0102410ms10ms
B-5312110ms3.3ms
C533510ms30.6ms

意义:

  • 高优先级进程:vruntime 增长慢 → 更多运行机会
  • 低优先级进程:vruntime 增长快 → 更少运行机会

3.3 红黑树

CFS 使用红黑树管理就绪进程,按 vruntime 排序:

                    ┌─────────────┐
                    │ vruntime=50 │
                    │   (黑色)    │
                    └──────┬──────┘
               ┌───────────┴───────────┐
        ┌──────┴──────┐         ┌──────┴──────┐
        │ vruntime=30 │         │ vruntime=70 │
        │   (红色)    │         │   (红色)    │
        └─────────────┘         └─────────────┘

操作复杂度:

  • 插入/删除:O(log n)
  • 查找最小值:O(1)(缓存最左节点)

3.4 权重计算

nice 值到权重的映射:

nice权重相对 CPU 时间
-2088761×87
-109548×9
01024×1 (基准)
10110×1/9
1915×1/68

每相差 1 个 nice 值,权重约差 1.25 倍。

3.5 时间片计算

时间片 = 调度周期 × (进程权重 / 运行队列总权重)

关键参数:

  • sched_latency_ns: 调度周期(默认 6ms)
  • sched_min_granularity_ns: 最小时间片(默认 0.75ms)

3.6 CFS 参数调优

# 查看 CFS 参数
cat /proc/sys/kernel/sched_latency_ns
cat /proc/sys/kernel/sched_min_granularity_ns
cat /proc/sys/kernel/sched_wakeup_granularity_ns

# 修改参数(临时)
echo 3000000 > /proc/sys/kernel/sched_latency_ns

# 永久修改
echo "kernel.sched_latency_ns = 3000000" >> /etc/sysctl.conf
sysctl -p

调优建议:

场景建议
桌面/交互减小 latency 和 granularity
服务器/吞吐增大 latency 和 granularity
低延迟减小 wakeup_granularity

4. 实时调度

4.1 实时系统分类

  • 硬实时: 必须在截止时间前完成,否则系统失败
  • 软实时: 尽量在截止时间前完成,偶尔超时可接受

4.2 SCHED_FIFO vs SCHED_RR

特性SCHED_FIFOSCHED_RR
时间片有(约100ms)
同优先级调度FIFO轮转
运行时间直到阻塞/让出时间片用完后轮转

4.3 实时带宽限制

# 查看限制
cat /proc/sys/kernel/sched_rt_period_us   # 1000000 (1秒)
cat /proc/sys/kernel/sched_rt_runtime_us  # 950000 (0.95秒)

# 含义:实时进程最多占用 95% CPU

4.4 设置实时调度

#include <sched.h>

struct sched_param param;
param.sched_priority = 50;  // 1-99

if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
    perror("sched_setscheduler");
}

4.5 实时调度注意事项

⚠️ 警告:

  1. SCHED_FIFO 进程死循环会独占 CPU
  2. 高优先级实时进程会抢占所有低优先级进程
  3. 需要 root 权限

安全措施:

  • 保留紧急恢复手段(SSH 会话)
  • 使用看门狗定时器
  • 先在虚拟机测试
  • 不使用优先级 99(留给内核)

5. 优先级与权重

5.1 优先级体系

Linux 使用 0-139 的内部优先级:

    0 ──────────── 99 ──────────── 139
    │              │               │
    │   实时进程   │   普通进程    │
    │  (RT_PRIO)   │   (nice)      │

映射关系:

  • 实时优先级 1-99 → 内核优先级 98-0(反转)
  • nice -20 到 19 → 内核优先级 100-139

5.2 nice 值

#include <sys/resource.h>

// 获取 nice 值
int nice_val = getpriority(PRIO_PROCESS, 0);

// 设置 nice 值
setpriority(PRIO_PROCESS, 0, 10);

// 增加 nice 值
nice(5);
# 命令行
nice -n 10 ./program        # 以 nice=10 启动
renice -n 5 -p <pid>        # 修改运行中进程

5.3 优先级反转

问题场景:

  1. 低优先级进程持有锁
  2. 高优先级进程等待锁
  3. 中优先级进程抢占低优先级进程

解决方案:

// 优先级继承
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);

6. CPU 亲和性

6.1 概念

CPU 亲和性指定进程/线程可以在哪些 CPU 上运行:

  • 软亲和性: 调度器尽量让进程在同一 CPU 运行(默认)
  • 硬亲和性: 强制进程只能在指定 CPU 运行

6.2 API

#include <sched.h>

// 获取亲和性
cpu_set_t mask;
CPU_ZERO(&mask);
sched_getaffinity(0, sizeof(mask), &mask);

// 设置亲和性
CPU_ZERO(&mask);
CPU_SET(0, &mask);  // 绑定到 CPU 0
CPU_SET(1, &mask);  // 也允许 CPU 1
sched_setaffinity(0, sizeof(mask), &mask);

6.3 CPU 集合操作

CPU_ZERO(&mask)       // 清空
CPU_SET(cpu, &mask)   // 添加
CPU_CLR(cpu, &mask)   // 移除
CPU_ISSET(cpu, &mask) // 检查
CPU_COUNT(&mask)      // 计数

6.4 线程亲和性

#include <pthread.h>

pthread_t thread = pthread_self();
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask);
pthread_setaffinity_np(thread, sizeof(mask), &mask);

6.5 命令行工具

# 查看亲和性
taskset -p <pid>

# 设置亲和性
taskset -c 0,1 ./program        # 绑定到 CPU 0,1
taskset 0x3 ./program           # 掩码方式
taskset -cp 0,1 <pid>           # 修改运行中进程

6.6 isolcpus

隔离 CPU,使其不参与常规调度:

# 内核启动参数
isolcpus=2,3

# 使用隔离的 CPU
taskset -c 2,3 ./realtime_app

7. 多核调度

7.1 SMP 调度

每个 CPU 有独立的运行队列:

┌─────────┐  ┌─────────┐  ┌─────────┐
│  CPU 0  │  │  CPU 1  │  │  CPU 2  │
├─────────┤  ├─────────┤  ├─────────┤
│ P1, P4  │  │ P2, P5  │  │ P3, P6  │
└─────────┘  └─────────┘  └─────────┘

7.2 负载均衡

调度器定期检查各 CPU 负载,进行进程迁移:

【负载均衡前】           【负载均衡后】
CPU0: ████████  (8)     CPU0: ████  (4)
CPU1: ██  (2)           CPU1: ████  (4)
CPU2: ██  (2)           CPU2: ████  (4)

负载均衡时机:

  • 周期性均衡
  • CPU 空闲时拉取任务
  • 进程唤醒时
  • fork 时

7.3 调度域

按 CPU 拓扑结构分层:

Level 0: SMT (超线程) - 共享 L1/L2
Level 1: MC (多核)    - 共享 L3
Level 2: NUMA        - 本地内存
Level 3: 系统        - 所有 CPU

7.4 NUMA 调度

┌──────────────┐              ┌──────────────┐
│  NUMA 节点 0 │              │  NUMA 节点 1 │
│  CPU 0-3     │ ←──────────→│  CPU 4-7     │
│  本地内存 A  │   互联(慢)  │  本地内存 B  │
└──────────────┘              └──────────────┘

NUMA 感知调度:

  • 尽量在本地节点运行进程
  • 考虑内存位置进行迁移决策

8. cgroups 调度控制

8.1 cgroups 概念

cgroups 用于限制、记录和隔离进程组的资源使用:

/sys/fs/cgroup/
├── cpu/
│   ├── tasks
│   ├── cpu.shares
│   └── mygroup/
│       ├── tasks
│       └── cpu.shares
└── cpuset/

8.2 cpu.shares

相对权重,用于 CPU 资源分配:

# 创建 cgroup
mkdir /sys/fs/cgroup/cpu/mygroup

# 设置权重
echo 1024 > /sys/fs/cgroup/cpu/mygroup/cpu.shares

# 添加进程
echo <pid> > /sys/fs/cgroup/cpu/mygroup/tasks

8.3 CFS 配额

# 限制为 1 个 CPU
echo 100000 > cpu.cfs_period_us
echo 100000 > cpu.cfs_quota_us

# 限制为 0.5 个 CPU
echo 100000 > cpu.cfs_period_us
echo 50000 > cpu.cfs_quota_us

# 限制为 2 个 CPU
echo 100000 > cpu.cfs_period_us
echo 200000 > cpu.cfs_quota_us

8.4 cpuset

# 绑定 CPU
echo 0-1 > /sys/fs/cgroup/cpuset/mygroup/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/mygroup/cpuset.mems

8.5 容器中的调度

# Docker
docker run --cpus=2 --cpu-shares=512 --cpuset-cpus=0,1 myimage

# Kubernetes
resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "1"

9. 调度性能分析

9.1 perf sched

# 记录调度事件
perf sched record -- sleep 1

# 查看延迟统计
perf sched latency

# 查看时间线
perf sched map
perf sched timehist

9.2 /proc 信息

# 进程调度统计
cat /proc/<pid>/sched
cat /proc/<pid>/schedstat

# 系统调度统计
cat /proc/schedstat

# 调度参数
ls /proc/sys/kernel/sched_*

9.3 上下文切换监控

# 系统级
vmstat 1              # cs 列

# 进程级
pidstat -w 1

# 性能计数
perf stat -e context-switches ./program

9.4 运行队列监控

# 负载平均
uptime
cat /proc/loadavg

# 实时监控
sar -q 1

10. 内核调度器内部

10.1 task_struct 调度字段

struct task_struct {
    volatile long state;          // 进程状态
    int prio, static_prio;        // 优先级
    unsigned int rt_priority;     // 实时优先级
    const struct sched_class *sched_class;
    struct sched_entity se;       // CFS 调度实体
    struct sched_rt_entity rt;    // RT 调度实体
    unsigned int policy;          // 调度策略
    cpumask_t cpus_allowed;       // CPU 亲和性
};

10.2 调度类

// 调度类层次(优先级从高到低)
stop_sched_class      // 停止 CPU
    ↓
dl_sched_class        // DEADLINE
    ↓
rt_sched_class        // FIFO/RR
    ↓
fair_sched_class      // CFS
    ↓
idle_sched_class      // IDLE

10.3 调度类接口

struct sched_class {
    void (*enqueue_task)(rq, task, flags);
    void (*dequeue_task)(rq, task, flags);
    void (*yield_task)(rq);
    struct task_struct *(*pick_next_task)(rq);
    void (*put_prev_task)(rq, task);
    void (*task_tick)(rq, task, queued);
};

10.4 schedule() 流程

void schedule(void) {
    preempt_disable();
    __schedule(false);
    preempt_enable();
}

void __schedule(bool preempt) {
    // 1. 获取当前 CPU 运行队列
    rq = this_rq();
    prev = rq->curr;
    
    // 2. 选择下一个任务
    next = pick_next_task(rq, prev);
    
    // 3. 上下文切换
    if (prev != next) {
        context_switch(rq, prev, next);
    }
}

11. 调度优化实践

11.1 CPU 密集型优化

# 使用 SCHED_BATCH
chrt -b 0 ./cpu_bound_program

# 增大 nice 值(后台任务)
nice -n 10 ./program

# CPU 绑定
taskset -c 0-3 ./program

11.2 I/O 密集型优化

  • 保持默认 SCHED_OTHER
  • CFS 自动给予 I/O 密集型进程优先
  • 不要绑定到单个 CPU

11.3 延迟敏感应用

# 减小调度周期
echo 1000000 > /proc/sys/kernel/sched_latency_ns

# 隔离 CPU
# 内核参数: isolcpus=2,3
taskset -c 2,3 ./latency_app

# 禁用频率调节
cpupower frequency-set -g performance

11.4 实时应用清单

  1. 使用实时调度策略
  2. 锁定内存:mlockall(MCL_CURRENT | MCL_FUTURE)
  3. 隔离 CPU:isolcpus=N nohz_full=N
  4. 禁用中断
  5. 使用 PREEMPT_RT 内核
  6. 避免动态内存分配和非必要系统调用

11.5 常见问题诊断

问题诊断解决
高调度延迟perf sched latency减少进程数、提高优先级
频繁上下文切换vmstat 1, pidstat -w 1优化锁、减少进程数
CPU 负载不均mpstat -P ALL 1调整亲和性
实时任务节流dmesg | grep throttl增大 rt_runtime_us

总结

Linux 调度器是一个复杂而精巧的系统,理解其底层原理对于系统性能优化至关重要。关键要点:

  1. CFS 是默认调度器,基于虚拟运行时间实现公平
  2. 实时调度 适用于时间敏感任务,但需要谨慎使用
  3. CPU 亲和性 可以优化缓存利用和 NUMA 访问
  4. cgroups 提供灵活的资源控制机制
  5. 性能分析 工具帮助定位调度问题

配套资源

https://download.csdn.net/download/weixin_43912621/92755817

在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐