本文介绍一种 比例份额(proportional-share) 调度程序,也称为 公平份额(fair-share)。
彩票调度
简介
彩票调度 的基本思想:
-
每隔一段时间,都会举行一次彩票抽奖,以确定接下来应该运行哪个进程;
-
越是应该频繁运行的进程,越是应该拥有更多地赢得彩票的机会。
关键在于如何按比例分配 CPU。
利用了 随机性(randomness),具有以下优势:
-
随机方法很轻量,几乎不需要记录任何状态;
-
随机方法很快,只要能很快地产生随机数,做出决策也就很快;
-
随机方法常常可以避免奇怪的边角情况,较传统的算法可能在处理这些情况时遇到麻烦。
彩票机制
*彩票数(ticket)*代表进程占有某个资源的份额:一个进程拥有的彩票数占总数的百分比,就是它占有资源的份额。
假设有两个进程 A 和 B,A 拥有 75 张彩票,B 拥有 25 张;我们希望 A 占用 75%的 CPU 时间,而 B 占用 25%。
彩票调度程序输出的中奖彩票:
对应的调度结果:
上面例子中,工作 B 运行了 20 个时间片中的 4 个,只是占了 20%,而不是期望的 25%。
但是,如果这两个工作运行得时间越长,它们得到的 CPU 时间比例就会越接近期望。
彩票调度还提供了一些机制,以不同且有效的方式来调度彩票。
- 彩票货币 (ticket currency)
用户可以自定义他们自己的货币, 并分给自己的不同工作;之后操作系统再将这种货币兑换为全局彩票。
假设用户 A 和用户 B 每人拥有 100 张全局彩票。用户 A 有两个工作 A1 和 A2,分给每个工作 500 个货币。用户 B 只运行一个工作,给它 10 个货币。
操作系统将进行兑换,将 A1 和 A2 拥有的 A 的货币 500 个,兑换成全局彩票 50 张;将 B1 的 10 个货币兑换成 100 张全局彩票;然后对 200 张全局彩票进行抽奖,决定哪个工作运行。
- 彩票转让 (ticket transfer)
通过转让,一个进程可以临时将自己的彩票交给另一个进程。
这种机制在 C/S 交互的场景中尤其有用,客户端进程向服务端发送消息,请求其按自己的需求执行工作,为了加速服务端的执行, 客户端可以将自己的彩票转让给服务端,从而尽可能加速服务端执行自己请求的速度。服务端执行结束后会将这部分彩票归还给客户端。
- 彩票通胀 (ticket inflation)
利用通胀,一个进程可以临时提升或降低自己拥有的彩票数量。
当然在竞争环境中,进程之间互相不信任,这种机制就没什么意义。一个贪婪的进程可能给自己非常多的彩票,从而接管机器。
但是,通胀可以用于进程之间相互信任的环境。在这种情况下,如果一个进程知道它需要更多 CPU 时间,就可以增加自己的彩票,从而将自己的需求告知操作系统,这一切不需要与任何其他进程通信。
具体实现
彩票调度的实现非常简单,只需要 一个不错的随机数生成器来选择中奖彩票 和 一个记录系统中所有进程的数据结构(一个列表),以及所有彩票的总数。
假定用列表记录进程,下面的例子中有 A、B、C 这 3 个进程,每个进程有一定数量的彩票。
代码实现如下:
// counter: used to track if we've found the winner yet
int counter = 0;
// get a value between 0 and the total of tickets
int winner = getrandom(0, totaltickets);
// current: use this to walk through the list of jobs
node_t *current = head;
// loop until the sum of ticket values is > the winner
while (current) {
counter = counter + current->tickets;
if (counter > winner)
break;
}
// now 'current' is the winner: schedule it...
如果要让这个过程更有效率,可以将列表项按照彩票数递减排序。
算法分析
我们知道彩票调度是一种“公平份额”调度算法,那么它的公平性如何呢?
假设有两个互相竞争的工作,每个工作都有相 100 张彩票,以及相同的运行时间 R。
我们希望两个工作大约同时完成,但由于彩票调度的随机性,一个工作可能先于另一个完成。
为了量化这种区别,我们定义一个不公平指标 U,将两个工作完成时刻相除得到 U 的值。
可以看出,当工作执行时间很短时,公平性非常糟糕。只有当工作执行非常多的时间片时,彩票调度算法才能得到期望的结果。
还有一个问题没有提到,那就是如何为工作分配彩票?
这是一个非常棘手的问题,系统的运行严重依赖于彩票的分配。
假设用户自己知道如何分配,则可以分给每个用户一定量的彩票,由用户自主分配给其工作。
然而还是没有具体的分配策略。对于给定的一组工作,彩票分配的问题依然没有最佳答案。
步长调度
简介
虽然随机方式可以使得调度程序的实现简单,但偶尔并不能产生正确的比例,尤其在工作运行时间很短的情况下。
出于这个原因,Waldspurger 提出 步长调度 (stride scheduling), 确定性的公平份额算法。
步长机制
步长调度的基本思想:
系统中的每个工作都有自己的 步长 (stride),与票数值成反比。比如 A、B、C 这 3 个工作的票数分别是 100、50 和 250,则可以用一个「大数」分别除以它们的票数来获得进程的步长。
如果用 10000 除以这些票数值,得到了 3 个进程的步长分别为 100、200 和 40。
每次进程运行后,会让它的计数器「行程值 (pass) 」增加它的步长,记录它的总体进展。
调度程序根据进程的步长及行程值来确定调度哪个进程:
在调度时,选择目前拥有最小行程值的进程,并在运行之后将该进程的行程值增加一个步长。
current = remove_min(queue); // pick client with minimum pass
schedule(current); // use resource for quantum
current->pass += current->stride; // compute next pass using stride
insert(queue, current); // put back into the queue
在上面例子中,3 个进程 (A、B、C) 的步长值分别为 100、200 和 40,初始行程值都为 0。因此,最初,所有进程都可能被选择执行。假设先执行 A,再执行 B,执行过程如下:
可以看出,C 运行了 5 次、A 是 2 次,B 是 1 次,正好是票数的比例——200、100 和 50。
算法分析
- 步长调度需要全局状态 (行程值)
彩票调度不需要对每个进程记录全局状态,只需要用新进程的票数更新全局的总票数即可;
- 选择步长仍然是一个棘手的问题
步长太小可能导致进程频繁地被切换,增加上下文切换的开销;步长太大可能导致某些进程长时间占用CPU而不释放,影响系统的响应性。