# 2.6.1 公平性

CFS的全称是Complete Fair Scheduler, 可见公平性是CFS的核心目标。所谓公平，就是调度器将CPU时间“按需分配”给每个进程。如何实现“按需分配”，以及如何简洁精炼地实现该逻辑，是CFS要考虑的首要问题。

在讨论调度器的演进历史时，我们看到不管是O(n)还是O(1)，实际上都在做这方面的工作：根据任务的优先级为其分配CPU时间。这是最直观也是最容易实现的策略，但他们判定交互式进程的逻辑过于复杂。CFS吸收了RSDL在公平性上的思想，放弃了“猜测”进程交互性的无谓尝试，根据任务的优先级来给任务分配时间，但与RSDL不同的是，CFS还放弃了“调度周期”这个概念，引入了“虚拟时间（vruntime）”来判断任务是否得到了公平对待。本章我们将深入分析CFS的算法思想，一探CFS设计者们是如何将复杂的问题抽象成一个简洁优雅的模型，并予以实现的！

## 理想模型

我们先来看在理想模型下，CFS如何有效地保证进程调度的公平性。这里我们首先明确两个概念：一是什么是理想模型；二是如何定义公平性。

理想模型：我们知道调度器的调度对象是进程（严格地说是调度实体），现实中进程有不同的种类，同一类进程也有不同的优先级，这些因素会造成进程对运行时间的需求存在差异性，从而影响调度器对公平性的定义。这里我们暂时忽略这些因素，将所有进程对时间的需求完全视为等价的。

公平性：对于理想模型而言，公平性就是每个进程得到同样多的CPU时间。即如果CPU运行的时间总量为T, 并且系统中有N个进程，那么最终每个进程得到的时间为T/N

但在实际情况下，系统是无法预先知道T的，此时调度器应该采用何种算法保证系统在任意时刻的公平性呢？在现实中，绝对的公平几乎是不可能实现的，也就是说，在任意时刻系统都存在着某种程度的不公平，即某些进程暂时获得了比其他进程更多的CPU时间。调度器要维持公平性，实际上就是要去除掉不公平性，那么每次调度时，调度器选择当前消耗CPU时间最少的进程即可。

这样我们抽象出了调度器的核心逻辑：以进程当前的实际运行时间为度量单位，记为runtime, 调度器每次调度时都选择runtime最小的进程。

该逻辑非常简单，因为此时我们只需要一个维度的指标（runtime）来度量当前系统的公平性。

## 优先级与权重

“理想模型”在现实情况中是不存在的！在现实世界中，我们必须考虑进程种类及优先级的不同所造成的对CPU时间需求的差异性。

> 前面章节我们已经详细讨论过了进程优先级的概念，这里我们只考虑CFS, 因此只讨论普通进程（Normal Process）的优先级给调度器带来的影响，即我们通常讨论的 nice 值。

CFS的公平性本质上是对CPU时间的公平分配，而优先级的加入并没有改变这一点，优先级本质上是为了区分不同进程的重要性，只要调度器能够根据每个进程的重要性的比例来分配时间，那么它就依然是公平的。例如我们有三个进程 A, B, C, 其中B与C的重要性相等，而A的重要性是他们的两倍，那么在公平的分配策略下，三个进程得到的CPU时间比例应该是：

* A: 50%
* B: 25%
* C: 25%

不同的优先级实际上代表着不同的权重，Linux中普通进程的nice值为\[-20, 19], Linux将nice值映射成了一个权重数组 `sched_prio_to_weight` ：

