linux内核分析——CFS(完全公平调度算法)

 

1.1 CFS原理

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

1.2 CFS基本设计思路

CFS思路很简单,就是根据各个进程的权重分配运行时间(权重怎么来的后面再说)。
进程的运行时间计算公式为:
分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和   (公式1)
    调度周期很好理解,就是将所有处于TASK_RUNNING态进程都调度一遍的时间,差不多相当于O(1)调度算法中运行队列和过期队列切换一次的时间(对O(1)调度算法看得不是很熟,如有错误还望各位大虾指出)。举个例子,比如只有两个进程A, B,权重分别为1和2,调度周期设为30ms,那么分配给A的CPU时间为:30ms * (1/(1+2)) = 10ms;而B的CPU时间为:30ms * (2/(1+2)) = 20ms。那么在这30ms中A将运行10ms,B将运行20ms。
    公平怎么体现呢?它们的运行时间并不一样阿?
其实公平是体现在另外一个量上面,叫做virtual runtime(vruntime),它记录着进程已经运行的时间,但是并不是直接记录,而是要根据进程的权重将运行时间放大或者缩小一个比例。
我们来看下从实际运行时间到vruntime的换算公式
vruntime = 实际运行时间 * 1024 / 进程权重 。 (公式2)
    为了不把大家搞晕,这里我直接写1024,实际上它等于nice为0的进程的权重,代码中是NICE_0_LOAD。也就是说,所有进程都以nice为0的进程的权重1024作为基准,计算自己的vruntime增加速度。还以上面AB两个进程为例,B的权重是A的2倍,那么B的vruntime增加速度只有A的一半。现在我们把公式2中的实际运行时间用公式1来替换,可以得到这么一个结果:
vruntime = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重 = 调度周期 * 1024 / 所有进程总权重 
看出什么眉目没有?没错,虽然进程的权重不同,但是它们的 vruntime增长速度应该是一样的 ,与权重无关。好,既然所有进程的vruntime增长速度宏观上看应该是同时推进的,
那么就可以用这个vruntime来选择运行的进程,谁的vruntime值较小就说明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间。这就是CFS的主要思想了。

或者可以这么理解:CFS的思想就是让每个调度实体(没有组调度的情形下就是进程,以后就说进程了)的vruntime互相追赶,而每个调度实体的vruntime增加速度不同,权重越大的增加的越慢,这样就能获得更多的cpu执行时间。

    再补充一下权重的来源,权重跟进程nice值之间有一一对应的关系,可以通过全局数组prio_to_weight来转换,nice值越大,权重越低。

1.3 CFS数据结构

介绍代码之前先介绍一下CFS相关的结构
第一个是调度实体sched_entity,它代表一个调度单位,在组调度关闭的时候可以把他等同为进程。每一个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。那么所有的sched_entity怎么组织在一起呢?红黑树。所有的sched_entity以vruntime为key(实际上是以vruntime-min_vruntime为key,是为了防止溢出,反正结果是一样的)插入到红黑树中,同时缓存树的最左侧节点,也就是vruntime最小的节点,这样可以迅速选中vruntime最小的进程。
    注意只有等待CPU的就绪态进程在这棵树上,睡眠进程和正在运行的进程都不在树上。

1.4 Vruntime溢出问题

之前说过红黑树中实际的作为key的不是vruntime而是vruntime-min_vruntime。min_vruntime是当前红黑树中最小的key。这是为什么呢,我们先看看vruntime的类型,是usigned long类型的,再看看key的类型,是signed long类型的,因为进程的虚拟时间是一个递增的正值,因此它不会是负数,但是它有它的上限,就是unsigned long所能表示的最大值,如果溢出了,那么它就会从0开始回滚,如果这样的话,结果会怎样?结果很严重啊,就是说会本末倒置的,比如以下例子,以unsigned char说明问题:

unsigned char a = 251,b = 254;

b += 5;//到此判断a和b的大小

看看上面的例子,b回滚了,导致a远远大于b,其实真正的结果应该是b比a大8,怎么做到真正的结果呢?改为以下:

unsigned char a = 251,b = 254;

b += 5;

signed char c = a - 250,d = b - 250;//到此判断c和d的大小

结果正确了,要的就是这个效果,可是进程的vruntime怎么用unsigned long类型而不处理溢出问题呢?因为这个vruntime的作用就是推进虚拟时钟,并没有别的用处,它可以不在乎,然而在计算红黑树的key的时候就不能不在乎了,于是减去一个最小的vruntime将所有进程的key围绕在最小vruntime的周围,这样更加容易追踪。运行队列的min_vruntime的作用就是处理溢出问题的。

