概述

进程调度器是操作系统内核的核心组件,它决定了哪个进程在哪个 CPU 上运行、运行多长时间。Linux 自 2.6.23 起采用 CFS(Completely Fair Scheduler)作为默认调度器,经过多年演进,在 6.6 内核中又引入了 EEVDF(Earliest Eligible Virtual Deadline First)替代 CFS。本文深入理解 CFS 的工作原理、nice/cgroup CPU 控制、实时调度、CPU 亲和性等核心主题,并分享生产环境的调优经验。

CFS 调度器原理

设计哲学

CFS 的核心目标是"完全公平"——每个进程按照其权重比例获得 CPU 时间。与传统调度器基于时间片不同,CFS 使用"虚拟运行时间"(vruntime)来跟踪每个进程的 CPU 消耗:

vruntime = 实际运行时间 × (NICE_0_LOAD / 进程权重)
  • nice 值为 0 的进程权重为 1024(NICE_0_LOAD
  • nice 值越低,权重越大,vruntime 增长越慢,获得更多 CPU 时间
  • CFS 始终选择 vruntime 最小的进程运行

红黑树与调度队列

CFS 使用红黑树维护运行队列,按 vruntime 排序:

              [vruntime=50]
             /              \
      [vruntime=20]      [vruntime=80]
       /         \
[vruntime=10]  [vruntime=35]
  • 最左节点(vruntime 最小)是下一个被调度的进程
  • 进程运行后 vruntime 增加,被重新插入红黑树
  • 查找、插入、删除均为 O(log N)

调度周期与最小粒度

# 调度周期(目标延迟):所有进程在这段时间内至少运行一次
$ sysctl kernel.sched_latency_ns
kernel.sched_latency_ns = 6000000  # 6ms(6.0×10⁶ ns)

# 最小运行粒度:单个进程一次最少运行的时间
$ sysctl kernel.sched_min_granularity_ns
kernel.sched_min_granularity_ns = 1000000  # 1ms

# 唤醒抢占粒度
$ sysctl kernel.sched_wakeup_granularity_ns
kernel.sched_wakeup_granularity_ns = 1000000  # 1ms

调度周期内的分配逻辑:

进程数 <= sched_latency_ns / sched_min_granularity_ns:
    每个进程的时间片 = sched_latency_ns / 进程数

进程数 > 阈值:
    每个进程的时间片 = sched_min_granularity_ns
    调度周期自动扩展 = sched_min_granularity_ns × 进程数

nice 值与权重

nice 值范围 -20 到 19,默认 0。每差 1 级,CPU 占比约差 10%:

nice 值权重相对 CPU 占比典型用途
-208876188x实时关键任务
-1095489.3x高优先级服务
010241x默认
101100.107x后台任务
19150.015x批处理任务
# 查看进程 nice 值
$ ps -o pid,ni,comm -p $PID

# 修改进程 nice 值
$ renice -n -5 -p $PID

# 以指定 nice 值启动
$ nice -n -5 /path/to/program

注意:nice 值是相对的。两个进程 nice 值分别为 0 和 1,CPU 占比约为 1.25:1,而非 2:1。权重计算公式为 1024 × 1.25^(-nice)

cgroup v2 CPU 控制

CPU 权重(cpu.weight)

# 创建 cgroup
$ mkdir /sys/fs/cgroup/app_a
$ mkdir /sys/fs/cgroup/app_b

# 设置 CPU 权重(默认 100,范围 1-10000)
$ echo 200 > /sys/fs/cgroup/app_a/cpu.weight
$ echo 100 > /sys/fs/cgroup/app_b/cpu.weight
# app_a : app_b = 2:1

# 将进程加入 cgroup
$ echo $PID_A > /sys/fs/cgroup/app_a/cgroup.procs
$ echo $PID_B > /sys/fs/cgroup/app_b/cgroup.procs

CPU 带宽限制(cpu.max)

# 格式: $MAX $PERIOD
# 限制为 1.5 个 CPU 核
$ echo "150000 100000" > /sys/fs/cgroup/app_a/cpu.max
# 含义: 每 100ms 周期内最多使用 150ms CPU 时间

# 查看当前配置
$ cat /sys/fs/cgroup/app_a/cpu.max
150000 100000

# 查看 CPU 使用统计
$ cat /sys/fs/cgroup/app_a/cpu.stat
usage_usec 12345678
user_usec 9876543
system_usec 2469135
nr_periods 12345
nr_throttled 0       # 被限流次数
throttled_usec 0     # 被限流的总时间

cpu.weight vs cpu.max 对比

特性cpu.weightcpu.max
类型相对权重绝对限制
CPU 空闲时可突破限制使用全部 CPU严格不超过限制
适用场景优先级控制硬性配额
类比nice 值CPU 配额
Docker 对应--cpu-shares--cpus

生产环境 CPU 限制策略

# 数据库服务:高优先级 + 硬限制防超卖
$ echo 500 > /sys/fs/cgroup/postgres/cpu.weight
$ echo "800000 100000" > /sys/fs/cgroup/postgres/cpu.max  # 8 核

# Web 服务:中等优先级
$ echo 200 > /sys/fs/cgroup/nginx/cpu.weight
$ echo "400000 100000" > /sys/fs/cgroup/nginx/cpu.max     # 4 核

# 日志收集:低优先级
$ echo 50 > /sys/fs/cgroup/filebeat/cpu.weight
$ echo "50000 100000" > /sys/fs/cgroup/filebeat/cpu.max   # 0.5 核

实时进程与 RT 调度

调度策略

策略说明适用场景
SCHED_NORMAL (0)CFS 调度普通进程
SCHED_BATCH (3)CFS 批处理模式CPU 密集型批处理
SCHED_IDLE (5)极低优先级后台任务
SCHED_FIFO (1)实时先进先出实时任务
SCHED_RR (2)实时轮转实时任务
SCHED_DEADLINE (6)基于 deadline严格实时需求

SCHED_FIFO 与 SCHED_RR

# 查看进程调度策略
$ chrt -p $PID

# 设置为 SCHED_FIFO,优先级 80
$ chrt -f -p 80 $PID

# 设置为 SCHED_RR,优先级 80
$ chrt -r -p 80 $PID

SCHED_FIFO 规则:

  • 同优先级进程按 FIFO 顺序运行
  • 高优先级进程始终抢占低优先级
  • 进程主动让出 CPU 后才切换(无时间片限制)

SCHED_RR 规则:

  • 同 SCHED_FIFO,但每个进程有时间片(默认 100ms)
  • 时间片耗尽后轮转到同优先级下一个进程

RT 调度参数调优

# RT 进程可用的 CPU 带宽比例(默认 95%)
$ sysctl kernel.sched_rt_runtime_us
kernel.sched_rt_runtime_us = 950000

$ sysctl kernel.sched_rt_period_us
kernel.sched_rt_period_us = 1000000
# 含义: 每 1 秒中 RT 进程最多用 950ms,留 50ms 给普通进程

# RT throttling 统计
$ grep -H . /proc/sys/kernel/sched_rt_*

警告:生产环境不要将 sched_rt_runtime_us 设为 -1(禁用 throttling),否则一个失控的 RT 进程可能锁死整个系统。

DEADLINE 调度器

struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime  = 10 * 1000 * 1000,  // 10ms 运行时间
    .sched_deadline = 30 * 1000 * 1000,  // 30ms deadline
    .sched_period   = 30 * 1000 * 1000,  // 30ms 周期
};
syscall(SYS_sched_setattr, pid, &attr, 0);

