Linux CFS 调度器深度解析:从算法原理到性能调优实战
目录
第一章:调度器概述与演进
1.1 调度器的核心目标与衡量指标
操作系统调度器的本质是在有限的 CPU 资源上,以合理的方式分配给多个竞争进程。一个优秀的调度器需要同时兼顾多个目标,而这些目标之间往往存在矛盾:
| 指标 | 含义 | 优化目标 |
|---|---|---|
| 吞吐量 (Throughput) | 单位时间内完成的工作量 | 最大化 |
| 响应时间 (Latency/Response Time) | 从触发到首次获得 CPU 的时间 | 最小化 |
| 公平性 (Fairness) | 每个任务获得 CPU 份额的比例与权重成正比 | 精确保障 |
| CPU 利用率 | CPU 非空闲时间占比 | 最大化 |
| 能效比 (Power Efficiency) | 每瓦特功耗完成的计算量 | 最大化 |
1.2 从 O(n) 到 O(1) 再到 CFS:调度器的演进之路
Linux 调度器经历了三个阶段的重要演进:
第一代:O(n) 调度器(Linux 2.4 时代)
// 早期 O(n) 调度器核心逻辑
for (每个可运行进程 p) {
p->counter = (p->counter >> 1) + p->nice; // 重新计算时间片
if (p->counter > best->counter)
best = p; // 遍历选择 counter 最大的进程
}
问题:随着进程数量增长,调度决策时间线性增长,严重限制了可扩展性。
第二三代:O(1) 调度器(Linux 2.6.0 - 2.6.22)
引入了两个优先级数组(active/expired)和 140 级优先级队列,实现了常数级调度决策。但其基于固定时间片和静态优先级表的"启发式"规则在多核场景下表现不佳,交互进程的探测逻辑也日趋复杂。
第三代:CFS(Completely Fair Scheduler, Linux 2.6.23+)
Ingo Molnár 提出的 CFS 颠覆了传统调度器的设计思路——不再分配时间片,而是追踪每个进程的虚拟运行时间,选择 vruntime 最小的进程运行。这一设计简洁而深刻,至今仍是 Linux 默认的公平调度器。
1.3 CFS 的核心哲学:"完全公平"与虚拟运行时间
CFS 的核心思想可以概括为一句话:让所有可运行进程的虚拟运行时间(vruntime)以相同的速率增长。
所谓"完全公平",是指在没有进程阻塞的理想多核系统中,每个进程应该获得与其权重成正比的实际 CPU 时间。CFS 不承诺完美公平——那是 NP-hard 问题——而是通过 vruntime 这个抽象概念实现"近似完全公平":
- 每个进程维护一个
vruntime(纳秒级) - 进程每运行 1 毫秒实际时间,高权重进程的 vruntime 增长慢,低权重进程增长快
- 调度时总是选择 vruntime 最小的进程上获得 CPU
- 红黑树保证选择操作 O(log n) 复杂度
1.4 调度类与优先级层次结构
Linux 调度器采用调度类(sched_class)的插件化架构,不同类别的调度器以优先级层次共存:
/* kernel/sched/sched.h */
extern const struct sched_class stop_sched_class; /* 最高优先级 - 停止任务 */
extern const struct sched_class dl_sched_class; /* 最早截止时间优先 */
extern const struct sched_class rt_sched_class; /* 实时调度 (FIFO/RR) */
extern const struct sched_class fair_sched_class; /* CFS - 普通进程 */
extern const struct sched_class idle_sched_class; /* 空闲任务 */
| 调度类 | 策略 | 优先级 | 说明 |
|---|---|---|---|
| stop_sched_class | N/A | 最高 | CPU 热插拔/迁移时的抢占式中断级任务 |
| dl_sched_class | SCHED_DEADLINE | 次高 | 最早截止时间优先算法,硬实时保障 |
| rt_sched_class | SCHED_FIFO / SCHED_RR | 高 | POSIX 实时调度,不受 CFS 时间片约束 |
| fair_sched_class | SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE | 中 | CFS 完全公平调度器 |
| idle_sched_class | N/A | 最低 | CPU 无任务时的 idle 线程 |
调度决策从最高优先级类开始依次查找,只有高优先级类没有可运行进程时,才会进入下一级。这意味着 所有实时进程在 CFS 获得 CPU 之前就被优先调度。
第二章:CFS 核心数据结构
2.1 task_struct 中的调度相关字段
每个进程的 task_struct 中嵌入了 sched_entity 结构体,这是 CFS 调度器的核心调度单元:
/* include/linux/sched.h */
struct task_struct {
/* 调度相关 */
int prio; /* 动态优先级 [0-139] */
int static_prio; /* 静态优先级 (nice+120) */
int normal_prio; /* 基于 static_prio 和调度策略 */
unsigned int rt_priority; /* 实时优先级 [0-99] */
const struct sched_class *sched_class; /* 调度类指针 */
struct sched_entity se; /* CFS 调度实体 */
struct sched_rt_entity rt; /* 实时调度实体 */
struct sched_dl_entity dl; /* 截止时间调度实体 */
#ifdef CONFIG_SMP
int recent_used_cpu; /* 最近使用的 CPU */
int wake_cpu; /* 唤醒时选择的 CPU */
#endif
};
2.2 sched_entity 深度解析
sched_entity 是每个 CFS 可运行实体的核心数据结构:
/* include/linux/sched.h */
struct sched_entity {
struct load_weight load; /* 权重(用于份额计算) */
unsigned long runnable_weight; /* CFS bandwidth 用 */
struct rb_node run_node; /* 红黑树节点 */
u64 exec_start; /* 本次调度开始时间 */
u64 vruntime; /* 虚拟运行时间 (ns) */
u64 prev_sum_exec_runtime; /* 上次累计运行时间 */
u64 nr_migrations; /* 跨 CPU 迁移次数 */
struct sched_statistics statistics; /* 统计信息 */
#ifdef CONFIG_FAIR_GROUP_SCHED
int depth; /* 层级深度 */
struct sched_entity *parent; /* 父调度实体 */
/* cfs_rq 的引用 */
struct cfs_rq *cfs_rq; /* 所属运行队列 */
struct cfs_rq *my_q; /* 子 cfs_rq(组调度时) */
#endif
};
2.3 cfs_rq:每 CPU 的公平调度运行队列
/* kernel/sched/sched.h */
struct cfs_rq {
struct load_weight load; /* 队列总权重 */
unsigned int nr_running; /* 可运行任务数 */
unsigned int h_nr_running; /* 包含子组的总数 */
u64 exec_clock; /* CPU 执行时间(单调递增) */
u64 min_vruntime; /* 队列最小 vruntime(公共基准) */
struct rb_root_cached tasks_timeline; /* 红黑树根(按 vruntime 排序) */
struct sched_entity *curr; /* 当前运行实体 */
struct sched_entity *next; /* 下一个要抢占的 */
struct sched_entity *last; /* 上次运行的(缓存热 */
#ifdef CONFIG_SMP
/*
* CPU 负载跟踪(PELT / WALT)
* 用于负载均衡决策
*/
unsigned long runnable_load_avg; /* 平均可运行负载 */
unsigned long blocked_load_avg; /* 阻塞状态负载贡献 */
u64 last_blocked_load_stamp;
#endif
#ifdef CONFIG_FAIR_GROUP_SCHED
struct rq *rq; /* 绑定的物理 runqueue */
struct task_group *tg; /* 所属的 task_group */
#endif
};
2.4 红黑树:为什么不用哈希表或数组?
CFS 选择红黑树作为 vruntime 排序容器,是有深思熟虑的设计选择:
| 容器类型 | 插入 | 删除 | 查找最小值 | 范围查询 |
|---|---|---|---|---|
| 未排序链表 | O(1) | O(n) | O(n) | O(n) |
| 最小堆 | O(log n) | O(n) | O(1) | O(n log n) |
| 红黑树 | O(log n) | O(log n) | O(log n) (cached O(1)) | O(log n + k) |
| 排序数组 | O(n) | O(n) | O(1) | O(log n + k) |
红黑树的选择原因:
- 查找最小值 O(1) 均摊:
rb_leftmost指针(rb_root_cached)缓存最左节点 - 稳定的 O(log n) 插入/删除:唤醒进程、阻塞进程都是 O(log n)
- 天然支持范围查询:pick_next、vruntime 追赶等操作需要"从最左边开始向右搜索"
rb_root_cached 是红黑树的一个增强结构,它在红黑树根节点上额外保存了一个 rb_node *rb_leftmost 指针,使获取最小值操作变为 O(1)。这是 CFS 高性能的关键微优化之一。
2.5 权重计算与 load_weight
CFS 的公平性通过权重(weight)实现。每个 sched_entity 的权重决定了它相对于其他任务获得的 CPU 时间比例:
/* 权重计算的核心公式 */
/*
* 实际获得CPU时间 = (进程权重 / cfs_rq总权重) * 调度周期
*
* 两个进程A(权重1024)和B(权重3072)竞争:
* A获得: 1024/(1024+3072) = 25%
* B获得: 3072/(1024+3072) = 75%
*/
/* update_load_avg() 中 PELT 负载跟踪 */
static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
u64 now = cfs_rq_clock_pelt(cfs_rq);
if (se->avg.last_update_time) {
/* 更新 PELT (Per-Entity Load Tracking) */
___update_load_avg(now, &se->avg);
cfs_se_util_change(&se->avg);
}
/* ... */
}
2.6 pick_next_task_fair:选择下一个进程
/* kernel/sched/fair.c */
static struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
struct cfs_rq *cfs_rq = &rq->cfs;
struct sched_entity *se;
struct task_struct *p;
/* 步骤1: 如果不是全换出,尝试简单处理 prev */
if (prev)
put_prev_task(rq, prev);
/* 步骤2: 从红黑树最左节点选出 vruntime 最小的实体 */
do {
se = pick_next_entity(cfs_rq, NULL);
set_next_entity(cfs_rq, se); /* 标记为当前运行 */
cfs_rq = group_cfs_rq(se); /* 如果是组调度,递归 */
} while (cfs_rq);
p = task_of(se);
/* 步骤3: 信息统计与追踪 */
if (hrtick_enabled(rq))
hrtick_start_fair(rq, p);
return p;
}
关键在于 pick_next_entity() 的实现——它总是选择红黑树最左侧(最小 vruntime)的节点:
static inline struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
struct sched_entity *left = __pick_first_entity(cfs_rq); /* O(1) */
struct sched_entity *se;
/*
* 如果 curr 的 vruntime 不比 left 大太多,
* 让 curr 继续运行(减少缓存失效)
*/
if (curr && (!left || entity_before(curr, left)))
se = curr;
else
se = left;
return se;
}
第三章:虚拟运行时间机制
3.1 vruntime 的计算公式
虚拟运行时间(vruntime)是 CFS 公平性的度量衡。其核心计算公式为:
/* kernel/sched/fair.c - update_curr() */
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(cfs_rq->rq); /* 当前时间戳 */
u64 delta_exec;
delta_exec = now - curr->exec_start; /* 实际运行时间 */
if ((s64)delta_exec <= 0)
return;
curr->exec_start = now; /* 更新下次起始时间 */
/* ===== 核心公式 ===== */
curr->vruntime += calc_delta_fair(delta_exec); /* 更新 vruntime */
/* ================== */
update_min_vruntime(cfs_rq); /* 更新队列最小值 */
}
/* calc_delta_fair: 将实际时间转换为虚拟时间 */
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
/* 基准权重是 NICE_0_LOAD = 1024 */
if (se->load.weight != NICE_0_LOAD)
delta = __calc_delta(delta, NICE_0_LOAD, &se->load); /* delta * 1024 / weight */
return delta;
}
/* 宏展开: __calc_delta(delta, NICE_0_LOAD, weight)
* 等价于: delta * 1024 / weight
*
* nice 0 (weight=1024): vruntime 增长速率 = 实际时间 (1:1)
* nice -5 (weight=1280): vruntime 增长慢 → 更频繁获得 CPU
* nice +5 (weight=102): vruntime 增长快 → 获得 CPU 更少
*/
Δvruntime = Δreal_time × (NICE_0_LOAD / weight)权重越大的进程,vruntime 增长越慢,因此被选中运行的频率越高,获得的实际 CPU 时间也越多。
3.2 nice 值到权重的转换表
Linux 内核使用预计算的 prio_to_weight 表将 [-20, +19] 的 nice 值映射到权重:
/* kernel/sched/core.c */
/*
* Nice levels are multiplicative, with a gentle 10% change in
* CPU usage for every nice level changed.
* 每增加 1 级 nice 值,CPU 时间减少约 10%
* (因为 nice+1 的权重是 nice 的 88.79%, 1/0.8879 ≈ 1.126 → 12.6%)
*
* 实际上 1024/1270 ≈ 0.806 → 1/0.806 ≈ 1.24, 但经验值约为 10%
* 因为调度周期内还涉及 slice 和 wakeup_granularity 等配置
*/
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
| nice | 权重 | 相对 CPU 份额 | 典型应用场景 |
|---|---|---|---|
| -20 | 88761 | 86.7% | 紧急系统任务 |
| -10 | 9548 | 9.3% | 高优先级服务 |
| 0 (默认) | 1024 | 1.00x (基准) | 普通进程 |
| +10 | 110 | 0.107x | 低优先级批处理 |
| +19 | 15 | 0.015x | 几乎仅在空闲时运行 |
相邻 nice 值的权重比例约为 1.25(即 1024/820 ≈ 1.249),这意味着每调高 1 级 nice 值,进程大约少获得 20% 的 CPU 份额(与老的 O(1) 调度器每级约 10% 不同)。
3.3 时钟中断中的 vruntime 更新路径
/* 时钟中断触发路径 */
tick_handle_periodic()
→ tick_periodic()
→ update_process_times() /* 内核定时器框架 */
→ scheduler_tick() /* CFS 时钟处理入口 */
→ task_tick_fair() /* 调用调度类的 tick 方法 */
→ entity_tick() /* 检查是否需要抢占 */
→ update_curr() /* 更新当前进程 vruntime */
→ resched_curr() /* 必要时设置 need_resched 标志 */
在 entity_tick() 中,CFS 检查当前进程是否已经用完了自己的"期望运行时间"(ideal_runtime):
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
/* 1. 更新 vruntime */
update_curr(cfs_rq);
/* 2. 理想运行时间检查 */
if (cfs_rq->nr_running > 1) {
struct sched_entity *se = __pick_first_entity(cfs_rq);
/* 判断当前进程是否比次优者领先太多 */
if (entity_before(se, curr) ||
curr->vruntime - se->vruntime > sched_slice(cfs_rq, curr)) {
resched_curr(rq_of(cfs_rq));
}
}
}
3.4 新建进程的 vruntime 继承策略
新进程创建时会继承父进程的 vruntime,但这需要特殊处理,否则新进程会因继承较大的 vruntime 而长时间得不到 CPU:
/* kernel/sched/fair.c - task_fork_fair() */
static void task_fork_fair(struct task_struct *p)
{
struct cfs_rq *cfs_rq = task_cfs_rq(current);
struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
struct rq *rq = this_rq();
struct rq_flags rf;
update_rq_clock(rq);
update_curr(cfs_rq); /* 确保父进程 vruntime 是最新的 */
/* 策略1: 子进程的 vruntime 不小于队列 min_vruntime */
if (sched_feat(PLACE_CHILD_SAME_CPU))
se->vruntime = curr->vruntime; /* 继承父进程 vruntime */
else
se->vruntime = max_vruntime(se->vruntime, cfs_rq->min_vruntime);
/*
* 策略2: RUN_TO_POSITION 特性
* 新建进程默认可以抢占同级的旧进程
* (如果 sched_features 开启了 PLACE_DEADLINE_INITIAL)
*/
if (sched_feat(PLACE_DEADLINE_INITIAL))
se->vruntime -= sched_latency(se);
}
3.5 时钟漂移与 NUMA 影响
在 NUMA 系统中,不同节点的时钟计数器(TSC)可能不完全同步。CFS 使用 sched_clock() 获取每个 CPU 的本地时间:
/* kernel/sched/clock.c */
u64 __always_inline sched_clock(void)
{
if (static_branch_unlikely(&sched_clock_stable))
return sched_clock_cpu(smp_processor_id());
return sched_clock_noincrement(); /* 回退方式 */
}
/*
* rq_clock_task(): CFS 使用的任务时钟
* 比当前 CPU 的 rq->clock 更精确,扣除了 IRQ 时间
*/
static inline u64 rq_clock_task(struct rq *rq)
{
return rq->clock_task;
}
第四章:多核调度与负载均衡
4.1 每 CPU runqueue 设计
Linux 采用 per-CPU runqueue 设计以获得最佳的可扩展性,减少跨 CPU 锁竞争:
/* kernel/sched/sched.h */
struct rq {
/* 运行队列锁(自旋锁) */
raw_spinlock_t __aligned(sizeof(long)) lock;
/* 三个调度类的运行队列 */
struct cfs_rq cfs; /* CFS 公平队列 */
struct rt_rq rt; /* 实时队列 */
struct dl_rq dl; /* 截止时间队列 */
/* 当前运行任务 */
struct task_struct *curr, *idle, *stop;
unsigned int nr_running; /* 可运行总任务数 */
unsigned long cpu_load[5]; /* 历史负载均线 */
/* 负载跟踪 */
#endif
#ifdef CONFIG_SMP
struct sched_domain __rcu *sd; /* 调度域起始指针 */
unsigned long rd_load; /* 运行域负载 */
unsigned int cpu_capacity; /* CPU 计算能力 */
unsigned int cpu_capacity_orig; /* 原始计算能力 */
#endif
};
/* 每 CPU 声明 */
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
- 每个 CPU 独立操作自己的 runqueue,无需跨 CPU 加锁
- 缓存亲和性更好:任务在自己的 CPU 上被调度,L1/L2 cache 热
- 减少总线锁争用,SMP 可扩展性显著提升
4.2 负载均衡算法
Linux 的负载均衡在多个层次运行,形成一种"自底向上"的均衡策略:
/* 负载均衡触发路径 */
scheduler_tick()
→ trigger_load_balance() /* 时钟中断触发 */
→ raise_softirq(SCHED_SOFTIRQ)
| 均衡类型 | 触发时机 | CPU 关系 | 迁移代价 |
|---|---|---|---|
| Idle Balance | CPU 进入 idle 前 | 目标到空闲 CPU | 最低 |
| Periodic Balance | tick / softirq | 调度域内 Busy CPU 间 | 中 |
| New Idle Balance | 刚变为 idle 的 CPU | 整个调度域搜索 | 中 |
| NUMA Balancing | 缺页异常时 | NUMA 节点间 | 最高 |
/* 核心均衡函数调用链 */
run_rebalance_domains()
→ rebalance_domains(this_rq, idle)
→ sd->balance_cpu() /* 遍历调度域自顶向下 */
→ load_balance(sd, this_rq, idle, continue_balancing)
→ find_busiest_group(sd) /* 找到最繁忙的调度组 */
→ move_tasks() /* 从繁忙组移动任务到本组 */
→ detach_tasks() /* 挑选合适任务 */
→ attach_tasks() /* 加入目标队列 */
4.3 调度域(sched_domain)与调度组(sched_group)
调度域是 Linux 用于描述 CPU 拓扑的层级化抽象,每一层对应一种物理拓扑级别:
/* kernel/sched/sched.h */
struct sched_domain {
/* 子域(更底层拓扑) */
struct sched_domain *parent; /* 必须被均衡后才均衡本层 */
struct sched_domain *child; /* 基础域(SMT 或 MC) */
/* 调度域内的调度组(环形链表) */
struct sched_group *groups; /* 第一个调度组 */
/* 均衡参数 */
unsigned int min_interval; /* 最小均衡间隔 */
unsigned int max_interval; /* 最大均衡间隔 */
unsigned int busy_factor; /* 繁忙因子 */
unsigned int imbalance_pct; /* 不平衡触发阈值 */
/* 不均衡标志位 */
unsigned long flags; /* SD_LOAD_BALANCE/SD_BALANCE_WAKE 等 */
/* 统计信息 */
unsigned int lb_count[NUM_LB_TYPES]; /* 各类型均衡执行次数 */
};
struct sched_group {
struct sched_group *next; /* 环形链表 */
const struct sched_group_energy *sge; /* 能耗数据 */
unsigned int group_weight; /* 组内 CPU 数量 */
/* 组的 CPU 掩码和负载统计 */
struct cpumask cpumask;
unsigned long cpumask_bit[BITS_TO_LONGS(nr_cpu_ids)];
unsigned long sg_load; /* 组的负载 */
unsigned long sg_capacity; /* 组的容量 */
};
典型的 x86 服务器调度域层次:
DIE (NUMA node)
└── MC (Multi-Core)
└── SMT (Hyper-Threading)
典型的 4-socket 服务器:
Level 0: SMT (每个物理 Core 的两个 HT) → 负载最频繁
Level 1: MC (同 DIE 的所有 Core) → 中等频率
Level 2: DIE/NUMA (不同 node 间) → 最不频繁
4.4 NUMA 感知调度和进程迁移
Linux 的 NUMA 调度包含两种机制:
/*
* 1. Auto NUMA Balancing (内核 3.13+)
* 由内核自动采样页面访问,识别 NUMA 远程页面并迁移
*
* 触发路径: do_anonymous_page() / do_swap_page()
*/
task_numa_fault()
→ numa_migrate_memory() /* 迁移内存页面 */
→ task_numa_assess() /* 评估是否需要迁移整个进程 */
→ migrate_task_to() /* 迁移进程到页面所在节点 */
/*
* 2. NUMA-aware initial placement
* 在 fork/wake 时选择进程的初始 CPU
*/
select_task_rq_fair() /* 选择最佳唤醒 CPU */
→ select_idle_sibling() /* 优先选择共享 cache 的空闲 CPU */
→ find_idlest_group() /* 空闲组选择 */
→ sched_cpu_startup_entry() /* 考虑 NUMA 距离 */
numactl --interleave=all 或 mbind() 手动控制。
4.5 load_balance() 与 move_tasks() 核心代码
/* kernel/sched/fair.c */
static inline int load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum cpu_idle_type idle,
int *continue_balancing)
{
/* 步骤1: 判断是否需要均衡 */
struct lb_env env = { ... };
struct sd_lb_stats *sds = ...;
update_sd_lb_stats(&env, sds);
/* 步骤2: 找到最繁忙的调度组 */
struct sched_group *busiest = find_busiest_group(&env, sds);
if (!busiest)
return 0; /* 已经均衡或不需要均衡 */
/* 步骤3: 选择具体的迁移任务 */
env.src_cpu = busiest_cpu;
return move_tasks(&env, &moved);
}
第五章:组调度(cgroup CPU 子系统)
5.1 Cgroup v1 cpu 子系统参数详解
| 参数 | 含义 | 默认值 | 典型配置 |
|---|---|---|---|
| cpu.shares | 该组在竞争 CPU 时获得的相对份额 | 1024 | Web: 2048, Batch: 512 |
| cpu.cfs_period_us | 带宽核算周期(微秒) | 100000 | 100ms |
| cpu.cfs_quota_us | 周期内可用 CPU 时间(微秒) | -1 (unlimited) | 80000 (8 核限 4 核) |
| cpu.stat | 运行统计(nr_periods / nr_throttled / throttled_time) | - | 诊断用 |
# 创建 cgroup
mkdir -p /sys/fs/cgroup/cpu/web_tier
# 设置相对份额(2 倍于默认)
echo 2048 > /sys/fs/cgroup/cpu/web_tier/cpu.shares
# 限制最高使用 0.5 核(50ms/100ms)
echo 100000 > /sys/fs/cgroup/cpu/web_tier/cpu.cfs_period_us
echo 50000 > /sys/fs/cgroup/cpu/web_tier/cpu.cfs_quota_us
# 将进程加入 cgroup
echo $PID > /sys/fs/cgroup/cpu/web_tier/cgroup.procs
# 查看统计(是否被 throttle)
cat /sys/fs/cgroup/cpu/web_tier/cpu.stat
输出示例:
nr_periods 12345 # 累计周期数
nr_throttled 123 # 被限流周期数
throttled_time 1.2e9 # 被限流总时间(纳秒)
5.2 层级化带宽控制实现原理
/* kernel/sched/fair.c - CFS bandwidth 控制 */
/*
* 每个 cfs_bandwidth 对应一个层级
* 过 quota 时,throttle_cfs_rq() 将实体从红黑树中移除
*/
struct cfs_bandwidth {
raw_spinlock_t lock;
ktime_t period;
u64 quota; /* 周期配额 */
u64 runtime; /* 当前周期剩余 */
struct hrtimer period_timer; /* 周期定时器 */
struct list_head throttled_cfs_rq; /* 被限流的队列 */
};
/* 限流处理 */
static void throttle_cfs_rq(struct cfs_rq *cfs_rq)
{
struct sched_entity *se;
long task_delta, dequeue = 1;
/* 从红黑树中移除所有子实体 */
for_each_sched_entity(se) {
struct cfs_rq *qcfs_rq = cfs_rq_of(se);
/* 从红黑树中 dequeue,减少 nr_running */
dequeue_entity(qcfs_rq, se, DEQUEUE_SLEEP);
qcfs_rq->h_nr_running -= task_delta;
}
/* 子 cfs_rq 从父灰黑树中移除 */
}
/* 周期定时器回调 - 补充 quota */
static enum hrtimer_restart sched_cfs_period_timer(struct hrtimer *timer)
{
/* 补充 runtime = quota */
start_cfs_bandwidth(cfs_b);
distribute_cfs_runtime(cfs_b); /* 给被限流的 rqs 恢复 */
}
5.3 容器/K8s 中的应用
K8s Pod 的 CPU requests/limits 在底层映射到 CFS cgroup 参数:
# K8s Pod 资源声明
resources:
requests:
cpu: "500m" # 0.5 核 → cpu.shares = 512 */
limits:
cpu: "2" # 2 核 → quota=200000, period=100000 */
| K8s 资源 | 底层映射 | 含义 |
|---|---|---|
| cpu.requests | cpu.shares | 相对份额(权重),影响竞争时的分配比例 |
| cpu.limits | cpu.cfs_quota_us + cfs_period_us | 硬性周期配额,超限即被 throttle |
| 无 requests | shares=2 (下限 2) | 至少获得极少量 CPU 份额 |
| 无 limits | quota=-1 (unlimited) | 可以使用空闲 CPU(按 shares 比例) |
K8s CPU Throttling 的常见问题:
容器内运行单线程 CPU 密集型进程,设 limits=1 核(period=100ms, quota=100ms):
- 该进程在一个 100ms 周期内只能运行 100ms
- 但 CFS 调度粒度更细,进程可能在周期前端就用尽 quota
- 随后被 throttle 在整个周期剩余时间内无法运行
5.4 Cgroup v2 cpu.weight 调优
# Cgroup v2 的 cpu 控制器参数 */
# 查看当前设置(权重范围 1-10000,默认 100)
cat /sys/fs/cgroup/web_tier/cpu.weight
# 设置高优先级组(100 倍于默认)
echo 10000 > /sys/fs/cgroup/web_tier/cpu.weight
# 设置最大带宽限制
echo "max 100000" > /sys/fs/cgroup/web_tier/cpu.max
# 含义: 在 100000us 周期内最多使用 100000us (不限制) */
echo "50000 100000" > /sys/fs/cgroup/web_tier/cpu.max
# 含义: 在 100000us 周期内最多使用 50000us (限 0.5 核) */
cat /sys/fs/cgroup/web_tier/cpu.max.burst
burst 值允许短期突发超过限制,但要在一个大周期内偿还 */
5.5 Cgroup 与 vruntime 的关系:throttle 机制
当一个 cgroup 的 cfs_rq 被 throttle 时:
- 该 cgroup 下的所有
sched_entity从红黑树中移除 - 进程保持在
TASK_RUNNING状态但实际上不执行 - vruntime 不再增长(关键!被限流的任务不会"落后"于其他任务)
- 当被 unthrottle 时重新入队,vruntime 维持在之前的值
/* unthrottle 时恢复策略 */
static void unthrottle_cfs_rq_async(struct cfs_rq *cfs_rq)
{
/* 重新入队,vruntime 不变 → 被限流进程不会获得不公平的优势 */
for_each_sched_entity(se) {
enqueue_entity(cfs_rq, se, ENQUEUE_WAKEUP);
/* vruntime 保持不变 → 进程回到队列中正确位置 */
}
}
第六章:性能调优实战
6.1 核心可调参数详解
| 参数 | 路径 | 默认值 | 含义 |
|---|---|---|---|
| sched_min_granularity_ns | /proc/sys/kernel/sched_min_granularity_ns | 1000000 (1ms) | CFS 最小调度粒度(一个进程最少运行时间) |
| sched_wakeup_granularity_ns | /proc/sys/kernel/sched_wakeup_granularity_ns | 15000000 (15ms) | 唤醒抢占粒度(被唤醒进程需要领先多少才抢占) |
| sched_migration_cost | /proc/sys/kernel/sched_migration_cost_ns | 500000 (0.5ms) | 判断进程是否 cache-hot 的时间阈值 |
| sched_nr_migrate | /proc/sys/kernel/sched_nr_migrate | 32 | 每次均衡最多迁移的任务数 |
| sched_cfs_bandwidth_slice_us | /proc/sys/kernel/ | 5000 (5ms) | CFS bandwidth 过期后默认切片 |
| sched_tunable_scaling | /proc/sys/kernel/ | logarithmic | 参数缩放模式 |
# 查看当前值
sysctl -a | grep sched_
# 调整最小调度粒度(减小以降低延迟,增大以提高吞吐)
sysctl -w kernel.sched_min_granularity_ns=2000000 # 2ms */
# 调整唤醒抢占粒度(增大减少不必要抢占)
sysctl -w kernel.sched_wakeup_granularity_ns=20000000 # 20ms */
# 降低迁移成本阈值,让负载均衡更积极
sysctl -w kernel.sched_migration_cost_ns=200000 # 0.2ms */
min_granularity越小 → 响应延迟越低,但切换开销越大wakeup_granularity越大 → 越不容易被抢占,吞吐越好,响应越差migration_cost越大 → 越不倾向迁移(有利于 cache 复用)
6.2 CPU 亲和性与 NUMA 策略
/* 设置 CPU 亲和性 */
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset); /* CPU 0 */
CPU_SET(2, &cpuset); /* CPU 2 */
sched_setaffinity(pid, sizeof(cpuset), &cpuset);
# 使用 taskset 工具 */
taskset -cp 0,2 $PID # 将 PID 绑定到 CPU 0 和 2 */
taskset -c 0 ./my_program # 启动时绑定到 CPU 0 */
# 使用 numactl 控制 NUMA 策略 */
numactl --cpunodebind=0 --membind=0 ./app # 绑定到 node0 */
numactl --interleave=all ./db_app # 交错分布 */
numactl --preferred=1 ./ml_training # 优先使用 node1 */
# 查看 NUMA 拓扑 */
numactl --hardware
lscpu | grep NUMA
6.3 实时线程与 CFS 共存
/* POSIX 实时调度设置 */
struct sched_param param;
param.sched_priority = 50; /* RT 优先级 1-99 */
// SCHED_FIFO: 先进先出,运行到阻塞或主动让出 */
sched_setscheduler(pid, SCHED_FIFO, ¶m);
// SCHED_RR: 时间片轮转(受 sched_rr_get_interval 约束)*/
sched_setscheduler(pid, SCHED_RR, ¶m);
# chrt: 实时调度工具 */
chrt -p $PID # 查看调度策略 */
chrt -f -p 50 $PID # 设为 SCHED_FIFO 优先级 50 */
chrt -r -p 80 -b /path/to/bin # 以 SCHED_RR 启动 */
# schedtool: 更强大的调度设置 */
schedtool -F -p 50 -e ./app # FIFO + 执行 */
schedtool -D -e ./batch_script # SCHED_IDLE(最小影响)*/
# 查看 rt 时间片信息 */
chrt -m # 查看支持的最小/大间隔 */
cat /proc/sys/kernel/sched_rt_runtime_us # RT 任务总 CPU 时间限制 */
cat /proc/sys/kernel/sched_rt_period_us # RT 周期 (默认 1s) */
sched_rt_runtime_us=950000)。在高实时性要求的嵌入式系统中可能需要调整为 -1(unlimited),但要确保有 watchdog 机制兜底。
6.4 sched_features 调优开关
/proc/sys/kernel/sched_features 中的特性开关:
# 查看当前特性 */
cat /proc/sys/kernel/sched_features
# 常用特性:
# GENTLE_FAIR_SLEEPERS - 对睡眠进程更友好(减少唤醒后对新进程的不公)
# NEXT_BUDDY - 刚唤醒的进程优先作为 "next" 运行
# LAST_BUDDY - 被切换下来的进程优先作为 "next" 运行
# CACHE_HOT_BUDDY - 优先使用 cache-hot 的进程
# WAKEUP_PREEMPTION - 唤醒时立即抢占(低延迟模式)
# HRTICK - 高精度定时器 tick
# DOUBLE_TICK - 双倍 tick(兼容)
# NONTASK_CAPACITY - 非任务容量感知
# TTWU_QUEUE - 唤醒队列到目标 CPU
# SIS_PROP - 搜索空闲 CPU 时考虑 cache 亲和性
# SIS_UTIL - 基于 CPU 利用率选择
# WARN_DOUBLE_CLOCK - 双时钟警告
# RT_RUNTIME_SHARE - RT 运行时间共享
# LB_MIN - 负载均衡最小任务数
# ATTACH_AGE_LOAD - 迁移时传递负载年龄
# WA_IDLE_AT_NOHZ - NOHZ 空闲唤醒
# NO_LATENCY_ACCOUNTING - 不计入延迟统计
# NO_NEXT_BUDDY - 不优先 next
# NO_LAST_BUDDY - 不优先 last
# NO_CACHE_HOT_BUDDY - 不优先 cache-hot
# NO_HRTICK - 关闭 HRTICK
# NO_DOUBLE_TICK - 关闭 DOUBLE_TICK
# NO_NONTASK_CAPACITY - 忽略 NONTASK_CAPACITY
# NO_TTWU_QUEUE_GLOBAL - 关闭 TTWU 全局队列
# NO_SIS_AVG_CPU - 不基于平均利用率搜索
# NO_SIS_PROP - 基于 cache 亲和性搜索
# NO_SIS_UTIL - 基于利用率搜索
# NO_UCLAMP_MAX - 不限制 uclamp max
# NO_RT_PUSH_IPI - 不通过 IPI 推送 RT
# NO_RT_RUNTIME_SHARE - 不共享 RT 运行
# NO_LB_MIN - 不限制最小均衡
# NO_ATTACH_AGE_LOAD - 不传递负载年龄
# NO_WA_IDLE_AT_NOHZ - 不 NOHZ 唤醒
# NO_LATENCY_ACCOUNTING_WARN - 不警告延迟统计
*/
# 通过 echo 关闭某些特性(加 NO_ 前缀) */
# 注意:需 echo 到位,实际使用较复杂,建议查看内核文档 */
6.5 实际案例:数据库工作负载调优
以 MySQL/PostgreSQL 为代表的高性能数据库系统,对调度器的要求是低延迟和高吞吐的平衡:
#!/bin/bash
# 数据库服务器 CFS 调优脚本 (适用于 OLTP 工作负载) */
# 1. 降低最小调度粒度以获得更好的交互响应 */
sysctl -w kernel.sched_min_granularity_ns=1000000 # 1ms */
# 2. 适度的唤醒抢占(避免频繁抢占导致锁竞争) */
sysctl -w kernel.sched_wakeup_granularity_ns=12000000 # 12ms */
# 3. 提高迁移成本阈值,减少数据跨 NUMA 节点 */
sysctl -w kernel.sched_migration_cost_ns=1000000 # 1ms */
# 4. NUMA 策略:数据库进程绑定到本地内存节点 */
NUMA_NODE=0
DB_PID=$(pidof mysqld | awk '{print $1}')
numactl --cpunodebind=$NUMA_NODE --membind=$NUMA_NODE \
-p $DB_PID 2>/dev/null || echo "Binding at process start preferred"
# 5. 专用 CPU set for DB foreground threads */
# 假设 CPU 0-3 给 DB,其余给操作系统和其他服务 */
systemctl set-property --runtime -- user.slice CPUQuota="400%"
# 6. 禁用 RT runtime share 以确保 OS 不被完全抢占 */
sysctl -w kernel.sched_rt_runtime_us=950000 # 默认值,谨慎修改 */
# 7. 大数据库系统应调整:允许更激进的进程迁移 */
sysctl -w kernel.sched_nr_migrate=64 # 增加批量均衡 */
# 8. irqbalance 确保中断不集中在 DB 使用的 CPU 上 */
systemctl enable irqbalance
第七章:观测与调试
7.1 /proc/{pid}/sched 深度解读
# 查看进程 1234 的调度信息 */
cat //proc/1234/sched | head -30
/*
* 关键输出字段解读:
* ------------------------------------------------------------------
* mysqld (1234, #threads: 45)
* ------------------------------------------------------------------
* exec_start : 19283471523.456789 # 本次调度开始时间 (s.μs)
* vruntime : 12345.678901 # 虚拟运行时间 (ns)
* sum_exec_runtime : 9876543210.123456 # 累计实际运行时间 (ns)
* nr_migrations : 234 # CPU 迁移次数
* nr_voluntary_switches : 56789 # 自愿切换 (等待I/O等)
* nr_involuntary_switches : 12345 # 非自愿切换 (时间片到期)
* se.load.weight : 1024 # 当前权重 (nice 0 = 1024)
* se.runnable_load_sum : 987654321 # 负载跟踪累加器
* se.runnable_load_avg : 1024 # 平均可运行负载
* se.avg.last_update_time : 1234567890 # 上次更新 (ns)
* se.avg.runnable_sum : 123456789 # PELT 累加器
* se.avg.runnable_avg : 512 # 平均运行份额
* se.avg.avg_period : 47360 # 平均周期 (48ms对应1024)
* se.avg.decay_count : 567890 # 衰减计数
* policy : 0 # SCHED_NORMAL (CFS)
* prio : 120 # 静态优先级 (= nice 0)
* clock-delta : 42 # 时钟偏差
* */
通过 nr_voluntary_switches 和 nr_involuntary_switches 的比例可以判断进程的调度行为:
- voluntary 很高:进程主动放弃 CPU(等待 I/O、锁、条件变量),属于 I/O 密集型
- involuntary 很高:进程被强制切换(时间片到期),属于 CPU 密集型或调度参数需调整
7.2 perf sched 工具
# 1. 录制调度事件(采样调度器决策) */
perf sched record -a sleep 10
# 生成 perf.data.sched 文件 */
# 2. 查看调度时间线(哪些任务在什么时候运行) */
perf sched timeline
/*
* TIMESTAMP CPU TASK NAME DURATION (ms)
* 12345.6789 0 mysqld:[1234] 10.5 ← 进程运行时长
* 12345.6894 0 swapper/0 0.1 ← 空行切换
* 12345.6895 0 nginx:[5678] 8.2 ← 长运行
* ...
*/
# 3. 统计每个任务的调度延迟(从唤醒到实际运行的等待时间) */
perf sched latency --sort max
/*
* TASK | Runtime (ms) | Switches | Average delay (ms) | Maximum delay (ms)
* mysqld:1234 | 8567.3 | 2345 | 0.5 | 15.2 ← 最大延迟!
* redis-server:5678 | 6123.4 | 1890 | 0.3 | 8.7
* ...
*/
# 4. 查看 CPU 时间分布(哪个任务占用了哪些 CPU 的时间) */
perf sched map
/*
* CPU 0 1 2 3
* t1 + - - -
* t2 - A - -
* t3 - - B -
* t4 - - - +
* - C C C C
* A/B/C 表示时间线,+ 是当前运行任务,- 是被抢占
*/
# 5. 重建调度序列(检测反复睡眠被唤醒但没有进展的死锁) */
perf sched replay -v
7.3 ftrace 调度事件跟踪
# 启用调度跟踪 */
cd /sys/kernel/tracing # 或 /sys/kernel/debug/tracing */
# 1. sched_switch:跟踪进程切换事件 */
echo 0 > tracing_on
echo sched_switch > set_event
echo 1 > tracing_on
# 等待 N 秒后... */
echo 0 > tracing_on
cat trace
/*
* 输出示例:
* mysqld-1234 [001] d.h. 12345.6789: sched_switch: \
* mysqld:1234 [120] S ==> nginx:5678 [120] R \
* target cpu: 001 comm=mysqld pid=1234 prio=120 state=S ==> comm=nginx pid=5678 prio=120 state=R
*/
# 2. sched_wakeup / sched_wakeup_new:跟踪进程唤醒 */
echo sched_wakeup sched_wakeup_new > set_event
echo 1 > tracing_on
# 3. 使用 trace-cmd 更方便 */
trace-cmd record -e sched_switch -e sched_wakeup -p function_graph sleep 10
trace-cmd report | less
7.4 eBPF/BCC 脚本分析调度延迟
# 安装 BCC-tools */
apt install bpfcc-tools # 或 yum install bcc-tools */
# 1. runqlat:测量任务在 runqueue 中等待 CPU 的延迟分布 */
/usr/share/bcc/tools/runqlat 1 3
/*
* usecs : count distribution
* 0 -> 1 : 5 |**** |
* 2 -> 3 : 8 |******* |
* 4 -> 7 : 15 |*************** |
* 8 -> 15 : 45 |******************************************|
* 16 -> 31 : 28 |**************************** |
* 32 -> 63 : 12 |************ |
* 64 -> 127 : 3 |*** |
* 128 -> 255 : 1 |* |
* 256 -> 511 : 0 | |
* 512 -> 1023 : 0 | |
* 1024 -> 2047 : 1 |* ← 极端延迟:1ms+!
*/
# 2. runqlen:测量队列长度(并行度) */
/usr/share/bcc/tools/runqlen -C 1 3
/*
* cpu = 0
* : count distribution
* 0 : 92 |***********************************************|
* 1 : 25 |*********** |
* 2 : 10 |**** |
* 3 : 3 |* |
* 5 : 1 | |
*/
# 3. offcptime:统计 CPU 在 off-CPU 状态的时间(等待 I/O 等) */
/usr/share/bcc/tools/offcptime -p $PID 5
# 4. wakeuptime:分析唤醒延迟(从 sched_wakeup 到实际调度) */
/usr/share/bcc/tools/wakeuptime 1 5
7.5 实际排查:响应时间抖动分析
场景:某 API 服务 P99 延迟周期性从 10ms 飙升到 200ms。
# 步骤1: 观测 P99 延迟是否与 runqueue 延迟相关 */
runqlat -p $(pidof api_server) 1 60 | tee runqlat.log
/* 发现: 高延迟时 runqlat 在 50-100μs 区间有峰值 */
# 步骤2: 检查 CPU 亲和性中断分布 */
perf stat -e cpu-clock -p $API_PID -a sleep 1
# 步骤3: 定位哪个进程在"偷"CPU */
perf sched record -p $API_PID sleep 5
perf sched latency --sort max | head -20
/* 发现: host-agent-backup 压缩进程间歇性占用同核 */
# 步骤4: 验证 NUMA 位置 */
cat /proc/$BACKUP_PID/status | grep Cpus_allowed_list
cat /proc/$API_PID/status | grep Cpus_allowed_list
# 修复: 将 backup 进程限制到不同 NUMA 节点 */
numactl --cpunodebind=1 --membind=1 -p $BACKUP_PID
taskset -cp 0-7 $API_PID
根本原因:日志压缩进程(nice=0)随机选择唤醒 CPU,反复抢占 API 进程所在的物理核。通过 CPU 亲和性隔离后,P99 延迟稳定在 15ms 以内。
第八章:最新发展趋势
8.1 EEVDF 调度器(Linux 6.6+)
2023 年 Linux 6.6 内核引入的 EEVDF(Earliest Eligible Virtual Deadline First)调度器是对 CFS 的突破性改进,旨在解决 CFS 在高负载和精确延迟场景下的不足:
/*
* EEVDF 核心概念:
*
* 传统 CFS: 选择 vruntime 最小的进程
* EEVDF: 选择 deadline 最小的进程
*
* 每个进程有三个关键时间:
* - vruntime: 虚拟运行时间(已见)
* - vdegrade: 虚拟降解时间
* - deadline: vruntime + 调度延迟期望
*
* 相比 CFS 的优势:
* 1. O(1) 调度决策(用当前时间戳替代红黑树搜索)
* 2. 更平滑的延迟分布(避免 CFS 可能出现的"饥饿窗口")
* 3. 更精确的 CPU 份额分配
*/
/* 切换到 EEVDF */
// 编译时启用 */
CONFIG_SCHED_CLASS=y
CONFIG_SCHED_CORE=y
CONFIG_SCHED_ALT=y /* 或 */
// 启动参数 */
sched=eevdf
| 特性 | CFS | EEVDF (6.6+) |
|---|---|---|
| 调度决策复杂度 | O(log n) | O(1) + O(log n) 维护 |
| 延迟稳定性 | 良好(平均值),尾部不稳定 | 优秀(严格保证截止时间) |
| 公平逼近度 | 渐近公平(理想延迟模型) | 精确公平(在每个粒度内) |
| 红黑树依赖 | 是(vruntime 排序) | 否(时间轮 + deadline 队列) |
| NUMA 均衡 | 逐步完善 | 更精确(每实体 deadline 同步) |
8.2 sched_ext:BPF 可扩展调度器框架
Linux 6.12 引入的 sched_ext(Scheduler Extender)框架允许用户空间通过 BPF 程序完全替换调度器策略:
/*
* sched_ext 架构:
*
* 用户空间 BPF 程序提供:
* - pick_next_task(): 自定义选择下一个进程的逻辑
* - enqueue_task(): 入队通知
* - dequeue_task(): 出队通知
* - dispatch(): 决定如何分发任务
*
* 内核提供:
* - CPU 共享的顶级调度逻辑
* - 安全边界和超时机制
* - BPF map 与 helpers
*/
/* 编译一个自定义 scheduler */
// 1. 编写 BPF 程序 (my_sched_ext.bpf.c) */
#include <scx/common.bpf.h>
SEC("")
int select_cpu(struct task_struct *p, int prev_cpu, u64 wake_flags)
{
/* 自定义 CPU 选择逻辑 */
return bscctx_select_cpu_from_idle(p, prev_cpu, available_mask);
}
SEC("")
void enqueue(struct task_struct *p, u64 enq_flags)
{
/* 自定义入队逻辑(如优先级调整) */
}
// 2. 加载并启用 */
$ sudo scx_rustland /* 社区实现的 Rust BPF 调度器 */
$ sudo scx_simple /* 最简单的参考实现 */
// 查看当前使用的调度器 */
$ sysctl kernel.sched_ext_name
kernel.sched_ext_name = scx_rustland
sched_ext 的首批社区调度器:
- scx_rustland:Rust 编写的通用调度器,综合性能优秀
- scx_nest:针对 Nest 型突发负载设计
- scx_layered:分层调度策略,不同层不同策略
- scx_pair:成对SMT感知调度器
8.3 异构调度(ARM big.LITTLE / Intel Thread Director)
在异构 CPU 架构下,调度器需要感知不同核心的计算能力差异:
/*
* CPU Capacity 模型:
*
* X86 Intel (大小核):
* P-core: capacity_orig = 1024 (最大)
* E-core: capacity_orig = 640 (约 63%)
*
* ARM big.LITTLE (示例):
* Cortex-X2: capacity_orig = 1024
* Cortex-A710: capacity_orig = 676
* Cortex-A510: capacity_orig = 320
*/
struct sched_domain {
unsigned int cpu_capacity; /* 该 CPU 的计算能力 */
unsigned int cpu_capacity_orig; /* 原始满频能力 */
};
关键技术要点:
/*
* 1. Capacity-aware placement: 任务放置考虑 CPU 能力
* 2. Capacity-aware load balancing: 负载均衡使用 PELT-SMT cap ratio
* 3. Utilization Clamping (UCLAMP): 用户空间可以限制任务的最小/最大 CPU 容量
*/
/* UCLAMP API (通过 sched_setattr 或 cgroup) */
// uclamp_min: 要求任务至少在此性能段运行(防止降频)
// uclamp_max: 限制任务不超过此性能段(限制功耗)
//
// 场景示例:
// - 视频解码线程: uclamp_min=512 (至少中等性能核)
// - 后台日志写入: uclamp_max限制到效率核
/* Intel Thread Director 支持 */
/* 6 级硬件提示 vs Linux 使用 AClearHint 跨 CPU 迁移 */
8.4 AI/ML 训练工作负载中的调度优化
AI/ML 训练工作负载(特别在大规模分布式训练中)对调度器有独特要求:
/*
* AI/ML 调度挑战:
*
* 1. 长耗时任务 + 敏感于抢占(GPU 上下文切换代价高)
* 2. 通信密集(NCCL/RCCL 集合通信需要精确同步)
* 3. 内存绑定(模型状态、梯度聚合占用大量内存)
* 4. NUMA 敏感(GPU 到 CPU 的亲和性)
*/
/* 实际优化手段 */
// 1. 使用 SCHED_IDLE 避免干扰训练任务 */
sysctl -w kernel.sched_latency_ns=24000000 # 延长调度周期 */
sysctl -w kernel.sched_min_granularity_ns=3000000 # 3ms 最小粒度 */
// 2. 使用 NUMA 绑定确保训练进程靠近 GPU */
# GPU 位于 PCIe NUMA node 0 */
numactl --cpunodebind=0 --membind=0 python train.py
// 3. CPU 隔离减少系统噪声 */
# 在 boot 参数中隔离 CPU 5-31 给训练任务 */
isolcpus=5,6,7,...,31 nohz_full=5-31 rcu_nocbs=5-31
// 4. 离线推理任务使用 cpu.idle 策略 */
chrt -i 0 ./inference_worker
对于使用 torchrun 等分布式训练框架的场景,关键调度优化参数:
# 分布式训练推荐配置 */
# 1. 关闭该 NUMA 节点的自动均衡 */
echo 0 > /proc/sys/kernel/numa_balancing
# 2. 增加迁移阈值(减少训练进程被均衡的时间) */
sysctl -w kernel.sched_migration_cost_ns=5000000
# 3. 使用 cpuset 精确隔离 GPU 附近的 CPU */
mkdir /sys/fs/cgroup/cpuset/training
echo "5-15" > /sys/fs/cgroup/cpuset/training/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/training/cpuset.mems
echo $TRAIN_PID > /sys/fs/cgroup/cpuset/training/cgroup.procs
# 4. IRQ 中断必须避开训练 CPU(由 irqbalance 处理或手动设置) */
# 手动示例: 将所有中断绑定到 CPU 0-4 */
for irq in $(ls /proc/irq/ | grep -v default); do
echo 0f > /proc/irq/$irq/smp_affinity # 绑定到 CPU 0-3 */
done

发表评论 取消回复