1.5 组调度

关于组调度,详见:《linux组调度浅析 》。简单来说,引入组调度是为了实现做一件事的一组进程与做另一件事的另一组进程的隔离。每件“事情”各自有自己的权重,而不管它需要使用多少进程来完成。在cfs中,task_group和进程是同等对待的,task_group的优先级也由用户来控制(通过cgroup文件cpu.shares)。
实现上,task_group和进程都被抽象成schedule_entity(调度实体,以下简称se),上面说到的vruntime、load、等这些东西都被封装在se里面。而task_group除了有se之外,还有cfs_rq。属于这个task_group的进程就被装在它的cfs_rq中(“组”不仅是一个被调度的实体,也是一个容器)。组调度引入以后,一系列task_group的cfs_rq组成了一个树型结构。树根是cpu所对应的cfs_rq(也就是root group的cfs_rq)、树的中间节点是各个task_group的cfs_rq、叶子节点是各个进程。
在一个task_group的两头,是两个不同的世界,就像《盗梦空间》里不同层次的梦境一样。

以group-1为例,它所对应的se被加入到父组(cpu_rq)的cfs_rq中,接受调度。这个se有自己的load(由对应的cpu.shares文件来配置),不管group-1下面有多少个进程,这个load都是这个值。父组看不到、也不关心group-1下的进程。父组只会根据这个se的load和它执行的时间来更新其vruntime。当group-1被调度器选中后,会继续选择其下面的task-11或task-12来执行。这里又是一套独立的体系,task-11与task-12的vruntime、load、等这些东西只影响它们在group-1的cfs_rq中的调度情况。树型结构中的每一个cfs_rq都是独立完成自己的调度逻辑。不过,从cpu配额上看,task_group的配额会被其子孙层层瓜分。
    例如上图中的task-11,它所在的group-1对应se的load是8,而group-1下两个进程的load是9和3,task-11占其中的3/4。于是,在group-1所对应的cfs_rq内部看,task-11的load是9,而从全局来看,task-11的load是8*3/4=6。而task_group下的进程的时间片也是这样层层瓜分而来的,比如说group-1的cfs_rq下只有两个进程,计算得来的调度延迟是20ms。但是task-11并非占其中的3/4(15ms)。因为group-1的se的load占总额的8/(8+3+5)=1/2,所以task-11的load占总额的1/2*3/4=3/8,时间片是20ms*3/8=7.5ms。
这样的瓜分有可能使得task_group里面的进程分得很小的时间片,从而导致频繁re-schedule。不过好在这并不影响task_group外面的其他进程,并且也可以尽量让task_group里面的进程在每个调度延迟内都执行一次。
    cfs曾经有过时间片不层层瓜分的实现,比如上图中的task-11,时间片算出来是15ms就是15ms,不用再瓜分了。这样做的好处是不会有频繁的re-schedule。但是task_group里的进程可能会很久才被执行一次。瓜分与不瓜分两种方案的示例如下(还是继续上图的例子,深蓝色代表task-11、浅蓝色是task-12,空白是其他进程):

     两种方案好像很难说清孰优孰劣,貌似cfs也在这两种方案间纠结了好几次。
在进程用完其时间片之前,有可能它所在的task_group的se先用完了时间片,而被其父组re-schedule掉。这种情况下,当这个task_group的se再一次被其父组选中时,上次得到执行、且尚未用完时间片的那个进程将继续运行,直到它用完时间片。(cfs_rq->last会记录下这个尚未用完时间片的进程。)

1.6 CFS小结

CFS还有一个重要特点,即调度粒度小。CFS之前的调度器中,除了进程调用了某些阻塞函数而主动参与调度之外,每个进程都只有在用完了时间片或者属于自己的时间配额之后才被抢占。而CFS则在每次tick都进行检查,如果当前进程不再处于红黑树的左边,就被抢占。在高负载的服务器上,通过调整调度粒度能够获得更好的调度性能。