CPU 亲和性

亲和性原理

CPU 亲和性(affinity)控制进程可以在哪些 CPU 核心上运行。绑定进程到特定 CPU 可以:

  • 减少 cache miss(CPU cache 热度保持)
  • 降低 NUMA 跨节点访存延迟
  • 隔离关键服务
# 查看进程亲和性
$ taskset -p $PID
pid 12345's current affinity mask: ff  # CPU 0-7

# 设置亲和性(绑定到 CPU 0 和 1)
$ taskset -cp 0,1 $PID

# 以指定亲和性启动
$ taskset -c 0,1 /path/to/program

# 查看所有 CPU 的亲和性掩码
$ taskset -cp $PID

NUMA 亲和性

# numactl 绑定进程到 NUMA 节点
$ numactl --cpunodebind=0 --membind=0 /path/to/program

# interleave 模式(内存均匀分布到所有 NUMA 节点)
$ numactl --interleave=all /path/to/program

# 查看进程的 NUMA 内存分布
$ numastat -p $PID

isolcpus:隔离 CPU 核心

# GRUB 配置:隔离 CPU 4-7
# /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7"

# 更新 GRUB
$ grub2-mkconfig -o /boot/grub2/grub.cfg
$ reboot

# 隔离后,使用 taskset 手动绑定进程到隔离核
$ taskset -c 4-7 /path/to/critical_service
参数说明
isolcpus=4-7将 CPU 4-7 从调度器中移除,不参与负载均衡
nohz_full=4-7在这些 CPU 上禁用周期性 tick(减少中断)
rcu_nocbs=4-7RCU 回调迁移到其他 CPU

