Linux内核架构浅谈24-Linux CFS调度器:延迟跟踪与调度周期动态调整
解析完全公平调度器的核心机制——如何通过延迟跟踪保证响应性,通过动态周期平衡公平与效率
Linux 2.6.23内核引入的完全公平调度器(CFS),彻底改变了传统Linux调度器的设计思路。“进程管理和调度”明确指出:CFS的核心目标是“模拟理想的公平调度”,而实现这一目标的关键技术包括延迟跟踪(确保进程响应性)和调度周期动态调整(平衡公平性与系统开销)。“完全公平调度类”的核心知识,详细拆解CFS的延迟跟踪机制(如何量化进程等待延迟)、调度周期的计算逻辑(如何根据进程数动态调整),并补充代码示例与实践验证方法,帮助读者理解CFS如何在“保证公平”与“降低延迟”之间找到平衡。
一、CFS延迟跟踪:从“理想公平”到“实际延迟控制”
CFS的设计灵感源于“理想CPU”模型——在理想情况下,N个进程可并行执行,每个进程获得1/N的CPU时间。但实际硬件中,CPU是串行调度的,这会导致进程产生“等待延迟”(从就绪到执行的时间差)。CFS通过延迟跟踪机制量化并控制这种延迟,核心是两个关键概念:调度延迟(sched_latency)和粒度(granularity)。

1. 延迟跟踪的核心参数
“延迟跟踪”,CFS通过三个核心参数定义延迟控制目标,这些参数可通过/proc/sys/kernel/接口配置,默认值适用于大多数场景:
参数名与默认值(2.6.24内核)
-
sysctl_sched_latency
默认值:20ms(20,000,000ns)
作用:保证所有可运行进程至少运行一次的最大时间间隔
配置路径:/proc/sys/kernel/sched_latency_ns -
sysctl_sched_min_granularity
默认值:4ms(4,000,000ns)
作用:进程最小运行时间粒度,避免进程切换过于频繁导致开销激增
配置路径:/proc/sys/kernel/sched_min_granularity_ns -
sysctl_sched_wakeup_granularity
默认值:4ms(4,000,000ns)
作用:唤醒抢占粒度,新唤醒进程需等待当前进程运行至少该时间后才能抢占
配置路径:/proc/sys/kernel/sched_wakeup_granularity_ns
参数设计逻辑:sched_latency是“公平性目标”——确保每个进程在该周期内都能得到CPU;min_granularity是“效率底线”——防止进程切换过于频繁(切换一次开销约1-2μs,若粒度过小,切换开销占比会超过实际工作时间);wakeup_granularity是“响应性保障”——避免新唤醒的交互式进程(如键盘输入)被长时间阻塞。
2. 延迟跟踪的实现:虚拟运行时间(vruntime)
CFS不使用传统的“时间片”,而是通过虚拟运行时间(vruntime)跟踪进程的“不公平程度”,进而实现延迟控制。vruntime的核心逻辑是:
- 进程优先级越高(nice值越小),vruntime增长越慢——确保高优先级进程能更频繁地被调度,降低等待延迟;
- 进程等待时间越长(睡眠后唤醒),其vruntime与队列最小vruntime的差值越大——确保长时间等待的进程优先被调度,避免延迟累积。
// __update_curr函数,简化版vruntime计算逻辑
static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr, unsigned long delta_exec) {
unsigned long delta_exec_weighted;
u64 vruntime;
// 1. 累加进程实际运行时间(物理时间)
curr->sum_exec_runtime += delta_exec;
// 2. 计算加权运行时间(虚拟时间):优先级越高,权重越大,虚拟时间增长越慢
// calc_delta_fair: 根据进程权重调整时间
if (curr->load.weight != NICE_0_LOAD) {
delta_exec_weighted = calc_delta_fair(delta_exec, &curr->load);
} else {
delta_exec_weighted = delta_exec; // nice=0时,虚拟时间=物理时间
}
// 3. 更新进程的虚拟运行时间
curr->vruntime += delta_exec_weighted;
// 4. 更新队列最小vruntime(用于计算进程的不公平程度)
cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, curr->vruntime);
}
通过一个例子解释vruntime的延迟跟踪效果:若系统中有2个进程(A:nice=0,权重1024;B:nice=10,权重335),A的vruntime增长速度是B的1024/335≈3倍。当A运行3ms时,其vruntime增加3ms,而B运行3ms时vruntime仅增加1ms——这意味着B的“不公平程度”更低,后续会优先被调度,间接控制了B的等待延迟。
二、调度周期动态调整:平衡公平与效率
“延迟跟踪”明确指出:CFS的调度周期(即“所有可运行进程都运行一次的时间间隔”)并非固定值,而是根据可运行进程数(nr_running)动态调整。核心逻辑是:当进程数较少时,使用默认的sched_latency作为周期;当进程数超过阈值时,按比例延长周期,避免单个进程运行时间过短导致切换开销激增。

