CFS-完全公平调度器

调度器
  调度器是一个操作系统的核心部分。可以比作是 CPU 时间的管理员。调度器主要负责选择某些就绪的进程来执行。不同的调度器根据不同的方法挑选出最适合运行的进程。目前 Linux 支持的调度器就有 RT scheduler、Deadline scheduler、CFS scheduler 及 Idle scheduler等。

1. 概念

  CFS 即Completely Fair Scheduler,顾名思义,完全公平调度器。CFS 作为主线调度器之一,也是最典型的 O(1) 调度器之一,在 Linux2.6.23 内核版本中引入,它最大的特点就是能保证任务调度的公平性。

  • O(n) 调度:内核调度算法理解起来简单:在每次进程切换时,内核依次扫描就绪队列上的每一个进程,计算每个进程的优先级,再选择出优先级最高的进程来运行;尽管这个算法理解简单,但是它花费在选择优先级最高进程上的时间却不容忽视。系统中可运行的进程越多,花费的时间就越大,时间复杂度为O(n)
  • O(1) 调度:其基本思想是根据进程的优先级进行调度。进程有两个优先级,一个是静态优先级,一个是动态优先级(每一个 CPU 都有一个全局就绪队列-红黑树)。静态优先级是用来计算进程运行的时间片长度,调度器则是每次都选取动态优先级最高的进程运行。CFS scheduler 每次都挑选红黑树最左边的节点作为下一个要运行的任务,这个节点是“缓存的”——由一个特殊的指针指向;不需要进行O(logn)遍历来查找。也因此,CFS 搜索的时间是 O(1)。
静态优先级(先计算进程分配时间片长度)-> 动态优先级(调度器选择优先级高的进程运行)

2. 进程分配时间计算

  CFS 能在真实硬件上模拟出一种“公平的、精确的任务多处理CPU”。

  CFS 调度器和以往的调度器不同之处在于没有时间片的概念,而是分配 cpu 使用时间的比例。例如:2个相同优先级的进程在一个 cpu 上运行,那么每个进程都将会分配 50% 的 cpu 运行时间。这就是要实现的公平。

2.1 进程优先级怎么计算?

  但是现实却并非如此,有些任务优先级就是比较高。那么 CFS 调度器的优先级是如何实现的呢?

  首先,我们引入权重的概念,权重代表着进程的优先级。各个进程之间按照权重的比例分配cpu时间。例如:2个进程A和B。A的权重是1024,B的权重是1277。那么A获得cpu的时间比例是1024/(1024+1277) =45%。B进程获得的cpu时间比例是2048/(1024+1277)=55%。我们可以看出,权重越大分配的时间比例越大,相当于优先级越高。

分配给进程的时间计算公式

分配给进程的时间 = 调度延迟 * 进程的权重/就绪队列(runqueue)所有进程权重之和

进程权重应该怎样计算呢?

  根据进程的 nice 值进行索引的,其实和权重是一一对应的关系。nice 值就是一个具体的数字,取值范围是[-20, 19]。数值越小代表优先级越大,同时也意味着权重值越大,nice值和权重之间可以互相转换。内核提供了一个表格转换nice值和权重。

nice值共有40个,与权重之间,每一个nice值相差10%左右。
const int sched_prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
}; 

举个例子,一个进程普通优先级进程,它的 nice 值是0,那么它的权重就是 1024。

2.2 调度延迟

  什么是调度延迟?调度延迟就是保证每一个可运行进程都至少运行一次的时间间隔。例如,每个进程都运行10ms,系统中总共有2个进程,那么调度延迟就是 20ms;如果有5个进程,那么调度延迟就是 50ms。

  那么 CFS 里调度延时怎么确定呢?

当进程数 < sched_nr_latency(8)时,值固定的为 sysctl_sched_latency(6ms)
    
当进程数 > sched_nr_latency(8)时,为进程数乘以 sched_min_granularity_ns(0.75ms)

分配给进程的时间 == 进程真实运行时间吗?

  我们来分析这个案例,当只有 3 个进程参与调度,优先级 的nice = 0,那么 3个进程分配的时间都是一样的。6 * 1024 /(1024+1024+1024)= 2ms,也就是说 cpu 没 2ms 会切换执行下一个进程任务。但实际情况要更复杂,因为执行调度代码写在时间中断的处理函数的代码中,也就是仅仅当发生时间中断的时候,我们才会去检查时间片是否用尽,是否应该进行进程切换了。

