调度器
调度器是一个操作系统的核心部分。可以比作是 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