适用于低延迟交易系统、电信级 VoIP、实时控制系统等场景。

IRQ 亲和性

# 查看 IRQ 分配
$ cat /proc/interrupts | head -30

# 将网卡中断绑定到特定 CPU
# 假设网卡中断号为 32
$ echo 0f > /proc/irq/32/smp_affinity  # 绑定到 CPU 0-3

# 使用 hex 掩码
# CPU 0-3:  0x0f (00001111)
# CPU 4-7:  0xf0 (11110000)
# CPU 0-7:  0xff (11111111)
# 使用 irqbalance 服务(默认开启)
$ systemctl status irqbalance

# 生产环境建议:关闭 irqbalance,手动绑定
$ systemctl stop irqbalance
$ systemctl disable irqbalance

调度延迟排查

延迟来源分析

进程唤醒 → [调度延迟] → 进程开始运行 → [执行延迟] → 完成
    可能原因:
    1. CPU 被高优先级进程占用
    2. CPU 被实时进程占满
    3. cgroup CPU 限流
    4. NUMA 跨节点
    5. 中断风暴

排查工具

1. schedstat

# 查看进程调度统计
$ cat /proc/$PID/schedstat
12345678 9876543 5678
# 字段: 等待 CPU 时间(ns)  运行时间(ns)  时间片切换次数

# 计算平均调度延迟
$ awk '{printf "avg schedule delay: %.2f ms\n", $1/$3/1000000}' /proc/$PID/schedstat

2. perf sched

# 记录调度事件(10 秒)
$ perf sched record -- sleep 10

# 分析调度延迟
$ perf sched latency --sort max

# 输出示例:
#   Task                  |   Runtime ms  | Switches | Average delay ms | Maximum delay ms |
#   nginx:worker          |      120.345  |      234 |          0.456   |          5.678   |

3. eBPF 追踪

# 追踪进程唤醒延迟
$ /usr/share/bcc/tools/runqlat -p $PID 5

# 追踪运行队列长度
$ /usr/share/bcc/tools/runqlen 5

# 追踪 CPU off-CPU 时间(等待调度的时间)
$ /usr/share/bcc/tools/offcputime -p $PID 5

# 追踪调度器性能
$ /usr/share/bcc/tools/schedstats 5

4. histly / flamegraph

# 生成 off-CPU 火焰图
$ /usr/share/bcc/tools/offcputime -p $PID -f 10 > offcpu.svg

常见延迟问题与解决

问题现象解决方案
CPU 限流cgroup nr_throttled 持续增长提高 cpu.max 或优化代码
RT 进程抢占普通进程延迟尖峰限制 RT 带宽或改用 SCHED_DEADLINE
NUMA 跨节点周期性延迟使用 numactl 绑定
中断风暴特定 CPU 100% sys分散 IRQ 到多核
过载运行队列长度 > 4扩容或降低负载
上下文切换过多cs/sec > 50000减少线程数、使用 IO 多路复用

案例:API 服务 P99 延迟优化

现象:Go API 服务 P99 延迟从 50ms 升至 200ms,CPU 使用率 60%。

排查

# 1. 检查 CPU 限流
$ cat /sys/fs/cgroup/app/cpu.stat
nr_throttled 8765     # 被限流 8765 次
throttled_usec 4567890  # 累计被限流 4.5 秒

# 2. 检查 cpu.max 配置
$ cat /sys/fs/cgroup/app/cpu.max
200000 100000  # 限制 2 核

# 3. 使用 runqlat 确认
$ /usr/share/bcc/tools/runqlat -p $PID 10
# 显示大量 10ms+ 的调度延迟

根因:cgroup CPU 限制为 2 核,但服务有突发 CPU 需求,频繁触发限流。

解决方案

# 方案 1:提高限制(如果资源允许)
$ echo "400000 100000" > /sys/fs/cgroup/app/cpu.max

