跳转至

调度

是什么 / 解决什么问题

Linux 调度器决定哪个线程在哪个 CPU 核心上运行、运行多长时间。理解调度行为有助于诊断延迟抖动、CPU 利用不均等性能问题。

调度策略

CFS (Completely Fair Scheduler) — 默认

原理:维护虚拟运行时间 (vruntime),始终调度 vruntime 最小的任务
  → CPU 时间按权重"公平"分配
  → nice 值影响权重(nice 越低,权重越大,分配更多 CPU)

数据结构:红黑树,按 vruntime 排序
时间复杂度:O(log n) 选择下一个任务

nice 值与 CPU 份额:

nice 权重 相对 nice 0 的 CPU 份额
-20 88761 ~40x
-10 9548 ~4x
0 1024 1x(基准)
10 110 ~0.1x
19 15 ~0.015x
// 设置 nice 值(-20 到 19)
nice(10);  // 降低优先级

// 或更精确的
setpriority(PRIO_PROCESS, 0, 10);

SCHED_FIFO — 实时先入先出

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

// 行为:
// - 高优先级 FIFO 线程始终抢占低优先级
// - 同优先级之间 FIFO(不主动让出就一直运行)
// - 没有时间片概念

SCHED_RR — 实时轮转

struct sched_param param = { .sched_priority = 50 };
sched_setscheduler(0, SCHED_RR, &param);

// 行为:同 SCHED_FIFO,但同优先级之间时间片轮转

SCHED_DEADLINE — 截止时间调度

struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime = 10000000,   // 10ms 运行预算
    .sched_deadline = 30000000,  // 30ms 内必须完成
    .sched_period = 30000000,    // 30ms 周期
};
syscall(SYS_sched_setattr, 0, &attr, 0);

// 最高优先级(高于所有 RT 任务)
// 内核保证在 deadline 前分配到 runtime 的 CPU 时间

优先级层次

SCHED_DEADLINE  (最高)
SCHED_FIFO / SCHED_RR  (实时,优先级 1-99)
SCHED_OTHER (CFS, nice -20 到 19)  (最低)

使用模式

CPU 亲和性 (affinity)

#define _GNU_SOURCE
#include <sched.h>

// 绑定到 CPU 0 和 1
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
CPU_SET(1, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);

// 查询当前亲和性
sched_getaffinity(0, sizeof(cpuset), &cpuset);
for (int i = 0; i < CPU_SETSIZE; i++) {
    if (CPU_ISSET(i, &cpuset))
        printf("can run on CPU %d\n", i);
}

用途: - 避免缓存失效(线程不跨核迁移) - 隔离延迟敏感线程到专用核心 - NUMA 感知的线程分布

cgroup CPU 控制

# cgroup v2: 限制 CPU 使用
echo "100000 100000" > /sys/fs/cgroup/myapp/cpu.max
# 含义:每 100ms 周期内最多用 100ms → 1 个 CPU 的 100%
# "50000 100000" → 0.5 个 CPU

# 设置 CPU 权重(类似 nice)
echo 200 > /sys/fs/cgroup/myapp/cpu.weight
# 默认 100,200 表示两倍份额

sched_yield

sched_yield();  // 主动让出 CPU 给其他可运行线程

// 几乎不该使用:
// - CFS 下可能立即被重新调度(因为 vruntime 最小)
// - 正确做法:用 mutex/condvar/futex 等待事件

行为与语义

调度延迟

任务变为可运行(如:被 futex WAKE 唤醒)
  → 不一定立即获得 CPU
  → 调度延迟 = 变为可运行到实际运行的时间

影响因素:
  - 所有 CPU 都被更高优先级任务占满
  - CFS 的目标延迟(sched_latency_ns)
  - 中断处理、内核临界区

CFS 参数

# 调度周期(所有可运行任务分享的时间窗口)
cat /proc/sys/kernel/sched_latency_ns
# 默认 6ms(如果 ≤ 8 个可运行任务)