深入Linux 内核架构之 CFS的更多相关文章

  1. 搭建《深入Linux内核架构》的Linux环境

    作者 彭东林 pengdonglin137@163.com 软件 Host: Ubuntu14.04 64 Qemu 2.8.0 Linux 2.6.24 busybox 1.24.2 gcc 4.4 ...

  2. Linux内核架构与底层--读书笔记

    linux中管道符"|"的作用 命令格式:命令A|命令B,即命令1的正确输出作为命令B的操作对象(下图应用别人的图片) 1. 例如: ps aux | grep "tes ...

  3. 深入Linux内核架构——进程管理和调度(上)

    如果系统只有一个处理器,那么给定时刻只有一个程序可以运行.在多处理器系统中,真正并行运行的进程数目取决于物理CPU的数目.内核和处理器建立了多任务的错觉,是通过以很短的间隔在系统运行的应用程序之间不停 ...

  4. 【深入理解Linux内核架构】3.3 页表

    页表:用于建立用户进程空间的虚拟地址空间和系统物理内存(内存.页帧)之间的关联. 向每个进程提供一致的虚拟地址空间. 将虚拟内存页映射到物理内存,因而支持共享内存的实现. 可以在不增加物理内存的情况下 ...

  5. Linux内核入门到放弃-页面回收和页交换-《深入Linux内核架构》笔记

    概述 可换出页 只有少量几种页可以换出到交换区,对其他页来说,换出到块设备上与之对应的后备存储器即可,如下所述. 类别为 MAP_ANONYMOUS 的页,没有关联到文件,例如,这可能是进程的栈或是使 ...

  6. Linux内核入门到放弃-网络-《深入Linux内核架构》笔记

    网络命名空间 struct net { atomic_t count; /* To decided when the network * namespace should be freed. */ a ...

  7. 深入Linux内核架构第一章笔记

    1. Linux是多任务系统, 支持并发执行若干进程,系统同时真正运行的进程数目不超过CPU的数量,因此内核会按照时间间隔在不同进程之间切换. 2.确定那个进程运行多长时间的过程称为调度. 3.内核启 ...

  8. [Wolfgang Mauerer] 深入linux 内核架构 第一章 概述

    作为Linux开发爱好者,从事linux 开发有两年多时间.做过bsp移植,熟悉u-boot代码执行流程:看过几遍<linux 设备驱动程序开发>,分析过kernel启动流程,写过驱动,分 ...

  9. 深入Linux内核架构——简介与概述

    一.内核的任务 纯技术层面上,内核是硬件与软件的之间的一个中间层.作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址. 从应用程序视角上看,内核可以被认为是一台增强 ...

随机推荐

  1. Linux 启动、停止、重启jar包脚本

    转至:https://www.cnblogs.com/foolash/p/13824647.html startOrStropJar.sh #!/bin/bash #这里可替换为你自己的执行程序,其他 ...

  2. 『无为则无心』Python日志 — 66、将日志信息保存到文件中

    目录 1.把日志信息保存到文件中 2.拓展 (1)观察代码 (2)提出问题 (3)问题说明 1.把日志信息保存到文件中 代码如下所示: """ logging模块是Pyt ...

  3. WPS二级标题链接到一级标题

    WPS二级标题链接到一级标题,即2后出现2.1 2.2而不是1.3 1.4什么的 样式中的编号什么的都不用动,默认即可,关键在于这些多级标题是否选择了同一个编号方式 WPS中,只需要将它们的编号选择为 ...

  4. 解决select 下拉框运行时总会有一个空值(空选项)的问题

    项目中用到很多下拉选项都会多出一个空选项,如图运行结果 总会有一个空值出现,解决办法如下: 效果如下: 添加图中框选的代码即可,我是在Angularjs中使用的,在不用框架的情况下: <opti ...

  5. 【论文考古】量化SGD QSGD: Communication-Efficient SGD via Gradient Quantization and Encoding

    D. Alistarh, D. Grubic, J. Li, R. Tomioka, and M. Vojnovic, "QSGD: Communication-Efficient SGD ...

  6. Dubbo是什么?核心总结

    Dubbo --是SOA架构的具体的实现框架! 2.1 Dubbo简介 Apache Dubbo是一款高性能的Java RPC框架.官网地址:[http://dubbo.apache.org] dub ...

  7. vue项目在nginx中不能刷新问题

    修改nginx配置文件为 server { listen 80; server_name www.vue.com; root html/xxx/dist/; client_max_body_size ...

  8. SP20173题解

    膜拜 rqy. 题意: 求: \[\sum_{i=1}^n \sigma_0(i^2) \] 首先我们知道 \(\sigma_0((p^k)^2)=2 \times k + 1=k+(k+1)=\si ...

  9. 使用socat反向Shell多台机器

    原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 场景 很多时候,我们需要批量操作多台机器,业界一般使用Ansible来实现,但使用Ansible来操作多台机器的前提是需 ...

  10. git 回滚方式

    git push 命用于从将本地的分支版本上传到远程并合并. 命令格式如下: git push <远程主机名> <本地分支名>:<远程分支名> 如果本地分支名与远程 ...