浅析Linux内核中进程完全公平CFS调度

news2024/11/21 2:30:16

一、前序

目前Linux支持三种进程调度策略,分别是SCHED_FIFO 、 SCHED_RR和SCHED_NORMAL;而Linux支持两种类型的进程,实时进程和普通进程。实时进程可以采用SCHED_FIFO 和SCHED_RR调度策略;普通进程则采用SCHED_NORMAL调度策略。从Linux2.6.23内核版本开始普通进程(采用调度策略SCHED_NORMAL的进程)采用了绝对公平调度算法,不再跟踪进程的睡眠时间,也不区分是否为交互式进程,它将所有的进程都统一对待,这就是完全公平的含义。

二、CFS基本原理概述

cfs定义了一种新调度模型,它给cfs_rq(cfs的run queue)中的每一个进程都设置一个虚拟时钟-virtual runtime(vruntime)。如果一个进程得以执行,随着执行时间的不断增长,其vruntime也将不断增大,没有得到执行的进程vruntime将保持不变。
而调度器将会选择最小的vruntime那个进程来执行。这就是所谓的“完全公平”。不同优先级的进程其vruntime增长速度不同,优先级高的进程vruntime增长得慢,所以它可能得到更多的运行机会。

三、CFS算法设计核心

CFS根据各个进程的权重分配进程运行时间。
进程的运行时间计算公式为:
分配给进程的运行时间 = 调度周期 * 当前进程权重 / 所有进程权重总和
备注:调度周期:将所有处于TASK_RUNNING态进程都调度一遍的时间,在O(1)调度算法中就是运行队列中进程运行一遍的时间。所以进程权重与分配给进程的运行时间成正比。

vruntime的计算公式为:
vruntime = 实际运行时间 * NICE_0_LOAD/ 当前进程权重 (公式3.2)

如果分配给进程的运行时间等于实际运行的时间时,将推到出另一vruntime计算公式。把公式3.2中的分配给进程的运行时间 与公式3.1中实际运行时间替换,将得出以下结果:
vruntime = (调度周期 * 当前进程权重 / 所有进程权重总和) * NICE_0_LOAD/ 当前进程权重

= 调度周期 * NICE_0_LOAD/ 所有进程权重总和
初步结论:当分配给进程的运行时间等于实际运行的时间时,虽然每个进程的权重不同,但是它们的 vruntime增长速度均相同,与权重无关。上文已述用vruntime来选择将要运行的进程,vruntime值较小表明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。如此一来既能公平选择进程,又能保证高优先级进程获得较多的运行时间。

如果分配给进程的运行时间不等于实际运行的时间时:CFS的思想就是让每个调度实体的vruntime增加速度不同,权重越大的增加的越慢,这样高优先级进程就能获得更多的cpu执行时间,而vruntime值较小者也得到执行。

每一个进程或者调度组都对应一个调度的实体,每一个进程都通过调度实体与CFS运行对列建立联系,每次进行CFS调度的时候都会在CFS运行对列红黑树中选择一个进程(vruntime值较小者)。cfs_rq代表CFS运行对列,它可以找到对应的红黑树。进程task_struct ,可以找到对应的调度实体。调度实体sched_entity对应运行对列红黑树上的一个节点。

四、CFS调度器

4.1调度器概述

现代的操作系统是多任务的操作系统,硬件的处理器核心和各种资源越来越多,CPU也是一个资源。为了保证进程合理的使用CPU资源,则需要一个管理单元,负责调度进程,由管理单元来决定下一刻应该由谁使用CPU,这里管理单元就是进程调度器。调度器可以临时分配一个任务在上面执行(单位是时间片)。进程调度器的任务就是合理分配CPU时间给运行的进程,创造一种所有进程并行运行的错觉,使得我们同时执行多个程序成为可能,可以具有各种需求的用户共享CPU。因此调度器必须在各个进程之间尽可能公平地共享CPU时间, 而同时又要考虑不同的任务优先级。调度器的一个重要目标是有效地分配 CPU 时间片,同时提供很好的用户体验。调度器的一般原理是, 按所需分配的计算能力, 向系统中每个进程提供最大的公正性, 或者从另外一个角度上说, 试图确保没有进程被亏待。

4.2调度器的结构

