CFS 调度算法的基本原理

调度单元有三种状态:

  1. 睡眠:不在 CPU 运行队列里
  2. PENDING:调度单元被唤醒,比如网络数据包到达,IO ready等,进程被唤醒,进入运行队列,但是还没得到 CPU 时间片
  3. 运行:调度单元得到 CPU 控制权,开始运行

调度延时其实就是指进程被唤醒,进入运行队列到得到 CPU 时间片之间的等待时间,也就是处于 PENDING 状态的时间

Linux 通过一个红黑树来维护所有进程的状态,每个 CPU 都会有一个运行队列,管理所有进程和进程组【注意,进程组也是内核的一个基本的调度单元】

每个调度单元都会有一个 vruntime 的属性,用来记录当前调度单元以运行的虚拟时间

为了简化问题,我们只考虑单个 CPU 上的运行队列的调度逻辑,跨 CPU 还涉及时间片重算等更复杂的逻辑,暂不描述

CFS 调度的基本如下:

  1. 从 CPU 运行队列选择一个 vruntime 最小的进程 → 公平性原则,目的是要确保所有进程都尽可能的得到调度
  2. 进程运行完毕之后,更新vruntime,重新放入运行队列里等待下次被调度

CFS 为了实现完全公平的目的,在很多场景下,都牺牲了优先级

1. 最小时间片

CFS 有一个调度周期的概念,公平的要义在于:在一个调度周期内,确保每个调度单元都能得到调度的机会

假设一个 CPU 运行队列里有两个进程 P1和P2,假设 P1和P2 都是 CPU-bound 进程,那么各自都能得到 12ms 的运行时间

如果再多一个进程 P3,那每个周期内,每个进程只能拿到4ms的时间片

再多一个进程的时候是这个样子的

但是时间片不可能会按照进程数无限的切分下去,内核有一个最小值,一旦小于这个值,时间周期就会被延长,确保每个进程都能得到一次调度的机会

也就是说,调度周期是这么决定的

period = max(sched_latency_ns, sched_min_granularity_ns * nr_tasks)

sched_latency_ns 当前我们服务器默认是24ms

sched_min_granularity_ns 默认是4ms

你可以想象,假如一个 CPU 上跑了1000多个进程,其中只有一个高优先级的进程,最坏情况下,高优先级进程可能得等 999 * 4ms = 4s 才能得到下一次调度的机会

实际情况可能远没有这么夸张,但是也相当夸张,调度延时在100ms级别是非常常见的

实际情况没有这么夸张的几个原因:

  1. 调度域非常大,容器进程都分散到所有 CPU 上面去了,机器利用率不高,风险降低了很多
  2. 进程绝大部分都不会执行完一个时间片,因为各种 IO 操作,sleep 系统调用,锁操作,都会导致进程进入睡眠状态,从而让出 CPU

2. 睡眠补偿

如果一个进程一直在睡眠,那么它的 vruntime 是非常小的,当睡眠中的进程被唤醒时,基于 CFS 的调度逻辑,会一直持续运行当前进程,知道 vruntime 不是最小的时候,才会选择下一个进程来调度

理论上来说,其实睡眠中的进程,不应该计算 vruntime,因为这部分睡眠的时间,是这个进程放弃运行的时间

因此,内核为了解决 sleep 进程获得过长时间的问题,增加了一个阈值限制,当进程被唤醒时,取当前运行队列的最小vruntime,并 + 上一个偏移量,这个偏移量默认是 1/2 个调度周期,12ms

也就是说,当进程被唤醒之后,至少会得到 4ms 完整的运行时间,这个时间不可被中断,除非进程主动让出 CPU

static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial) { u64 vruntime = cfs_rq->min_vruntime;   /* * The 'current' period is already promised to the current tasks, * however the extra weight of the new task will slow them down a * little, place the new task so that it fits in the slot that * stays open at the end. */ if (initial && sched_feat(START_DEBIT)) vruntime += sched_vslice(cfs_rq, se);   /* sleeps up to a single latency don't count. */ if (!initial) { unsigned long thresh = sysctl_sched_latency;   /* * Halve their sleep time's effect, to allow * for a gentler effect of sleepers: */ if (sched_feat(GENTLE_FAIR_SLEEPERS)) thresh >>= 1;   vruntime -= thresh; }   /* ensure we never gain time by being placed backwards. */ se->vruntime = max_vruntime(se->vruntime, vruntime); }

极端情况下,我们可以模拟一个进程,不断的睡眠和被唤醒,就可以触发这个逻辑

3. 最短保证运行时间

CFS 还有一个机制,就是 sched_min_granularity_ns ,主要是为了保证时间片机制不会被无限分割下去的一个防御机制

确保每个可调度的进程,都有一个足够的 CPU 时间片

在 sched_min_granularity_ns 时间内,除非进程主动让出 CPU,否则其他所有进程都必须在队列里等待

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注