引言:为什么进程调度器是系统性能的核心
Linux 进程调度器是整个操作系统最核心的组件之一——它决定了哪个进程在何时获得 CPU 时间片,直接影响系统的吞吐量、响应延迟和资源利用率。无论是高并发 Web 服务的 P99 延迟、容器化环境的 CPU 资源隔离、还是实时系统的确定性响应,最终都要落实到调度器的行为上。
本文将从调度器的完整内核实现路径出发,系统覆盖 CFS(完全公平调度器)的红黑树与 vruntime 机制、实时调度策略(SCHED_FIFO/SCHED_RR/SCHED_DEADLINE)、cgroup v2 的 CPU 带宽控制、CPU 亲和性与 NUMA 感知、内核抢占配置,以及生产级低延迟与高密度部署的调优实践,帮助读者建立从内核源码到生产调优的全栈工程能力。
第一章:Linux 调度器架构全景
1.1 调度器演进历程
Linux 调度器经历了多个重大版本迭代:
- O(n) 调度器(Linux 2.4 之前):遍历所有可运行进程选择最优,时间复杂度 O(n),无法支撑大规模并发
- O(1) 调度器(Linux 2.6):引入 active/expired 双数组 + 优先级位图,schedule() 执行时间与进程数无关,但交互性评分算法过于复杂
- CFS(Completely Fair Scheduler,Linux 2.6.23+):基于红黑树的虚拟运行时间(vruntime)排序,实现真正的"完全公平",成为通用调度策略的默认选择
- EAS(Energy Aware Scheduling,Linux 5.0+):面向 ARM big.LITTLE 架构的能量感知调度,在能耗与性能间平衡
- PREEMPT_RT(实时抢占补丁):将中断处理线程化、自旋锁转为可抢占互斥锁,使 Linux 达到硬实时级别延迟
1.2 调度器核心数据结构
调度器的核心数据结构关系如下:
struct task_struct {
int prio; // 静态优先级 (0-139)
int normal_prio; // 基于 static_prio 计算
unsigned int rt_priority; // 实时优先级 (1-99)
const struct sched_class *sched_class; // 调度类
struct sched_entity se; // CFS 调度实体
struct sched_rt_entity rt; // RT 调度实体
struct sched_dl_entity dl; // DEADLINE 调度实体
const char *comm; // 进程名
struct list_head tasks; // 全局进程链表
struct mm_struct *mm; // 内存描述符
...
};
struct sched_entity {
struct load_weight load; // 权重
struct rb_node run_node; // 红黑树节点
unsigned int on_rq; // 是否在运行队列
u64 vruntime; // 虚拟运行时间
u64 exec_start; // 开始执行时间
u64 sum_exec_runtime; // 累计执行时间
u64 prev_sum_exec_runtime; // 上次累计值
struct cfs_rq *cfs_rq; // 所属 CFS 运行队列
...
};
struct rq {
raw_spinlock_t lock; // 运行队列锁
unsigned int nr_running; // 可运行进程数
struct cfs_rq cfs; // CFS 运行队列
struct rt_rq rt; // 实时运行队列
struct dl_rq dl; // DEADLINE 运行队列
struct task_struct *curr; // 当前运行进程
struct task_struct *idle; // idle 进程
u64 clock; // 运行队列时钟
int cpu; // CPU ID
...
};
struct cfs_rq {
struct load_weight load; // 总权重
unsigned long runnable_weight;
unsigned int nr_running; // CFS 红黑树中进程数
u64 min_vruntime; // 最小 vruntime
struct rb_root_cached tasks_timeline; // 红黑树根
struct sched_entity *curr; // 当前调度实体
...
};
1.3 调度类链(Scheduling Class Chain)
Linux 采用调度类链表实现多策略优先级排列:
// 内核: kernel/sched/core.c
const struct sched_class stop_sched_class; // 优先级最高(停机任务)
const struct sched_class dl_sched_class; // SCHED_DEADLINE(最早截止时间优先)
const struct sched_class rt_sched_class; // SCHED_FIFO / SCHED_RR(实时调度)
const struct sched_class fair_sched_class; // SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE
const struct sched_class idle_sched_class; // SCHED_IDLE(空闲任务)
当需要选择下一个进程时,pick_next_task() 函数按优先级从高到低遍历调度类:先检查 DEADLINE 队列,再检查 RT 队列,然后是 CFS 红黑树,最后是 idle 任务。这意味着 DEADLINE 任务的优先级永远高于 RT,RT 任务永远高于普通进程。
第二章:CFS 完全公平调度器深入实现
2.1 核心概念:vruntime(虚拟运行时间)
CFS 的核心思想是让每个进程的虚拟运行时间(vruntime)增长速率与其权重成反比。权重越大的进程,vruntime 增长越慢,因此被调度的机会越多。
vruntime 的计算公式:
// 内核: kernel/sched/fair.c: update_curr()
delta_exec = now - curr->exec_start; // 实际执行时间
delta_exec_weighted = calc_delta_fair(delta_exec, curr); // 根据权重换算
curr->vruntime += delta_exec_weighted; // 累加虚拟时间
calc_delta_fair() 的实现:
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se) {
// NICE_0_LOAD = 1024,对应 nice=0 的权重
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
nice 值每降低 1(优先级提高),权重增加约 1.25 倍。具体映射关系由 sched_prio_to_weight[] 数组定义:
| nice | 权重 | 相对 CPU 份额(nice=0 为基准) |
|---|---|---|
| -20 | 88761 | ~10.0x |
| -10 | 1561 | ~2.9x |
| -5 | 312 | ~1.7x |
| 0 | 1024 | 1.0x |
| 5 | 330 | ~0.57x |
| 10 | 110 | ~0.32x |
| 15 | 36 | ~0.17x |
| 19 | 15 | ~0.09x |
2.2 红黑树与进程选择
CFS 使用红黑树(Red-Black Tree)按 vruntime 组织所有可运行进程:
- 最左节点 = vruntime 最小 = 最该被调度
- 选择操作 O(1):通过
rb_leftmost缓存直接获取 - 插入操作 O(log n):标准红黑树插入
// 内核: kernel/sched/fair.c: __enqueue_entity()
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) {
struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
bool leftmost = true;
// 按 vruntime 查找插入位置
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = false;
}
}
rb_link_node(&se->run_node, parent, link);
rb_insert_color_cached(&se->run_node, &cfs_rq->tasks_timeline, leftmost);
// 如果是新的最左节点,更新 leftmost 缓存
}
// 选择下一个运行进程 O(1)
static struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq) {
struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
if (!left) return NULL;
return rb_entry(left, struct sched_entity, run_node);
}
2.3 调度周期与目标延迟
CFS 使用两个关键参数控制时间分配:
- sched_latency:目标调度延迟,默认 6ms(所有可运行进程至少运行一次的时间窗口)
- sched_min_granularity:最小时间片,默认 0.75ms,防止过多进程时的切换开销
- sched_wakeup_granularity:唤醒抢占粒度,默认 1ms,控制新唤醒进程的抢占时机
每个进程分配的时间片计算公式:
time_slice = sched_latency / nr_running // 如果结果 >= min_granularity
time_slice = min_granularity // 否则取最小粒度
当可运行进程数超过 sched_latency / sched_min_granularity = 8 个时,每个进程的时间片不再缩小,但调度周期会拉长(min_granularity * nr_running)。
2.4 组调度(Group Scheduling)
组调度使得调度决策在用户组间也保持公平:
// 两级调度流程:用户组间 CFS → 组内 CFS → 进程选择
用户 A (shares=1024)
├── 进程 A1 (shares=1024) → vruntime 在组内红黑树排序
└── 进程 A2 (shares=512)
用户 B (shares=1024)
└── 进程 B1 (shares=1024)
当用户 A 有两个活跃进程而用户 B 只有一个时,用户 A 虽然有组级别的公平份额 1024,但每个进程实际获得 512/1024 的份额。这就是"组内公平、组间也公平"的双重平衡机制。
2.5 负载均衡(Load Balancing)
多核环境下,每个 CPU 有独立的运行队列。负载均衡在以下时机触发:
- tick 时钟中断时:检查是否需要进行周期性负载均衡(load_balance())
- 空闲 CPU 时:空闲 CPU 会尝试从繁忙 CPU 窃取任务(idle_balance())
- exec/fork 时:选择最空闲的 CPU 放置新进程
- 唤醒新进程时:select_task_rq_fair() 选择最佳 CPU
负载均衡的核心度量是 "负载"(load),不是简单的可运行进程数,而是包含历史衰减权重的加权平均值:
// 内核负载的指数移动平均
load_avg = load_avg * decay + instant_load * (1 - decay)
// 每 32ms 衰减一次,半衰期约 32ms
第三章:实时调度策略
3.1 SCHED_FIFO:先进先出实时策略
SCHED_FIFO 是严格优先级的非抢占式策略——高优先级进程始终运行直到主动让出 CPU:
特点:
- 优先级范围 1-99(99 最高)
- 同优先级进程按 FIFO 顺序运行
- 除非主动 sched_yield()、阻塞或被更高优先级抢占,否则永远运行
- 可能导致低优先级任务 "饿死"(starvation)
// 设置 SCHED_FIFO 的策略示例
#include
struct sched_param param;
param.sched_priority = 50;
sched_setscheduler(pid, SCHED_FIFO, ¶m);
// 限制实时任务最大 CPU 使用时间(防止系统挂起)
// /proc/sys/kernel/sched_rt_period_us = 1000000 (1 秒周期)
// /proc/sys/kernel/sched_rt_runtime_us = 950000 (实时任务最多占 95%)
3.2 SCHED_RR:轮转实时策略
SCHED_RR 与 SCHED_FIFO 类似,但同优先级进程之间有时间片轮转:
- 每个 RR 进程的默认时间片为 100ms(
sched_rr_timeslice = RR_TIMESLICE) - 时间片耗尽后,进程被移到同优先级队列末尾
- 适合需要公平性的实时任务
3.3 SCHED_DEADLINE:最早截止时间优先(EDF)
SCHED_DEADLINE 是基于 EDF 算法的周期性实时任务调度策略,是 Linux 实时调度中最严格和最强大的策略:
每个任务由三个参数定义:
- runtime(Q):每个周期内需要的最坏执行时间
- period(T):任务的周期
- deadline(D):截止时间(通常等于 period)
[[Q 运行 | 空闲 ] [Q 运行 | 空闲 ] [Q 运行 | 空闲 ]]
├──────── period ────────┤├──────── period ────────┤
可调度性检验(Schedulability Test):
// 必要条件:Σ(Q_i / T_i) <= 1 (对于隐式截止时间 D_i = T_i)
// 内核中的准入检查: dl_task_can_fit()
u64 util = 0;
for_each_dl_task(t) {
util += t->runtime / t->period;
}
return util <= 1;
Deadline 任务使用独立的 dl_rq(按运行时间排序的红黑树),永远优先于 FIFO/RR/CFS 任务执行。
第四章:CPU 亲和性与 NUMA 感知
4.1 CPU 亲和性(CPU Affinity)
CPU 亲和性使进程/线程绑定到特定 CPU 核心,减少 CPU Cache 失效:
// 使用 taskset 命令绑定进程
taskset -c 0-3 ./my_app // 绑定到 CPU 0-3
taskset -cp 4,5 // 设置已运行进程的亲和性
// 使用 sched_setaffinity() 系统调用
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
CPU_SET(1, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);
// 使用 cgroup cpuset.cpus(容器场景)
echo "0-3" > /sys/fs/cgroup/mygroup/cpuset.cpus
echo "0" > /sys/fs/cgroup/mygroup/cpuset.mems // NUMA node 0
CPU 亲和性的底层原理:
struct task_struct {
...
cpumask_t cpus_allowed; // 允许的 CPU 位图
...
};
// 选择 CPU 时的过滤逻辑
for_each_cpu(cpu, &p->cpus_allowed) {
if (!cpu_active(cpu)) continue;
if (available) return cpu; // 选择可用的绑定 CPU
}
4.2 NUMA 架构与调度优化
NUMA(Non-Uniform Memory Access)架构下,进程访问本地节点的内存远快于远程节点。调度器通过多层级调度域(sched_domain)实现 NUMA 感知:
NUMA 层级调度域结构:
┌─────────────────────────────────────┐
│ DIE 域(Socket 级,共享 L3 Cache) │
│ ┌─────────┐ ┌─────────┐ │
│ │ NUMA MC │ │ NUMA MC │ ← 多核域 │
│ │ CPU 0-5 │ │ CPU 6-11│ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────────┘
// 查看调度域层级
cat /proc/sys/kernel/sched_domain/cpu0/domain*/name
# OUTPUT: DIE → NUMA
// 查看 NUMA 节点拓扑
numactl --hardware
# available: 2 nodes (0, 1)
# node 0 cpus: 0 1 2 3 4 5 6 7
# node 1 cpus: 8 9 10 11 12 13 14 15
# node 0 size: 65448 MB
# node 1 size: 65536 MB
# node distances:
# node 0 1
# 0: 10 21 ← 本地 10,远程 21(2.1x 延迟)
# 1: 21 10
NUMA 感知唤醒(NUMA Aware Wakeup):
Linux 4.0+ 引入了 numa_fork 和 numa_wake 优化——唤醒进程时不仅考虑 CPU 负载,还考虑内存所在 NUMA 节点:
// 内核: kernel/sched/fair.c: select_idle_sibling()
static int select_idle_sibling(struct task_struct *p, int prev, int target) {
// 1. 首先尝试 prev CPU(缓存亲和)
if (available_idle_cpu(prev))
return prev;
// 2. 尝试同 NUMA 节点内 waker CPU
if (cpumask_test_cpu(target, p->cpus_allowed) &&
available_idle_cpu(target))
return target;
// 3. 在同 NUMA 域内寻找空闲 CPU
sd = rcu_dereferencesched_domain(target);
for_each_domain(target, sd) {
// 在该调度域内寻找空闲 CPU
}
}
第五章:cgroup v2 资源控制
5.1 cgroup v2 CPU 控制
cgroup v2 提供了精细的 CPU 资源隔离机制:
// 创建 cgroup
mkdir /sys/fs/cgroup/myapp
// 设置 CPU 带宽(基于 CFS Bandwidth Control)
echo "200000 1000000" > /sys/fs/cgroup/myapp/cpu.max
// 表示每 1s 周期内最多使用 200ms CPU → 等效 2 个核心的 20%
// 超出后进程会被节流(throttled),直到下一个周期
// 设置 CPU 权重(相对于其他 cgroup)
echo "100" > /sys/fs/cgroup/myapp/cpu.weight
// 权重区间 1-10000,默认 100
// cgroup A weight=100 vs cgroup B weight=200 → B 获得 2x CPU 份额
// 查看 cpu.stat 了解节流情况
cat /sys/fs/cgroup/myapp/cpu.stat
// nr_periods 1234 (周期数)
// nr_throttled 56 (被节流次数)
// throttled_time 123456789(纳秒)
// usage_usec 987654321
5.2 cgroup v2 cpuset 控制
cpuset 子集提供物理 CPU 和内存节点绑定:
// 限制 cgroup 只能使用 CPU 4-7 和 NUMA node 1
echo "4-7" > /sys/fs/cgroup/myapp/cpuset.cpus
echo "1" > /sys/fs/cgroup/myapp/cpuset.mems
echo "1" > /sys/fs/cgroup/myapp/cpuset.cpus.partition // 独占分区模式
// 查看当前绑定情况
cat /sys/fs/cgroup/myapp/cpuset.cpus.effective
5.3 cgroup v2 与 Kubernetes 映射
Kubernetes 的 cpu request/limits 直接映射到 cgroup 参数:
// Linux 内核 CFS 带宽触发逻辑
// cpu request 1000m → cpu.weight = 3908(近似值,按比例换算)
// cpu limit 2000m → cpu.max = "200000 1000000"(200ms/1s 周期)
// 查看 Pod 实际的 cgroup 配置
cat /sys/fs/cgroup/kubepods/pod/cpu.weight
cat /sys/fs/cgroup/kubepods/pod/cpu.max
第六章:内核抢占模型与实时性
6.1 内核抢占模式
Linux 支持多种内核抢占配置,在系统启动时选择:
| 模式 | 配置 | 说明 |
|---|---|---|
| No Forced Preempt | PREEMPT_NONE | 服务器默认,仅在明确抢占点让出,最高吞吐 |
| Voluntary Preempt | PREEMPT_VOLUNTARY | 在主要抢占点自动让出,平衡吞吐与延迟 |
| Preemptible Kernel | PREEMPT | 桌面/通用,内核几乎处处可抢占,较好响应 |
| Full RT Preempt | PREEMPT_RT | 实时扩展,中断线程化,自旋锁→互斥锁 |
抢占相关的内核参数:
// 检查当前抢占模式
uname -a # 查看是否有 PREEMPT 字样
zgrep CONFIG_PREEMPT /proc/config.gz
// 抢占计数器(task_struct->preempt_count)
#define PREEMPT_BITS 8 // 禁用抢占深度(preempt_disable 嵌套)
#define SOFTIRQ_BITS 8 // 是否在 softirq 上下文
#define HARDIRQ_BITS 4 // 是否在 hardirq 上下文
#define NMI_BITS 1 // 是否在 NMI 上下文
6.2 上下文切换优化
上下文切换涉及的关键步骤:
// 内核切换流程: __switch_to()
1. 保存当前进程寄存器状态(callee-saved + 栈指针)
2. 切换 page table (CR3 寄存器)→ 触发 TLB flush
3. 恢复目标进程寄存器状态
4. 切换内核栈指针(rsp)
5. 返回目标进程的执行点
减少 TLB flush 的优化手段:
- KPTI(PTI)隔离:虽然带来 TLB flush 开销,但防止 Meltdown 漏洞
- Lazy TLB mode:进程切换时不立即 flush,推迟到首次用户态访问
- ASID(Address Space ID):ARM/x86 PCID 支持,无需每次 flush 整个 TLB
- 大页(Huge Page):2MB/1GB 页减少 TLB miss 率
第七章:生产级调优实战
7.1 低延迟场景调优
金融交易、音视频处理等对延迟敏感的系统:
# 1. 禁用 C-states 深度休眠
echo 0 > /sys/devices/system/cpu/cpu*/cpuidle/state{C3,C6,C7}/disable
# 2. 限制 C1E(增强 Halt 状态)
# 在 BIOS 中禁用 C-states 或添加内核参数: processor.max_cstate=1 idle=poll
# 3. CPU 频率锁定为 performance 模式
echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# 4. 隔离 CPU 核心(启动参数)
# GRUB: isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7
# 让 CPU 4-7 不参与常规调度,进程需手动绑定
# 5. 中断亲和性绑定——网卡中断到非隔离核心
echo 3 > /proc/irq/IRQ_NUMBER/smp_affinity # CPU 0-1
# 业务进程绑到 CPU 4-7,完全不处理中断
# 6. 禁用透明大页(某些 THP 策略会引入不确定的延迟)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 7. 使用 PREEMPT_RT 内核
# 实时内核将中断处理、自旋锁等全部线程化,典型延迟 < 50>
7.2 高密度容器场景调优
云原生环境下最大化 CPU 利用率:
# 1. 调整调度周期(默认 6ms 偏保守,适当缩短提升响应)
sysctl -w kernel.sched_latency_ns=4000000 # 4ms
sysctl -w kernel.sched_min_granularity_ns=500000 # 0.5ms
sysctl -w kernel.sched_wakeup_granularity_ns=800000 # 0.8ms
# 2. NUMA 感知调度
sysctl -w kernel.numa_balancing=1 # 自动 NUMA 任务迁移
# 3. 负载均衡优化
sysctl -w kernel.sched_migration_cost_ns=500000 # 500us 内不迁移
sysctl -w kernel.sched_nr_migrate=32 # 每次最多迁移 32 个任务
# 4. cgroup v2 CPU burst 允许临时超额
echo "150000 1000000" > cpu.max # 允许突发到 150%,但总量受限
# 5. CPU 超线程感知调度(SMT 级调度域)
# 同物理核心的逻辑核共享 L1/L2 Cache
# 如果任务 Cache-sensitive,尽量跨物理核心调度避免 HT 争抢
7.3 批处理与离线任务优化
大数据分析、CI/CD 构建等可以容忍较高延迟但追求吞吐的场景:
# 1. 使用 SCHED_BATCH 策略减少唤醒抢占
schedtool -B -e ./batch_job # 启动时设置
chrt -b 0 # 动态修改策略
# 2. SCHED_IDLE 策略——万不得已才运行
chrt -i 0 # 优先级最低,只有系统空闲时才获得 CPU
# 3. 利用 CPU bandwidth 控制离线任务的带宽上限
echo "100000 1000000" > /sys/fs/cgroup/offline/cpu.max # 限制到 10%
# 4. 禁用自动均衡,手动绑定到 NUMA node
taskset -c 0-15 -p # 绑定到指定 NUMA 节点
# 5. 大页支持减少 TLB miss(TLB 是批处理的重要瓶颈)
echo 1024 > /proc/sys/vm/nr_hugepages
# 或挂载 hugetlbfs
mount -t hugetlbfs none /dev/hugepages
第八章:调度器监控与可观测性
8.1 内核调度统计
/proc/sched_debug # 每个 CPU 的运行队列详细状态
/proc/schedstat # 调度器统计(负载均衡次数、上下文切换等)
/proc//sched # 单进程调度统计
// 典型 sched_debug 输出
cfs_rq[0]:/
.exec_clock : 12345.678
.MIN_vruntime : 0.000
.min_vruntime : 9876543.210
.nr_running : 5
.load : 5120
.runnable_load : 5120
.util : 4800
// /proc//sched 关键字段
nr_switches # 总上下文切换次数
nr_voluntary_switches # 主动切换(等待 IO 等)
nr_involuntary_switches # 被动切换(时间片耗尽)
se.vruntime # 当前 vruntime
se.sum_exec_runtime # 累计执行时间
policy # 调度策略
prio / normal_prio # 优先级
8.2 perf sched 分析工具
// 录制调度事件(持续 10 秒)
perf sched record -a sleep 10
// 查看调度延迟分布
perf sched latency
# 输出: 每个任务的平均/最大# 生成调度时间线(适合分析调度抖动)
perf sched map
// 查看调度器统计汇总
perf sched stat
# 输出:
# Task | Runtime ms | Switches | Average delay ms | Max delay ms
# ---------------------------------------------------------------------------------------
# myapp (1234) | 987.654 | 5234 | 0.12 | 45.67
# nginx (5678) | 456.789 | 3456 | 0.09 | 12.34
8.3 eBPF/Bpftrace 调度器追踪
// 追踪所有上下文切换事件
sudo perf trace -e sched:sched_switch
// bpftrace 脚本:统计每个进程的上下文切换次数
sudo bpftrace -e 'tracepoint:sched:sched_switch {
@comm[t->comm] = count();
}'
// 追踪调度延迟(任务就绪到实际运行的时间)
sudo bpftrace -e '
tracepoint:sched:sched_wakeup,
tracepoint:sched:sched_wakeup_new {
@qtime[args->pid] = nsecs;
@state[args->pid] = args->prio;
}
tracepoint:sched:sched_switch /@qtime[args->next_pid]/ {
$delay = nsecs - @qtime[args->next_pid];
@delay_us = hist($delay / 1000); // 微秒级延迟统计
delete(@qtime[args->next_pid]);
}
'
// 追踪实时调度抢占延迟(RT latency histogram)
sudo bpftrace -e '
kprobe:__schedule {
if (prev != 0 && prev->policy != SCHED_NORMAL) {
$now = nsecs;
$delta = $now - prev->last_queued_ts;
@rt_latency_us = hist($delta / 1000);
}
}'
8.4 ftrace 调度器深度追踪
// 设置 ftrace 追踪器
cd /sys/kernel/debug/tracing
echo nop > current_tracer
echo 1 > events/sched/enable # 启用所有调度事件追踪
echo 1 > tracing_on
# 查看特定 CPU 的运行队列状态(sched_update_nr_running)
cat trace_pipe | grep "sched_switch"
# 主要追踪事件:
# sched_wakeup / sched_wakeup_new — 新任务被唤醒
# sched_switch — 进程上下文切换
# sched_migrate_task — 负载均衡迁移
# sched_process_exit — 进程退出
# sched_process_wait — 进程进入等待
# sched_wait_task — 任务等待 CPU
# sched_stat_runtime — 任务运行时间统计
第九章:典型生产场景案例分析
案例 1:高并发 Nginx 调度优化
场景:8 核物理机 + 16 逻辑核(HT),64 并发 Nginx worker,混合静态资源和反向代理。
// 问题:RSS 波动时部分 worker 出现长尾请求(P99 延迟飙升)
// 根因分析:Nginx worker 在 CPU 间频繁迁移,L1/L2 Cache 失效
// 解决方案:
// 1. 将 Nginx worker 绑定到物理核心
worker_cpu_affinity 0001 0010 0100 1000 0001 0010 0100 1000;
// 2. 网卡中断绑定到独立核心
echo 0f > /proc/irq//smp_affinity # CPU 0-3 处理中断
worker_cpu_affinity f0 f0 f0 f0 f0 f0 f0 f0; # CPU 4-7 运行 worker
// 3. 对比优化前后
// 优化前:P99 延迟 15ms,Cache-miss 23%
// 优化后:P99 延迟 3ms, Cache-miss 5%
案例 2:K8s HPA 触发延迟优化
场景:K8s 集群 Deployment 配置 CPU request=500m limit=1000m,HPA 阈值 70%。
问题:Rapid traffic spikes 时扩容触发延迟 30s+,导致上游超时。
// 根因:cgroup CPU 限流导致 probe / 启动脚本被 throttle
// container cpu.weight = 3908(request=500m),启动时 burst 能力受限
// 优化方案:
// 1. 适当提高 request 权重,确保启动 burst
resources:
requests:
cpu: "500m"
limits:
cpu: ""
// 不设置 limit 则可利用 node 上空闲 CPU burst
// 2. 容器启动阶段使用 CRIO cpu burst
# 在 kubelet 配置中启用: --cpu-manager-policy=static
# 为 Guaranteed Pod 分配独占 CPU(cpuset 绑定)
// 3. 在容器内部 pin 关键启动进程
taskset -c 0 /start-app.sh # 绑定到独占 CPU 加速启动
案例 3:实时音视频确定性延迟
场景:XMPP 媒体网关,音频 RTP 处理线程需要 < 1ms>
// 使用 SCHED_DEADLINE 策略
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 800000, // 800μs 最坏执行时间
.sched_deadline = 1000000, // 1ms 截止时间
.sched_period = 1000000, // 1ms 周期
};
sched_setattr(pid, &attr, 0);
// 系统侧隔离 CPU
// 启动参数 isolcpus=3 nohz_full=3 rcu_nocbs=3
// 中断亲和性:echo 7 > /proc/irq/*/smp_affinity(绑定 0-2)
// CPU governor: performance
// 配合 PREEMPT_RT 内核,实测结果:
// 非实时内核:P99 调度延迟 ~800μs,抖动 ±500μs
// PREEMPT_RT:P99 延迟 < 50>
第十章:内核源码关键路径速查
// 调度器入口
kernel/sched/core.c
├── schedule() // 主调度函数(中断返回/系统调用返回时调用)
├── __schedule() // 真正的调度逻辑
│ ├── pick_next_task() // 按调度类优先级选择下一个任务
│ ├── context_switch() // 执行上下文切换
│ │ ├── switch_mm() // 切换地址空间
│ │ └── __switch_to() // 切换寄存器状态
│ └── update_rq_clock() // 更新运行队列时钟
└── scheduler_tick() // tick 中断触发的调度更新
// CFS 调度器
kernel/sched/fair.c
├── enqueue_task_fair() // 入队到 CFS 红黑树
├── dequeue_task_fair() // 从红黑树出队
├── pick_next_task_fair() // 从红黑树最左节点选任务
├── update_curr() // 更新 vruntime(每个 tick)
├── task_tick_fair() // tick 回调(检查时间片 + 抢占)
├── check_preempt_wakeup() // 唤醒时的抢占检查
├── select_task_rq_fair() // 多核负载选择
└── can_migrate_task() // 负载均衡迁移判断
// 实时调度
kernel/sched/rt.c
├── enqueue_task_rt() // RT 入队
├── dequeue_task_rt()
├── pick_next_task_rt() // 按优先级位图选择 RT 任务
└── task_tick_rt()
// DEADLINE 调度
kernel/sched/dl.c
├── enqueue_task_dl()
├── pick_next_task_dl()
// 负载均衡
kernel/sched/fair.c
├── load_balance() // 核心负载均衡函数
├── detach_tasks() // 从忙 CPU 拆下任务
├── attach_tasks() // 附加到空闲 CPU
└── can_migrate_task() // 可迁移性检查(Cache hot/affinity)
// 上下文切换
arch/x86/kernel/process_64.c
└── __switch_to() // x86 专用汇编切换路径
总结
Linux 进程调度器是一个精心设计的多层系统,理解它的内核行为对系统性能至关重要:
- CFS 通过红黑树和 vruntime 实现 "完全公平",是日常工作的基石
- 实时策略(FIFO/RR/DEADLINE)为确定性延迟任务提供严格保障
- CPU 亲和性与 NUMA 感知 减少了 Cache 失效和跨节点访存开销
- cgroup v2 提供了云原生环境下精细的资源隔离与带宽控制
- PREEMPT_RT 和 隔离 CPU 是低延迟场景的终极武器
掌握这些机制后,无论是构建百万级并发的 Web 服务、设计低延迟的交易系统,还是优化 Kubernetes 集群的 CPU 利用率,都能做到有的放矢,从内核源码级别理解并解决性能问题。调度器不是一成不变的——从 CFS 到 EAS,从 cgroup v1 到 v2,从标准内核到 PREEMPT_RT,持续演进的设计始终在吞吐、延迟和能效之间寻找最佳平衡。

发表评论 取消回复