在目前Linux内核中,调度器分成两个层级,在进程中被直接调用的成为通用调度器或者核心调度器,它们作为一个组件和进程其它部分分开,而通用调度器和进程并没有直接关系,其通过第二层的具体的调度器类来直接管理进程。具体架构如下图:

4.2.1调度器类:

Linux内核中实现了一个调度器类的框架,其中定义了调度器应该实现的函数,每一个具体的调度器类都要实现这些函数 。

在Linux版本中(3.11.1),使用了四个调度器类:stop_sched_class、rt_sched_class、fair_sched_class、idle_sched_class,在最新的内核中又添加了一个调度类dl_sched_class。每个进程必然属于一个特定的调度器类,Linux会根据不同的需求实现不同的调度器类。各个调度器类之间具备一定的层次关系,即在通用调度器选择进程的时候,会从最高优先级的调度器类开始选择,如果通用调度器类没有可运行的进程,就选择下一个调度器类的可用进程,这样逐层递减。调度器类的定义 为sched_class的结构体。

struct sched_class {
    const struct sched_class *next;
 
    //向就绪队列添加一个进程,该操作发生在一个进程变成就绪态(可运行态)的时候。
    void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
 
    //执行enqueue_task的逆操作,在一个进程由运行态转为阻塞的时候就会发生该操作。
    void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
    
    //进程自愿放弃控制权的时候
    void (*yield_task) (struct rq *rq);
    bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
 
    void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
    
    //挑选下一个可运行的进程,发生在进程调度的时候
    struct task_struct * (*pick_next_task) (struct rq *rq);
    void (*put_prev_task) (struct rq *rq, struct task_struct *p);
 
#ifdef CONFIG_SMP
    int  (*select_task_rq)(struct task_struct *p, int sd_flag, int flags);
    void (*migrate_task_rq)(struct task_struct *p, int next_cpu);
 
    void (*pre_schedule) (struct rq *this_rq, struct task_struct *task);
    void (*post_schedule) (struct rq *this_rq);
    void (*task_waking) (struct task_struct *task);
    void (*task_woken) (struct rq *this_rq, struct task_struct *task);
 
    void (*set_cpus_allowed)(struct task_struct *p,
                 const struct cpumask *newmask);
 
    void (*rq_online)(struct rq *rq);
    void (*rq_offline)(struct rq *rq);
#endif
    //当进程的调度策略发生变化时,需要执行此函数
    void (*set_curr_task) (struct rq *rq);
    //在每次激活周期调度器时,由周期调度器调用
    void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
    //建立fork系统调用和调度器之间的关联,每次新进程建立后,就调用该函数通知调度器
    void (*task_fork) (struct task_struct *p);
 
    void (*switched_from) (struct rq *this_rq, struct task_struct *task);
    void (*switched_to) (struct rq *this_rq, struct task_struct *task);
    void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
                 int oldprio);
 
    unsigned int (*get_rr_interval) (struct rq *rq,
                     struct task_struct *task);
 
#ifdef CONFIG_FAIR_GROUP_SCHED
    void (*task_move_group) (struct task_struct *p, int on_rq);
#endif
};

4.2.2周期调度器:

资料直通车:最新Linux内核源码资料文档+视频资料

内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

周期调度器根据频率自动调用scheduler_tick函数。其主要作用就是根据进程运行时间触发调度;在进程遇到资源等待被阻塞也可以显示的调用调度器函数进行调度;另外在有内核空间返回到用户空间时,会判断当前是否需要调度,在进程对应的thread_info结构中,有一个flag,该flag字段的第二位(从0开始)作为一个重调度标识TIF_NEED_RESCHED,当被设置的时候表明此时有更高优先级的进程,需要执行调度。另外目前的内核支持内核抢占功能,在适当的时机可以抢占内核的运行。周期性调度器并不直接调度,至多设置进程的重调度位TIF_NEED_RESCHED,在返回用户空间的时候仍然由主调度器执行调度。

void scheduler_tick(void)
{
	int cpu = smp_processor_id();
	struct rq *rq = cpu_rq(cpu);
	struct task_struct *curr = rq->curr;
 
	sched_clock_tick();
 
	spin_lock(&rq->lock);
	update_rq_clock(rq);
	update_cpu_load(rq);
	curr->sched_class->task_tick(rq, curr, 0);
	spin_unlock(&rq->lock);
 
	perf_event_task_tick(curr, cpu);
 
#ifdef CONFIG_SMP
	rq->idle_at_tick = idle_cpu(cpu);
	trigger_load_balance(rq, cpu);
#endif
}