# 方案 2:使用 cpu.weight 替代硬限制(集群环境)
$ echo 500 > /sys/fs/cgroup/app/cpu.weight
$ echo "max" > /sys/fs/cgroup/app/cpu.max  # 不限制

# 方案 3:优化 GOMAXPROCS(Go 1.19+ 会自动感知 cgroup)
$ GOMAXPROCS=2 ./app  # 显式设置匹配 cpu.max

EEVDF 调度器(内核 6.6+)

Linux 6.6 引入 EEVDF(Earliest Eligible Virtual Deadline First)替代 CFS:

EEVDF 核心改进

特性CFSEEVDF
调度依据vruntime 最小eligibility + virtual deadline
延迟保证无精确保证基于 deadline 的精确延迟
nice 值影响 vruntime 增速影响 weight 和 lag
公平性渐进公平精确公平(lag-based)
实时性无保证有 deadline 保障

关键概念

  • Eligibility:进程是否"有资格"被调度(基于 lag 计算)
  • Virtual Deadline:每个进程有一个虚拟截止时间,优先调度最早截止的进程
  • Lag:实际获得 CPU 时间与应得 CPU 时间的差值,用于精确补偿
# 查看内核版本
$ uname -r
6.6.0-1234-generic

# EEVDF 相关 sysctl(6.6+)
$ sysctl kernel.sched_base_slice
kernel.sched_base_slice = 750000  # 基础时间片 0.75ms(取代 sched_min_granularity_ns)

EEVDF 对延迟敏感型应用有显著改善,特别是音视频处理、游戏服务器等场景。

多核负载均衡

负载均衡层级

                        [ sched domain hierarchy ]
                              ┌─────────┐
                              |  NUMA   |  (跨节点)
                              | domain  |
                              └────┬────┘
                          ┌────────┼────────┐
                     ┌────┴────┐   ...  ┌────┴────┐
                     |  CPU    |        |  CPU    |  (物理核)
                     | socket  |        | socket  |
                     └────┬────┘        └─────────┘
                   ┌──────┼──────┐
              ┌────┴───┐    ...    ┌────┴───┐
              |  LLC   |           |  LLC   |  (共享 L3 cache)
              | domain |           | domain |
              └────┬───┘           └────────┘
              ┌────┼────┐
         ┌────┴──┐  ... ┌──┴────┐
         |  SMT  |      |  SMT  |  (超线程)
         | domain|      | domain|
         └───────┘      └───────┘

负载均衡参数

# 负载均衡间隔(忙/闲)
$ sysctl kernel.sched_migration_cost_ns
kernel.sched_migration_cost_ns = 500000  # 0.5ms
# 任务在迁移前必须 idle 的最短时间

# 启用/禁用 SMT 调度
$ sysctl kernel.sched_smt_sibling_cost

手动控制负载均衡

# 为特定 CPU 集创建独立的调度域(CPU 隔离)
# 见 isolcpus 部分

# 使用 cgroup v2 的 cpuset
$ mkdir /sys/fs/cgroup/dedicated
$ echo "4-7" > /sys/fs/cgroup/dedicated/cpuset.cpus
$ echo "0" > /sys/fs/cgroup/dedicated/cpuset.mems

总结

Linux 进程调度器是系统性能的关键决定因素,理解其工作原理有助于在以下场景做出正确决策:

  1. CFS/EEVDF 的公平性基于权重而非时间片:nice 值每差 1 级约 10% CPU 差异,cgroup cpu.weight 遵循相同机制。
  2. cgroup v2 的 cpu.max 是硬限制:适合容器配额管理,但要注意 throttling 对延迟的影响;cpu.weight 是相对优先级,CPU 空闲时可突破。
  3. 实时调度需要谨慎使用:SCHED_FIFO/RR 会抢占普通进程,务必配置 sched_rt_runtime_us 留出安全余量。
  4. CPU 亲和性是延迟优化的利器isolcpus + nohz_full + IRQ 绑定可实现微秒级延迟保证。
  5. 调度延迟排查需工具链配合:从 schedstat/runqlat 定位延迟分布,到 offcputime 追踪根因,到 perf sched 分析全局调度行为。
  6. EEVDF 是调度器的未来:6.6+ 内核已默认启用,对延迟敏感型应用有显著改善。

调度器调优的黄金法则:默认配置已经足够好。只有在有明确的延迟或吞吐量问题、且有基线数据支撑时,才修改调度参数。