# 最小时间片
cat /proc/sys/kernel/sched_min_granularity_ns
# 默认 0.75ms

# 唤醒抢占粒度
cat /proc/sys/kernel/sched_wakeup_granularity_ns
# 默认 1ms(唤醒任务 vruntime 差距超过此值才抢占)

NUMA 与调度

NUMA (Non-Uniform Memory Access):
  - 多 CPU socket 系统,每个 socket 有本地内存
  - 访问本地内存快,访问远端内存慢(~2x 延迟)

调度器行为:
  - 尽量让线程在本地 NUMA node 运行
  - 自动 NUMA balancing:迁移线程或内存页以减少远端访问
# 查看 NUMA 拓扑
numactl --hardware

# 绑定到指定 NUMA node
numactl --cpunodebind=0 --membind=0 ./app

性能考量

延迟敏感应用的调优

# 1. isolcpus: 内核启动参数隔离 CPU
# GRUB: isolcpus=2,3
# 隔离的 CPU 不会被 CFS 调度普通任务

# 2. 绑定关键线程到隔离 CPU
taskset -c 2 ./latency_critical_thread

# 3. 使用 SCHED_FIFO
chrt -f 50 ./realtime_app

# 4. 关闭节能(C-states 导致唤醒延迟)
# /sys/devices/system/cpu/cpuN/cpuidle/stateN/disable

上下文切换开销

自愿切换(线程主动让出,如 I/O 等待):~1-5μs
非自愿切换(时间片用完被抢占):~5-15μs

主要开销:
  - 保存/恢复寄存器
  - TLB 刷新(如果切换到不同进程)
  - 缓存冷启动
# 查看进程的上下文切换次数
cat /proc/<pid>/status | grep ctxt
# voluntary_ctxt_switches: 主动切换
# nonvoluntary_ctxt_switches: 被动切换

常见陷阱与 FAQ

1. RT 线程饿死系统

# SCHED_FIFO/RR 线程如果不让出 CPU,所有普通进程饿死
# 内核保护:rt_runtime 限制
cat /proc/sys/kernel/sched_rt_runtime_us   # 默认 950000
cat /proc/sys/kernel/sched_rt_period_us    # 默认 1000000
# 含义:RT 线程最多占用 95% CPU,留 5% 给普通任务

2. nice 不是线性的

nice 差 1 ≈ CPU 份额差 10%。nice 0 和 nice 1 的差异远小于 nice 18 和 nice 19。

3. 过度使用 CPU 亲和性

绑核过细可能适得其反:
  - 某些核心过载,其他核心空闲
  - 失去调度器的自动负载均衡能力
只对确实需要的延迟敏感线程绑核

观测与调试

# 查看线程调度策略和优先级
chrt -p <pid>

# 查看 CPU 使用和调度统计
cat /proc/<pid>/schedstat
# 第1个数:在 CPU 上运行的时间 (ns)
# 第2个数:在运行队列等待的时间 (ns)
# 第3个数:时间片用完次数

# perf 看调度事件
perf sched record -- ./app
perf sched latency  # 调度延迟统计
perf sched map      # CPU 时间线

# bpftrace 查看运行队列延迟
bpftrace -e '
tracepoint:sched:sched_wakeup { @qtime[args->pid] = nsecs; }
tracepoint:sched:sched_switch /@qtime[args->next_pid]/ {
    @runq_lat_us = hist((nsecs - @qtime[args->next_pid]) / 1000);
    delete(@qtime[args->next_pid]);
}'

# mpstat 看每核 CPU 使用率
mpstat -P ALL 1

延伸阅读

  • man 7 sched — 调度概述
  • man 2 sched_setscheduler — 设置策略
  • man 2 sched_setaffinity — CPU 亲和性
  • man 7 cpuset — cgroup CPU 集合
  • /proc/sys/kernel/sched_* — CFS 调优参数