浅析Linux内核中进程完全公平CFS调度
一、前序
目前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_SMPint (*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_SCHEDvoid (*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_SMPrq->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++;//设置当前调度进程为nextrq->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);} elseraw_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_switchprepare_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);} elseswitch_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_CTXSWspin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endifcontext_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);
相关文章:

浅析Linux内核中进程完全公平CFS调度
一、前序 目前Linux支持三种进程调度策略,分别是SCHED_FIFO 、 SCHED_RR和SCHED_NORMAL;而Linux支持两种类型的进程,实时进程和普通进程。实时进程可以采用SCHED_FIFO 和SCHED_RR调度策略;普通进程则采用SCHED_NORMAL调度策略。从…...

安装 RustDesk 服务器 (适用 Rocky Linux, CentOS, RHEL 系列发行版)
环境:Rocky Linux 9.1 1. 安装 Docker Engine 可以参考 [[linux-docker-rocky-install]] https://cc01cc.com/2023/03/02/linux-docker-rocky-install/英文可以参考官方文档 Install Docker Engine on RHEL https://docs.docker.com/engine/install/rhel/ 2. 安装…...

23种设计模式-策略模式
策略模式是一种设计模式,它允许在运行时选择算法的行为。它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户端。在本文中,我们将深入探讨策略模式的概念和实际应用&#…...

C#开发的OpenRA的游戏主界面怎么样创建
通过前面加载界面布局数据,可以把整个界面逻辑的数据加载到内存, 但是这些数据怎么显示出来,又是没有定义的。比如前面定义了多个界面的布局, 又是怎么样知道需要显示哪一个界面? 现在就来解决这个问题,其实整个游戏都是可以通过yaml文件进行配置的, 所以我们需要从yaml…...

考研还是工作?两战失败老道有话说
老道入职第一周自我介绍谈谈考研谈谈工作新的启程自我介绍 大家好!在下是一枚考研失败两次的自认为聪明能干的有点小帅的实则超级垃圾的三非名校毕业的自动化渣男。大一下就加入实验室,在实验室焊板子、画板子、培训、打比赛外加摸鱼;参加过…...

引用是否有地址的讨论的
说在前头,纯属个人理解,关于引用是否有地址,实际上并没有一个很统一的说法, C标准没有规定一个引用是否需要占用一块内存。 这里引用知乎“C 中引用是一块内存的标记,那引用本身有地址吗_百度知道 (baidu.com)”里面的…...

1、JAVA 开发环境搭建 - JDK 的安装配置
文章目录一、下载 JDK81、官网地址:[**https://www.oracle.com**](https://www.oracle.com)二、安装 JDK1、鼠标右键安装包,以管理员身份运行(无脑下一步即可)2、细节说明三、配置环境变量1、为啥要配置环境变量呢?2、原因分析3、配置环境变量…...

【Storm】【六】Storm 集成 Redis 详解
Storm 集成 Redis 详解 一、简介二、集成案例三、storm-redis 实现原理四、自定义RedisBolt实现词频统计一、简介 Storm-Redis 提供了 Storm 与 Redis 的集成支持,你只需要引入对应的依赖即可使用: <dependency><groupId>org.apache.storm…...

算法代码题——模板
文章目录1. 双指针: 只有一个输入, 从两端开始遍历2. 双指针: 有两个输入, 两个都需要遍历完3. 滑动窗口4. 构建前缀和5. 高效的字符串构建6. 链表: 快慢指针7. 反转链表8. 找到符合确切条件的子数组数9. 单调递增栈10. 二叉树: DFS (递归)]11. 二叉树: DFS (迭代)12. 二叉树: …...

CentOS 7.9汇编语言版Hello World
先下载、编译nasm汇编器。NASM汇编器官网如下图所示: 可以点图中的List进入历史版本下载网址: 我这里下载的是nasm-2.15.05.tar.bz2 在CentOS 7中,使用 wget http://www.nasm.us/pub/nasm/releasebuilds/2.15.05/nasm-2.15.05.tar.bz2下载…...

CoreData数据库探索
CoreData是什么 Core Data 是苹果公司提供的一个对象-关系映射框架(Object-Relational Mapping,ORM),用于管理应用程序的数据模型。Core Data 提供了一个抽象层,使开发人员能够使用面向对象的方式访问和操作数据&…...

FreeRTOS入门
目录 一、简介 二、堆的概念 三、栈的概念 四、从官方源码中精简出第一个FreeRTOS程序 五、修改官方源码增加串口打印 一、简介 FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、…...

JVM运行时数据区划分
Java内存空间 内存是非常重要的系统资源,是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了JAVA在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的jvm对于内存的划分方式和管理机…...

重装系统一半电脑蓝屏如何解决
小编相信大家在使用电脑或者给电脑重装系统的时候都遇到过电脑蓝屏等等故障问题。最近有用户就遇到了这样一个问题,问小编重装系统一半电脑蓝屏怎么办,那么今天小编就告诉大家重装系统一半电脑蓝屏的解决方法。 工具/原料: 系统版本&#x…...

SpringBoot(tedu)——day01——环境搭建
SpringBoot(tedu)——day01——环境搭建 目录SpringBoot(tedu)——day01——环境搭建零、今日目标一、IDEA2021项目环境搭建1.1 通过 ctrl鼠标滚轮 实现字体大小缩放1.2 自动提示设置 去除大小写匹配1.3 设置参数方法自动提示1.4 设定字符集 要求都使用UTF-8编码1.5 设置自动编…...

springboot整合redis
1.redis的数据类型,一共有5种.后面结合Jedis和redistemplate,以及单元测试junit一起验证 1)字符串 2)hash 3)列表 4)set(无序集合) 5)zset(有序集合) 2.Jedis的使用 a)引入依赖 <!--加入springboot的starter的起步依赖--><dependency><groupId>…...