1. 调度周期的计算公式
__sched_period函数的逻辑,调度周期(period)的计算分为两步:
- 计算阈值进程数:
sched_nr_latency = sched_latency / sched_min_granularity——当可运行进程数≤该值时,使用默认周期; - 动态调整周期:若进程数>阈值,则
period = nr_running * sched_min_granularity——确保每个进程至少运行min_granularity时间。
// 已知默认参数(2.6.24内核) sched_latency = 20ms,sched_min_granularity = 4ms // 步骤1:计算阈值进程数 sched_nr_latency = 20ms / 4ms = 5 → 进程数≤5时用默认周期 // 步骤2:不同进程数下的周期计算 情况1:nr_running=3(≤5) period = sched_latency = 20ms → 每个进程平均运行20ms/3≈6.67ms 情况2:nr_running=6(>5) period = 6 * 4ms = 24ms → 每个进程运行4ms(刚好min_granularity) 情况3:nr_running=10(>5) period = 10 * 4ms = 40ms → 每个进程运行4ms,总周期40ms
动态调整的意义:若进程数从5增加到10时仍使用20ms周期,每个进程平均运行时间会从4ms降至2ms——此时进程切换次数会翻倍(从5次增至10次),切换开销占比从(5次×2μs)/20ms=0.05%增至(10次×2μs)/20ms=0.1%,长期会影响系统效率。动态延长周期后,切换开销占比可维持在(10次×2μs)/40ms=0.05%,兼顾了公平性与效率。
2. 周期调整的核心函数:__sched_period
给出了__sched_period函数的实现逻辑,该函数是CFS动态周期调整的核心,根据可运行进程数返回最终的调度周期:
// __sched_period函数,完整逻辑
static u64 __sched_period(struct cfs_rq *cfs_rq) {
unsigned long nr_running = cfs_rq->nr_running; // 可运行进程数
u64 period;
// 步骤1:计算阈值进程数
unsigned long sched_nr_latency = sysctl_sched_latency / sysctl_sched_min_granularity;
// 步骤2:根据进程数动态调整周期
if (nr_running <= sched_nr_latency) {
// 进程数少,用默认调度延迟作为周期
period = sysctl_sched_latency;
} else {
// 进程数多,按min_granularity比例延长周期
period = nr_running * sysctl_sched_min_granularity;
}
// 步骤3:确保周期不小于最小阈值(避免极端情况)
if (period < sysctl_sched_min_granularity) {
period = sysctl_sched_min_granularity;
}
return period;
}
3. 进程切片时间的分配
调度周期确定后,CFS通过sched_slice函数为每个进程分配“切片时间”(该周期内可运行的最大时间),核心是按进程权重比例分配:
// sched_slice函数,简化版切片分配逻辑
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se) {
u64 period = __sched_period(cfs_rq); // 获取动态调度周期
u64 slice;
// 按进程权重比例分配切片时间:slice = 周期 × (进程权重 / 总权重)
slice = (period * se->load.weight) / cfs_rq->load.weight;
// 确保切片时间不小于最小粒度(避免进程运行时间过短)
if (slice < sysctl_sched_min_granularity) {
slice = sysctl_sched_min_granularity;
}
return slice;
}
示例:若系统中有2个进程(A:权重1024,B:权重512),调度周期为20ms,总权重=1024+512=1536,则A的切片时间=20ms×1024/1536≈13.3ms,B的切片时间=20ms×512/1536≈6.7ms——符合“高权重进程获得更多CPU时间”的公平性原则。
三、延迟与周期的协同:抢占决策机制
“处理周期性调度器”提到:CFS通过周期性检查(task_tick_fair)和唤醒抢占检查(check_preempt_wakeup)两种机制,结合延迟跟踪与调度周期,决定是否抢占当前进程,确保延迟控制目标落地。
1. 周期性检查:task_tick_fair
每个时钟周期(由HZ决定,2.6.24内核默认1000Hz,即1ms一次),CFS会调用task_tick_fair函数,检查当前进程是否已超过其切片时间,若超过则触发抢占:
// task_tick_fair函数,简化版抢占检查逻辑
static void task_tick_fair(struct rq *rq, struct task_struct *curr) {
struct cfs_rq *cfs_rq = task_cfs_rq(curr);
struct sched_entity *se = &curr->se;
u64 slice = sched_slice(cfs_rq, se); // 计算当前进程的切片时间
u64 delta_exec = curr->se.sum_exec_runtime - curr->se.prev_sum_exec_runtime;
// 检查:当前进程运行时间是否超过切片时间
if (delta_exec > slice) {
// 触发重调度:设置TIF_NEED_RESCHED标志
resched_task(rq->curr);
return;
}
// 额外检查:是否有等待时间更长的进程(vruntime差值过大)
if (cfs_rq->nr_running > 1) {
struct sched_entity *next = __pick_next_entity(cfs_rq); // 获取下一个候选进程
// 若下一个进程的vruntime远小于当前进程,触发抢占
if (next->vruntime + sysctl_sched_wakeup_granularity < se->vruntime) {
resched_task(rq->curr);
}
}
}
2. 唤醒抢占检查:check_preempt_wakeup
当新进程被唤醒(如交互式进程接收到键盘输入)时,CFS会调用check_preempt_wakeup函数,检查是否需要抢占当前进程,核心是平衡“响应性”与“切换开销”:
// check_preempt_wakeup函数,简化版唤醒抢占逻辑
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p) {
struct task_struct *curr = rq->curr;
struct cfs_rq *cfs_rq = task_cfs_rq(curr);
struct sched_entity *se = &curr->se, *pse = &p->se;
unsigned long gran = sysctl_sched_wakeup_granularity;
// 1. 若当前进程是SCHED_BATCH(批处理进程),直接允许抢占(不影响交互性)
if (curr->policy == SCHED_BATCH) {
resched_task(curr);
return;
}
// 2. 计算当前进程已运行时间
unsigned long delta_exec = curr->se.sum_exec_runtime - curr->se.prev_sum_exec_runtime;
// 3. 若当前进程运行时间不足唤醒粒度,不抢占(避免切换频繁)
if (delta_exec < gran) {
return;
}
// 4. 若新进程的vruntime+粒度 < 当前进程vruntime,触发抢占(新进程等待更久)
if (pse->vruntime + gran < se->vruntime) {
resched_task(curr);
}
}
唤醒抢占的意义:当用户在终端输入命令时,bash进程(交互式)被唤醒,若当前进程是一个已运行5ms的编译进程(超过唤醒粒度4ms),且bash的vruntime更小(等待更久),则CFS会抢占编译进程,让bash优先执行——这保证了交互式应用的低延迟响应。
四、实践验证:观察CFS的延迟与周期特性
我们可以通过Linux系统工具观察CFS的延迟跟踪与周期调整效果,将理论与实际结合。
1. 查看CFS调度参数
通过cat /proc/sys/kernel/sched_*查看CFS的延迟与周期相关参数,描述的默认值:
// 查看CFS核心参数(2.6.24内核示例输出) $ cat /proc/sys/kernel/sched_latency_ns 20000000 # 20ms, $ cat /proc/sys/kernel/sched_min_granularity_ns 4000000 # 4ms, $ cat /proc/sys/kernel/sched_wakeup_granularity_ns 4000000 # 4ms,
2. 观察调度周期动态调整
通过perf sched record和perf sched latency工具,观察不同进程数下的调度周期:
// 1. 运行5个进程(≤sched_nr_latency=5),记录调度行为
$ perf sched record -g -- sleep 10 &
$ for i in {1..5}; do while true; do :; done & done
// 2. 查看调度周期(平均约20ms,符合默认sched_latency)
$ perf sched latency --sort=pid
...
PID | avg age | max age | avg run | max run
1234 | 10.2ms | 19.8ms | 6.7ms | 7.2ms # 平均切片约6.7ms(20ms/3)
1235 | 9.8ms | 19.5ms | 6.5ms | 7.0ms
...
// 3. 运行10个进程(>sched_nr_latency=5),再次记录
$ for i in {6..10}; do while true; do :; done & done
// 4. 查看调度周期(平均约40ms,符合动态调整逻辑)
$ perf sched latency --sort=pid
...
PID | avg age | max age | avg run | max run
1234 | 20.1ms | 39.8ms | 4.0ms | 4.2ms # 切片约4ms(min_granularity)
1235 | 19.9ms | 39.5ms | 3.9ms | 4.1ms
...
3. 验证延迟跟踪:vruntime与优先级的关系
通过chrt命令修改进程优先级,观察vruntime增长差异:
// 1. 运行两个进程:A(nice=0,优先级120)、B(nice=10,优先级130) $ chrt -o 0 -- nice -n 0 while true; do :; done & # 进程A,PID=1234 $ chrt -o 0 -- nice -n 10 while true; do :; done & # 进程B,PID=1235 // 2. 通过/proc查看进程的vruntime(需内核支持CONFIG_SCHED_DEBUG) $ cat /proc/1234/sched | grep vruntime vruntime : 123456789012ns # 运行5ms后,vruntime增长约5ms $ cat /proc/1235/sched | grep vruntime vruntime : 123456289012ns # 运行5ms后,vruntime增长约1.6ms(5ms×335/1024≈1.6ms) // 结论:优先级高的进程(A)vruntime增长更快,后续会优先被调度
五、总结:CFS延迟与周期机制的设计思想
CFS的延迟跟踪与调度周期动态调整机制,体现了Linux内核“实用主义”的设计思想:
- 用“虚拟时间”替代“时间片”:传统时间片调度容易导致“优先级反转”(低优先级进程占用CPU,高优先级进程等待),而vruntime通过加权计算,天然实现了优先级与等待延迟的平衡,更接近“理想公平”。
- 动态周期兼顾“公平”与“效率”:当进程数较少时,用短周期保证低延迟;当进程数较多时,延长周期降低切换开销——这种“因地制宜”的调整,避免了“一刀切”的设计缺陷。
- 抢占机制保障“响应性”:周期性抢占确保进程不会超出切片时间,唤醒抢占确保交互式进程快速响应——二者结合,让CFS在服务器、桌面、嵌入式等场景下都能表现优异。
对开发者而言,理解CFS的延迟与周期机制,不仅能掌握内核调度的核心逻辑,更能学习到“如何在复杂约束下(公平、效率、响应性)设计平衡的系统”。“CFS的成功,在于它放弃了传统调度器的‘精确时间片’,转而追求‘相对公平’,这种思想上的转变,让Linux调度器的性能迈上了新台阶”。
更多推荐


所有评论(0)