4.2.3主调度器:

主调度器是通过schedule()函数来完成进程的选择和切换。

static void __sched __schedule(void)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq *rq;
    int cpu;
 
need_resched:
    //禁止内核抢占
    preempt_disable();
    cpu = smp_processor_id();
    //获取CPU 的调度队列
    rq = cpu_rq(cpu);
    rcu_note_context_switch(cpu);
    //保存当前进程任务
    prev = rq->curr;
 
    schedule_debug(prev);
 
    if (sched_feat(HRTICK))
        hrtick_clear(rq);
 
    /*
     * Make sure that signal_pending_state()->signal_pending() below
     * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
     * done by the caller to avoid the race with signal_wake_up().
     */
    smp_mb__before_spinlock();
    raw_spin_lock_irq(&rq->lock);
 
    switch_count = &prev->nivcsw;
     /*  当内核态没有被抢占, 并内核抢占有效时
        即同时满足以下条件:
        1  该进程处于停止状态
        2  该进程没有在内核态被抢占 */
    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
        if (unlikely(signal_pending_state(prev->state, prev))) {
            prev->state = TASK_RUNNING;
        } else {
            deactivate_task(rq, prev, DEQUEUE_SLEEP);
            prev->on_rq = 0;
 
            /*
             * If a worker went to sleep, notify and ask workqueue
             * whether it wants to wake up a task to maintain
             * concurrency.
             */
            if (prev->flags & PF_WQ_WORKER) {
                struct task_struct *to_wakeup;
 
                to_wakeup = wq_worker_sleeping(prev, cpu);
                if (to_wakeup)
                    try_to_wake_up_local(to_wakeup);
            }
        }
        switch_count = &prev->nvcsw;
    }
 
    pre_schedule(rq, prev);
 
    if (unlikely(!rq->nr_running))
        idle_balance(cpu, rq);
    //通知调度器prev进程将被调度出去
    put_prev_task(rq, prev);
    //选择下一个可运行进程
    next = pick_next_task(rq);
    //清除pre的TIF_NEED_RESCHED标志
    clear_tsk_need_resched(prev);
    rq->skip_clock_update = 0;
   //如果next和当前进程不一致时可以调度
    if (likely(prev != next)) {
        rq->nr_switches++;
        //设置当前调度进程为next
        rq->curr = next;
        ++*switch_count;
        //切换进程上下文
        context_switch(rq, prev, next); /* unlocks the rq */
        /*
         * The context switch have flipped the stack from under us
         * and restored the local variables which were saved when
         * this task called schedule() in the past. prev == current
         * is still correct, but it can be moved to another cpu/rq.
         */
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
    } else
        raw_spin_unlock_irq(&rq->lock);
 
    post_schedule(rq);
  
    sched_preempt_enable_no_resched();
    if (need_resched())
        goto need_resched;
}

4.2.4上下文切换:

上下文切换主要由context_switch函数完成,主要做了两件事情:切换地址空间、切换寄存器域和栈空间。整个切换过程需要加锁和关中断,首先切换的是地址空间,mm 和active_mm分别代表调度和被调度的进程的 mm_struct,如果mm为空,则表明next是内核线程,内核线程没有自己独立的地址空间,所以其mm为null,运行的时候使用prev的active_mm即可。如果非空,则是用户进程,那么可以直接切换,这里调用switch_mm函数进行切换;如果prev为内核线程,由于其没有独立地址空间,所以需要设置其active_mm为null。最后进程切换的部分调用switch_to来切换寄存器域和栈。(switch_to是一个宏,由汇编代码实现,有能力者可深入学习)

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
    struct mm_struct *mm, *oldmm;
    //进程切换准备工作加锁和关中断,最后调用finish_task_switch
    prepare_task_switch(rq, prev, next);
    
    mm = next->mm;
    oldmm = prev->active_mm;
    /*
     * For paravirt, this is coupled with an exit in switch_to to
     * combine the page table reload and the switch backend into
     * one hypercall.
     */
    arch_start_context_switch(prev);
    //如果要执行的是内核线程
    if (!mm) {
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);
    } else
        switch_mm(oldmm, mm, next);
    //如果被调度的是内核线程
    if (!prev->mm) {
        prev->active_mm = NULL;
        rq->prev_mm = oldmm;
    }
    /*
     * Since the runqueue lock will be released by the next
     * task (which is an invalid locking op but in the case
     * of the scheduler it's an obvious special-case), so we
     * do an early lockdep release here:
     */
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
    spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif
 
    context_tracking_task_switch(prev, next);
    /* Here we just switch the register state and the stack. */
    //切换寄存器域和栈
    switch_to(prev, next, prev);
 
    barrier();
    /*
     * this_rq must be evaluated again because prev may have moved
     * CPUs since it called schedule(), thus the 'rq' on its stack
     * frame will be invalid.
     */
    finish_task_switch(this_rq(), prev);

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/387848.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【架构师】跟我一起学架构——调用链

