彻底搞懂Linux内核CFS调度器:让每个进程都公平运行的秘密

彻底搞懂Linux内核CFS调度器:让每个进程都公平运行的秘密

【免费下载链接】linux Linux kernel source tree 【免费下载链接】linux 项目地址: 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调度器的工作流程可以分为以下几个步骤:

  1. 入队(enqueue):当进程变为可运行状态时,调用enqueue_task_fair将其加入红黑树
  2. 选择下一个进程(pick_next):调用pick_next_task_fair从红黑树中选择vruntime最小的进程
  3. 切换进程(context_switch):内核负责将CPU切换到选中的进程
  4. 出队(dequeue):当进程不再可运行时,调用dequeue_task_fair将其从红黑树中移除
  5. 抢占检查(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/目录下的文件进行修改:

主要可调参数

  1. sched_min_granularity_ns:最小调度粒度,默认为0.7毫秒
  2. sched_latency_ns:目标延迟,默认为6毫秒
  3. sched_wakeup_granularity_ns:唤醒抢占粒度,默认为1毫秒
  4. 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的核心优势在于:

  1. 公平性:每个进程获得与其优先级成正比的CPU时间
  2. 灵活性:能够适应不同类型的工作负载
  3. 高效性:O(log n)的调度复杂度
  4. 可配置性:通过参数调整适应不同场景

通过深入理解CFS的工作原理,系统管理员和开发人员可以更好地优化系统性能,应用程序开发人员也可以据此编写更高效的多线程程序。

CFS调度器的实现代码主要集中在以下文件中:

  • kernel/sched/fair.c:CFS调度器的主要实现
  • kernel/sched/sched.h:调度相关的数据结构定义
  • kernel/sched/core.c:核心调度函数

如果你对CFS的实现细节感兴趣,可以从这些文件入手,深入研究Linux内核的调度机制。

参考资料

  1. Linux内核源代码:kernel/sched/目录
  2. 《Linux内核设计与实现》(Robert Love著)
  3. 《深入理解Linux内核》(Daniel P. Bovet等著)
  4. Linux内核文档:Documentation/scheduler/目录

【免费下载链接】linux Linux kernel source tree 【免费下载链接】linux 项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值