调度¶
是什么 / 解决什么问题¶
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 |
SCHED_FIFO — 实时先入先出¶
struct sched_param param = { .sched_priority = 50 }; // 1-99
sched_setscheduler(0, SCHED_FIFO, ¶m);
// 行为:
// - 高优先级 FIFO 线程始终抢占低优先级
// - 同优先级之间 FIFO(不主动让出就一直运行)
// - 没有时间片概念
SCHED_RR — 实时轮转¶
struct sched_param param = { .sched_priority = 50 };
sched_setscheduler(0, SCHED_RR, ¶m);
// 行为:同 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 时间
优先级层次¶
使用模式¶
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:迁移线程或内存页以减少远端访问
性能考量¶
延迟敏感应用的调优¶
# 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
上下文切换开销¶
# 查看进程的上下文切换次数
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 调优参数