博客昵称:架构师Cool 最喜欢的座右铭:一以贯之的努力,不得懈怠的人生。 作者简介:一名Coder,软件设计师/鸿蒙高级工程师认证,在备战高级架构师/系统分析师,欢迎关注小弟! 博主小留言…

C/C++:动态内存管理

目录 一. C/C内存分布 二. C/C动态内存管理 2.1 C语言动态内存管理 2.2 C动态内存管理 2.2.1 new/delete操作符 2.2.2 operator new与operator delete函数 2.3 new/delete的实现原理 2.4 定位new(placement - new) 2.5 new/delete和malloc/free的…

代码随想录算法训练营day49 | 动态规划 123.买卖股票的最佳时机III 188.买卖股票的最佳时机IV

day49123.买卖股票的最佳时机III1.确定dp数组以及下标的含义2.确定递推公式3.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组188.买卖股票的最佳时机IV1.确定dp数组以及下标的含义2.确定递推公式4.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组123.买卖股票的最佳时机III …

Zookeeper3.5.7版本——选举机制(非第一次启动)

目录一、ZooKeeper集群中哪些情况会进入Leader选举二、当一台机器进入Leader选举流程时,当前集群的两种状态2.1、集群中本来就已经存在一个Leader2.2、集群中确实不存在Leader三、Zookeeper中的一些概念了解3.1、SID3.2、ZXID3.3、Epoch一、ZooKeeper集群中哪些情况…

谈“对象“

面向对象一切皆对象,和java一样,各编程语言一样的思想规范类名首字母大写,和java一样创建的规范python3创建类的时候,可以不带括号,也可以带,也可以显示继承object,如果带个()空括号&#xff0c…

【网络】序列化和反序列化

🥁作者: 华丞臧. 📕​​​​专栏:【网络】 各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞收藏关注)。如果有错误的地方,欢迎在评论区指出。 推荐一款刷题网站 👉 LeetCode刷题网站 文章…

WSL2使用Nvidia-Docker实现深度学习环境自由部署

1. Win11 显卡驱动的安装 注意:WSL2中是不需要且不能安装任何显卡驱动的,它的显卡驱动完全依赖于 Win11 中的显卡驱动,因此我们只需要安装你显卡对应的 Win11 版本显卡驱动版本(必须是 Win11 版本的驱动),…

Three.js高级应用--利用Three.js+WebGL实现fbx和obj格式模型的自定义加载

通过对webgl和three.js的不断学习与实践,在三维应用场景建设过程中,利用Three.js与webgl配合可以实现大部分三维场景的应用需求,这一篇主要讲述如何利用Three.js加载已有的模型,支持的三维模型格式有.fbx和.obj,同时.o…

English Learning - L2 第1次小组纠音 [ɑː] [ɔː] [uː] 2023.2.25 周六

English Learning - L2 第1次小组纠音 [ɑː] [ɔː] [uː] 2023.2.25 周六共性问题分析大后元音 [ɑː]大后元音 [ɔː]后元音 [uː]我的发音问题后元音 [uː]大后元音 [ɑː] 和 [ɔː]纠音过程第一次第二次第三次共性问题分析 大后元音 [ɑː] 嘴唇过于松散,没…

SpringMVC文件上传、下载、国际化配置

Java知识点总结:想看的可以从这里进入 目录3.6、文件上传、下载3.6.1、文件上传3.6.2、文件下载3.7、国际化配置3.6、文件上传、下载 3.6.1、文件上传 form 表单想要具有文件上传功能,其必须满足以下 3 个条件。 form 表单的 method 属性必须设置为 p…