【Java】Spring Boot下的MVC
文章目录Spring MVC程序开发1. 什么是Spring MVC?1.1 MVC定义1.2 MVC 和 Spring MVC 的关系2. 为什么学习Spring MVC?3. 怎么学习Spring MVC?3.1 Spring MVC的创建和连接3.1.1 创建Spring MVC项目3.1.2 RequestMapping 注解介绍3.1.3 Request…...

【项目精选】 塞北村镇旅游网站设计(视频+论文+源码)
点击下载源码 摘要 城市旅游产业的日新月异影响着村镇旅游产业的发展变化。网络、电子科技的迅猛前进同样牵动着旅游产业的快速成长。随着人们消费理念的不断发展变化,越来越多的人开始注意精神文明的追求,而不仅仅只是在意物质消费的提高。塞北村镇旅游…...

十、Spring IoC注解式开发
1 声明Bean的注解 负责声明Bean的注解,常见的包括四个: ComponentControllerServiceRepository Controller、Service、Repository这三个注解都是Component注解的别名。 也就是说:这四个注解的功能都一样。用哪个都可以。 只是为了增强程序…...

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

手敲Mybatis-反射工具天花板
历时漫长的岁月,终于鼓起勇气继续研究Mybatis的反射工具类们,简直就是把反射玩出花,但是理解起来还是很有难度的,涉及的内容代码也颇多,所以花费时间也比较浩大,不过当了解套路每个类的功能也好,…...

Java -数据结构,【优先级队列 / 堆】
一、二叉树的顺序存储 在前面我们已经讲了二叉树的链式存储,就是一棵树的左孩子和右孩子 而现在讲的是:顺序存储一棵二叉树。 1.1、存储方式 使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中。 一般只适合表示完全二叉树&a…...

Python+Qt指纹录入识别考勤系统
PythonQt指纹录入识别考勤系统如需安装运行环境或远程调试,见文章底部个人QQ名片,由专业技术人员远程协助!前言这篇博客针对<<PythonQt指纹录入识别考勤系统>>编写代码,代码整洁,规则,易读。 学…...

K_A14_004 基于STM32等单片机驱动旋转角度传感器模块 串口与OLED0.96双显示
K_A14_004 基于STM32等单片机驱动旋转角度传感器模块 串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明IIC地址/采集通道选择/时序对应程序:四、部分代码说明1、接线引脚定义1.1、STC89C52RC旋转角度传感器模块1.2、STM32F103C8T6旋转角度传感器模块五、…...

2023年全国最新机动车签字授权人精选真题及答案12
百分百题库提供机动车签字授权人考试试题、机动车签字授权人考试预测题、机动车签字授权人考试真题、机动车签字授权人证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 11.注册登记前所有机动车都应进行安全技术检验。 答案…...
Linux小黑板(10):信号
我们写在linux系统环境下写一个程序,唔,"它的功能是每隔1s向屏幕打印hello world。"这时,我们在键盘上按出"Ctrl C"后,进程会发生什么??我们清晰地看到,进程已经在我们按出"Ctrl…...

GO 语言基础语法一 (快速入门 Go 语言)
Go语言基础语法一. golang 标识符,关键字,命名规则二. golang 变量三. golang 常量四. golang 数据类型五. golang 布尔类型六. golang 数字类型七. golang 字符串1. go语言字符串字面量2. go语言字符串连接(1). 使用加号(2). 使用 fmt.Sprintf() 函数(3…...

Java高效率复习-SpringMVC[SpringMVC-2]
SpringMVC获取请求参数 SpringMVC获取请求参数的两种方式↓ 通过ServletAPI获取请求参数 将HttpServletRequest作为控制器方法的形参,此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象 通过request的API——getParameter(String s)方法来获取…...

【前端】一个更底层库-React基础知识点
目录Reat是什么?为什么要使用React类库比较容易学习,API非常少。组件内聚,容易组合原生组件和自定义组件融合渲染状态/属性驱动全局更新commonjs生态圈/工具栏完善React基础知识JSX概述JSX嵌入变量Event事件组合组合CHILDREN总结大家好&#…...

C++ 之枚举类型
文章目录参考描述枚举类型枚举类型枚举变量的声明及定义细节枚举常量的默认初始值枚举常量不可被修改赋值运算枚举常量与数据类型为枚举常量指定数据类型可选择的数据类型特殊的 Bool强枚举类型命名冲突强枚举类型参考 项目描述菜鸟教程C 枚举类型详解精通C (第九版…...