那么时间中断多久发生一次呢?

  这就是tick周期,这个取决于硬件频率,取决与我们的CPU,X86架构一般支持100 Hz, 250 Hz和1000 Hz,对应的间隔分别是10ms, 4ms和1ms。

  我们可以通过grep CONFIG_HZ /boot/config-$(uname -r)来查看,本机是tick周期:

在这里插入图片描述

  也就是每执行 4ms 发生一次时间中断 ,才检查到进程 CPU时间片耗尽,才进行进程任务切换。

3. CFS 调度器如何选择进程?

  CFS 使用红黑树结构,来存储要调度的任务队列。每个节点代表了一个要调度的任务,节点的 key 即为虚拟时间(vruntime),虚拟时间由这个任务的运行时间计算而来;key越小,也就是 vruntime 越小的话,红黑树对应的节点就越靠左。CFS 调度器通过 vruntime 来保证进程任务调度公平。

在这里插入图片描述

vruntime 的计算公式

vruntime += 实际运行时间 * 1024 / 进程权重

  假设只有2个进程 A 和 B,A 的权重是 1024,B 的权重是 1277;A、B 的实际运行时间都是一个时间分片。

A 时间分片 = 6 * 1024 /1024 + 1277; 
A vruntime = 时间分片 * 1024 / 1024 = 6 * 1024 /(所有进程权重之和); # 这里假设每个进程任务都执行完时间分片,没有IO阻塞

B 时间分片 = 6 * 1277 /1024 + 1277;
B vruntime = 时间分片 * 1024 / 1277 = 6 * 1024 /(所有进程权重之和); # 这里假设每个进程任务都执行完时间分片,没有IO阻塞

  我们可以看出尽管 A 和 B 进程的权重值不一样,但是计算得到的 虚拟时间是一样的。CFS 调度器记录每一个进程的 vruntime,保证每个进程获取 CPU 执行时间的公平。但当哪个进程 vruntime 最少,应该让哪个进程运行。

新创建进程 vruntime = 0?

  新创建的进程实际运行实际 = 0,vruntime = 0?vruntime 并不是无限小的,有一个最小值来限定。假如新进程的 vruntime 初值为 0 的话,比老进程的值小很多,那么它在相当长的时间内都会保持抢占 CPU 的优势,老进程就要饿死了,这显然是不公平的。

  CFS 是这样做的:每个 CPU 的运行队列 cfs_rq 都维护一个 min_vruntime 字段,记录该运行队列中所有进程的 vruntime 最小值,新进程的初始 vruntime 值就以它所在运行队列的 min_vruntime 为基础来设置,与老进程保持在合理的差距范围内。

CFS的唤醒抢占特性:

  对于 sleep/IO 这类的操作,由于相对来说并不占用过多资源,vruntime 并不会被马上结算,仍会保持最初的 vruntime。在该进程被重新唤醒之后会重新计算 vruntime(而不会累加 vruntime += vruntime;),vruntime 将取当期线程的 vruntime 和当前系统内最小 vruntime 阈值这两个值中的最大值;它在醒来的时候有能力抢占CPU是大概率事件,这也是 CFS 调度算法的本意,即保证交互式进程的响应速度

  想象一下当你执行每一个敲击键盘、移动鼠标等交互操作的时候,对于系统来说,这就是来了个新任务->运行时间为 0->vruntime 为最小 vruntime->被放到调度任务队列红黑树的最左节点->最左节点通过一个特殊的指针指向,且该指针已被缓存

  • 对于 CPU 计算密集型作业,将运行很长时间,因此它将逐渐移到最右侧
  • 对于 IO 计算密集型作业,会运行很短的时间,唤醒后会重新 vruntime(不会累加)因此它只会稍微向右移动

也就是在就绪队列中,被唤醒的进程(IO)比 一直进行计算的进程(CPU)更容易拿到 CPU 执行权,保证交互式进程的响应速度

引用:
https://blog.csdn.net/weixin_42269817/article/details/108229723
https://zhuanlan.zhihu.com/p/83795639?ivk_sa=1024320u
https://zhuanlan.zhihu.com/p/372441187
https://www.csdn.net/tags/MtTaIgxsNzAzMzAtYmxvZwO0O0OO0O0O.html

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值