Spring基础知识(Spring注解开发大全)

原本xml文件写法 文件头 文件信息 配置Bean 初步修改的xml文件写法 文件头 文件信息 <context:component-scan base-package"要扫描的包"/>注解开发Bean 第一步&#xff1a;写config文件 Configuration//代表xml文件的文件头 ComponentScan(“要扫描的包”…

大型JAVA版云HIS医院管理系统源码 Saas应用+前后端分离+B/S架构

SaaS运维平台多集团多医院入驻强大的电子病历完整文档 有源码&#xff0c;有演示&#xff01; 云HIS系统技术栈&#xff1a; 1、前端框架&#xff1a;AngularNginx 2、后台框架&#xff1a;JavaSpring&#xff0c;SpringBoot&#xff0c;SpringMVC&#xff0c;SpringSecurity&…

【2022.1.3】手脱压缩壳练习(含练习exe)

【2022.1.3】手脱压缩壳练习&#xff08;含练习exe&#xff09; 文章目录【2022.1.3】手脱压缩壳练习&#xff08;含练习exe&#xff09;0、简介1、单步跟踪法&#xff08;#&#xff09;方法介绍&#xff08;0&#xff09;练习exe下载&#xff08;1&#xff09;、查看源程序&am…

精确率与召回率,ROC曲线与PR曲线

精确率与召回率&#xff0c;ROC曲线与PR曲线 在机器学习的算法评估中&#xff0c;尤其是分类算法评估中&#xff0c;我们经常听到精确率(precision)与召回率(recall)&#xff0c;ROC曲线与PR曲线这些概念&#xff0c;那这些概念到底有什么用处呢&#xff1f; 首先&#xff0c…

Linux系统GPIO应用编程

目录应用层如何操控GPIOGPIO 应用编程之输出GPIO 应用编程之输入GPIO 应用编程之中断在开发板上测试GPIO 输出测试GPIO 输入测试GPIO 中断测试本章介绍应用层如何控制GPIO&#xff0c;譬如控制GPIO 输出高电平、或输出低电平。应用层如何操控GPIO 与LED 设备一样&#xff0c;G…

【办公类05-03】Python批量修改文件名前面的序号(已有的序号错了,需要改成正确的号码)

背景需求下载教程&#xff0c;手动输入编号&#xff0c;有一个编号错误&#xff0c;导致后面所有编号都错了。30实际是29&#xff0c;以此类推怎样才能快速修改编号数字&#xff1f;前期考虑到可能要改编号&#xff0c;所以在每个编号后面加“ ”&#xff08;空格&#xff09;&…

python版协同过滤算法图书管理系统

基于协同过滤算法的图书管理系统 一、简介&#xff08;v信&#xff1a;1257309054&#xff09; ​ 本系统基于推荐算法给用户实现精准推荐图书。 ​ 根据用户对物品或者信息的偏好&#xff0c;发现物品或者内容本身的相关性&#xff0c;或者是发现用户的相关性&#xff0c;然…

Typora上传文档图片链接失效的问题+PicGo布置图床在Github

文章目录typora图片链接失效原因PicGO开源图床布置先配置Github2.1先创建新仓库、用于存放图片2.2生成一个token&#xff0c;用picGo访问github3.下载picGo,并进行配置3.1 配置v4.1typora图片链接失效原因 因为你是保存在本地的&#xff0c;因此图片是不能访问&#xff0c;可以…

laravel 邮件发送

配置 Laravel 的邮件服务可以通过 config/mail.php 配置文件进行配置。 邮件中的每一项都在配置文件中有单独的配置项&#xff0c;甚至是独有的「传输方式」&#xff0c;允许你的应用使用不同的邮件服务发送邮件 mailers > [smtp > [transport > smtp,host > env(M…

【超级猜图案例上半部分的实现 Objective-C语言】

一、超级猜图这么一个案例: 1.实现之后的效果是这样的: 1)中间有一个图片,点一下,能放大,背景变半透明的黑色: 2)再点一下图片,或者点周围黑色的阴影,图片回归原状, 3)右边有一个“大图”按钮,点一下,实现跟点图片一样的效果, 4)左边有一个“提示”按钮,点…