彻底搞懂Linux内核CFS调度器:让每个进程都公平运行的秘密
【免费下载链接】linux Linux kernel source tree 项目地址: https://gitcode.com/GitHub_Trending/li/linux
你是否曾经遇到过这样的情况:电脑明明配置很高,却在同时打开多个程序时变得卡顿?或者运行视频编辑软件时,后台下载任务总是抢占CPU资源导致编辑卡顿?这些问题的背后,都与操作系统的进程调度机制密切相关。在Linux系统中,CFS(Completely Fair Scheduler,完全公平调度器) 就是解决这些问题的核心机制之一。今天,我们就来深入探讨Linux内核中最重要的调度类——fair_sched_class,看看它是如何确保每个进程都能"公平地"获得CPU时间的。
读完本文后,你将能够:
- 理解CFS调度器的核心设计思想
- 掌握虚拟运行时间(vruntime)的计算方法
- 了解CFS如何在实际系统中实现进程调度
- 知道如何通过内核参数调整CFS的行为
CFS调度器的诞生:从O(1)到公平调度的演进
在CFS出现之前,Linux内核使用的是O(1)调度器。这种调度器虽然效率很高,但在处理交互式应用时存在明显缺陷,常常导致用户界面卡顿。2007年,Ingo Molnar在内核2.6.23版本中引入了CFS调度器,彻底改变了这一局面。
CFS的核心思想非常简单却又极其巧妙:让每个进程获得与其优先级成正比的CPU时间片。为了实现这一点,CFS抛弃了传统的时间片分配方式,转而采用了虚拟运行时间(vruntime) 的概念。
CFS调度器的实现代码主要集中在kernel/sched/fair.c文件中,而fair_sched_class结构体则定义了CFS调度器的各种操作方法:
const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
.dequeue_task = dequeue_task_fair,
.yield_task = yield_task_fair,
.check_preempt_curr = check_preempt_wakeup,
.pick_next_task = pick_next_task_fair,
.put_prev_task = put_prev_task_fair,
// 其他方法...
};
核心原理:虚拟运行时间(vruntime)
CFS调度器的核心在于虚拟运行时间(vruntime) 的计算。简单来说,vruntime是进程实际运行时间经过优先级加权后的结果。优先级越高的进程,其vruntime增长得越慢,从而能够获得更多的CPU时间。
vruntime的计算公式
在fair.c中,calc_delta_fair函数负责计算虚拟运行时间:
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
这里的__calc_delta函数实现了核心的加权计算:
static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
{
u64 fact = scale_load_down(weight);
u32 fact_hi = (u32)(fact >> 32);
int shift = WMULT_SHIFT;
int fs;
__update_inv_weight(lw);
if (unlikely(fact_hi)) {
fs = fls(fact_hi);
shift -= fs;
fact >>= fs;
}
fact = mul_u32_u32(fact, lw->inv_weight);
fact_hi = (u32)(fact >> 32);
if (fact_hi) {
fs = fls(fact_hi);
shift -= fs;
fact >>= fs;
}
return mul_u64_u32_shr(delta_exec, fact, shift);
}
优先级与权重的转换
Linux内核将传统的nice值(-20到+19)转换为权重,用于vruntime的计算。这个转换表定义在sched/prio.h中,优先级越高(nice值越小)的进程,其权重越大。
数据结构:红黑树中的进程调度
CFS使用红黑树(rbtree) 来组织所有可运行进程,树中的节点按照vruntime值排序。红黑树的使用使得CFS能够在O(log n)时间内找到vruntime最小的进程,也就是下一个应该运行的进程。
在fair.c中,__enqueue_entity和__dequeue_entity函数负责将进程加入和移出红黑树:
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
avg_vruntime_add(cfs_rq, se);
se->min_vruntime = se->vruntime;
se->min_slice = se->slice;
rb_add_augmented_cached(&se->run_node, &cfs_rq->tasks_timeline,
__entity_less, &min_vruntime_cb);
}
static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
rb_erase_augmented_cached(&se->run_node, &cfs_rq->tasks_timeline,
&min_vruntime_cb);
avg_vruntime_sub(cfs_rq, se);
}
pick_next_task_fair函数则负责从红黑树中选择vruntime最小的进程作为下一个运行的进程:
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 __node_2_se(left);
}
进程调度的完整流程
CFS调度器的工作流程可以分为以下几个步骤:
- 入队(enqueue):当进程变为可运行状态时,调用
enqueue_task_fair将其加入红黑树 - 选择下一个进程(pick_next):调用
pick_next_task_fair从红黑树中选择vruntime最小的进程 - 切换进程(context_switch):内核负责将CPU切换到选中的进程
- 出队(dequeue):当进程不再可运行时,调用
dequeue_task_fair将其从红黑树中移除 - 抢占检查(preempt):定期检查当前运行进程是否应该被抢占
抢占机制
CFS通过check_preempt_wakeup函数实现抢占检查。当一个新的进程被唤醒时,如果其vruntime远小于当前运行进程的vruntime,就会触发抢占:
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
struct task_struct *curr = rq->curr;
struct sched_entity *se = &curr->se, *pse = &p->se;
struct cfs_rq *cfs_rq = task_cfs_rq(curr);
if (unlikely(se->load.weight == NICE_0_LOAD &&
pse->load.weight == NICE_0_LOAD))
goto preempt;
// 检查是否需要抢占
// ...
preempt:
if (wake_flags & WF_PREEMPT)
sched_preempt_enable_no_resched();
else
resched_curr(rq);
}
CFS的关键参数与调优
CFS提供了多个可调整的参数,允许管理员根据系统需求优化调度行为。这些参数可以通过/proc/sys/kernel/目录下的文件进行修改:
主要可调参数
- sched_min_granularity_ns:最小调度粒度,默认为0.7毫秒
- sched_latency_ns:目标延迟,默认为6毫秒
- sched_wakeup_granularity_ns:唤醒抢占粒度,默认为1毫秒
- sched_tunable_scaling:调整参数的缩放方式
这些参数的定义和初始化可以在fair.c中找到:
unsigned int sysctl_sched_tunable_scaling = SCHED_TUNABLESCALING_LOG;
unsigned int sysctl_sched_base_slice = 700000ULL;
static unsigned int normalized_sysctl_sched_base_slice = 700000ULL;
如何调整这些参数
例如,要将最小调度粒度调整为1毫秒,可以执行:
echo 1000000 > /proc/sys/kernel/sched_min_granularity_ns
实际应用:CFS在不同场景下的表现
CFS调度器在各种场景下都表现出色,特别是在桌面环境中,能够很好地平衡交互式应用和后台任务。
桌面环境优化
对于桌面用户,CFS的默认设置已经相当优化。如果需要进一步提升交互式体验,可以适当减小sched_wakeup_granularity_ns,使新唤醒的进程更容易抢占CPU。
服务器环境优化
对于服务器环境,特别是CPU密集型应用,可以适当增大sched_min_granularity_ns,减少进程切换开销,提高系统吞吐量。
实时应用优化
虽然CFS主要面向通用应用,但通过调整参数,也可以在一定程度上优化实时性要求较高的应用。不过对于严格的实时需求,建议使用Linux的实时调度策略(如SCHED_FIFO和SCHED_RR)。
总结:CFS如何改变Linux的调度方式
CFS调度器通过引入虚拟运行时间和红黑树等创新技术,彻底改变了Linux内核的进程调度方式。它不仅解决了传统调度器在交互式应用上的不足,还保持了对各种工作负载的良好适应性。
CFS的核心优势在于:
- 公平性:每个进程获得与其优先级成正比的CPU时间
- 灵活性:能够适应不同类型的工作负载
- 高效性:O(log n)的调度复杂度
- 可配置性:通过参数调整适应不同场景
通过深入理解CFS的工作原理,系统管理员和开发人员可以更好地优化系统性能,应用程序开发人员也可以据此编写更高效的多线程程序。
CFS调度器的实现代码主要集中在以下文件中:
kernel/sched/fair.c:CFS调度器的主要实现kernel/sched/sched.h:调度相关的数据结构定义kernel/sched/core.c:核心调度函数
如果你对CFS的实现细节感兴趣,可以从这些文件入手,深入研究Linux内核的调度机制。
参考资料
- Linux内核源代码:
kernel/sched/目录 - 《Linux内核设计与实现》(Robert Love著)
- 《深入理解Linux内核》(Daniel P. Bovet等著)
- Linux内核文档:
Documentation/scheduler/目录
【免费下载链接】linux Linux kernel source tree 项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