```
/* file: kernel/sched/core.c */
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值的关系是： `weight = 1024 / 1.25^nice`, 这样设计的目的是为了满足CFS的分配原则：进程的nice数值每变化1, 那么其获得的CPU时间将变化10%. 我们以任意两个相邻的权重来校验一下：

* A: 88761 / (88761 + 71755) = 0.55
* B: 71755 / (88761 + 71755) = 0.45

也就是说，如果进程A,B最初的nice值都是-20(此时两个进程各分配50%的时间)，那么当B变为-19后，它的CPU时间就会比A少了10%.

有了权重的概念之后，我们需要使用权重比例来刻画公平性。调度算法需要根据runtime计算出当前任务被分配到的实际比例，然后跟自己应该得到的比例相比较，差值最大的那个任务就是最应该被调度的任务。同样拿前文中A,B,C三个进程作为例子（三个任务的权重比重分别为：50%,25%,25%），假设系统已经运行了100ms, 三个任务均分了该时间片：

* A: 33.3ms, 实际比例1/3, 期望比例为1/2, 差值为：1/6
* B: 33.3ms, 实际比例1/3, 期望比例为1/4, 差值为：1/12
* C: 33.3ms, 实际比例1/3, 期望比例为1/4, 差值为：1/12

此时A的比例差值最大，说明A所分配到的时间比例偏少，调度器应该选择A作为下一个任务来执行。整个过程略显复杂，需要每个任务都记录下自己的时间比例与权重比例，而前文我们讨论理想模型时，我们只需要知道每个任务的runtime即可。

有没有什么度量方式，让我们在引入权重差异性之后，依然保持理想模型下的调度简洁性呢？答案是“虚拟时间”！

## 虚拟时间

在上一节讨论权重时，我们自然地从权重比例想到了时间比例，导致了计算逻辑稍显复杂，但如果换一种思路：通过任务的权重比例对runtime进行伸缩（scale）的话，那么我们依然可以使用“伸缩后的时间”这一个维度来观察系统的公平性。

再次回到前文中的例子：A,B,C 三个任务的权重比分别为50%, 25%, 25%, 如果100ms的总时间按照公平的方式分配，那么他们将分别得到：

* A: 50ms
* B: 25ms
* C: 25ms

我们将该时间称为墙上时间（wall time, 时钟挂在墙上，所以我们将物理时间叫着墙上时间），虽然三者的墙上时间不同，但只要将他们除以各自的权重比例，就可以得到一个归一化的数值：

* A: 50 / 50% = 100
* B: 25 / 25% = 100
* C: 25 / 25% = 100

我们可以将该结果叫着虚拟时间（virtual runtime, 简称vruntime），如果任务的墙上时间分配是公平的，那么大家的vruntime就肯定是相等的。我们可以理解为每个任务有一个自己的虚拟时钟，该时钟的走速等于墙上时钟的走速除以任务的权重比例，也就是说任务的权重越低，其虚拟时钟就走得越快，权重越高就走得越慢，例如上例中，B与C的虚拟时钟走速就是A的两倍，但只要大家的虚拟时间在数值上相等，系统在时间分配上就是公平的。

有了虚拟时间这个概念，我们依然可以维持调度算法的简洁性：调度器要维持公平性，就是要维持所有任务的虚拟时间相同，也就是说每次选择虚拟时间最小的任务进行执行即可。

既然虚拟时间这么重要，那么系统如何计算呢？上一节我们给出了任务权重的计算公式： `weight = 1024 / 1.25^nice`, 当nice=0时，任务的权重值就是1024, 1024是权重的一个基准，内核中专门使用宏 `NICE_0_LOAD` 来记录该数值, 其他所有的权重都是在该基准上进行伸缩得到的。因此，在已知墙上时间之后，虚拟时间的计算就简单了： `vruntime = wall_time * NICE_0_LOAD / weight`, 可以看到 nice=0 的任务虚拟时间就是墙上时间。为了避免浮点数计算，程序中会将数字先放大再缩小，所以最终的计算公式为： `vruntime = (wall_time * ((NICE_0_LOAD * 2^32) / weight)) >> 32`, 其中 232/weight 也叫着 invweight, 其值预先计算好了存放在数组 `sched_prio_to_wmult` 中，所以可以得到最终的虚拟时间计算公式为： `vruntime = (wall_time * NICE_0_LOAD * inv_weight) >> 32`. 之所以优化做到了这种程度，是因为计算虚拟时间是调度器中非常高频次的操作，调度器需要将任务消耗掉的墙上时间计算成虚拟时间，然后累计起来。后续讨论CFS的runqueue时我们将看到Linux使用一颗红黑树来保存所有的任务，Key值就是任务的虚拟时间，因此红黑树最左侧节点的任务就是调度器应该选择的任务。

总结起来，虚拟时间就是CFS衡量系统公平性的唯一指标。可以发现CFS在公平性这方面的思路比O(n)和O(1)都简洁，到底CFS如何基于虚拟时间完成调度，我们将在后续章节中深入讨论。
