1、简单介绍
比例份额(proportional-share)算法基于一个简单的想法:调度程序的最终目标,是确保每个工作获得一定比例的 CPU 时间,而不是优化周转时间和响应时间。
他有一种简单的实习——彩票调度(lottery scheduling),十分简单:每隔一段时间,都会举行一次彩票抽奖,以确定接下来应该运行哪个进程。越是应该频繁运行的进程,越是应该拥有更多地赢得彩票的机会。
接下来就涉及细节问题了?如何设计调度程序来按比例分配 CPU?其关键的机制是什么?效率如何?
2、基本概念
彩票调度背后是一个非常基本的概念:彩票数(ticket)代表了进程(或用户或其他)占有某个资源的份额。一个进程拥有的彩票数占总彩票数的百分比,就是它占有资源的份额。
下面来看一个例子。假设有两个进程 A 和 B,A 拥有 75 张彩票,B 拥有 25 张。因此我们希望 A 占用 75%的 CPU 时间,而 B 占用 25%。
通过不断定时地(比如,每个时间片)抽取彩票,彩票调度从概率上(但不是确定的)获得这种份额比例。抽取彩票的过程很简单:调度程序知道总共的彩票数(在我们的例子中,有 100 张)。调度程序抽取中奖彩票,这是从 0 和 99①之间的一个数,拥有这个数对应的彩票的进程中奖。假设进程 A 拥有 0 到 74 共 75 张彩票,进程 B 拥有 75 到 99 的 25 张,中奖的彩票就决定了运行 A 或 B。调度程序然后加载中奖进程的状态,并运行它。
可以看出,彩票调度中利用了随机性,这导致了从概率上满足期望的比例,但并不能确保。但是,工作运行得时间越长,它们得到的 CPU 时间比例就会越接近期望。
3、优点
彩票调度最精彩的地方在于利用了随机性(randomness)。当你需要做出决定时,采用随机的方式常常是既可靠又简单的选择。
随机方法相对于传统的决策方式,至少有 3 点优势:
第一,随机方法常常可以避免奇怪的边角情况,较传统的算法可能在处理这些情况时遇到麻烦。例如 LRU 替换策略。虽然 LRU 通常是很好的替换算法,但在有重复序列的负载时表现非常差。但随机方法就没有这种最差情况。
第二,随机方法很轻量,几乎不需要记录任何状态。在传统的公平份额调度算法中,记录每个进程已经获得了多少的 CPU 时间,需要对每个进程计时,这必须在每次运行结束后更新。而采用随机方式后每个进程只需要非常少的状态(即每个进程拥有的彩票号码)。
第三,随机方法很快。只要能很快地产生随机数,做出决策就很快。因此,随机方式在对运行速度要求高的场景非常适用。当然,越是需要快的计算速度,随机就会越倾向于伪随机。
4、彩票机制
彩票调度还提供了一些机制,以不同且有效的方式来调度彩票。
4.1 彩票货币(ticket currency)
类似于加权。这种方式允许拥有一组彩票的用户以他们喜欢的某种货币,将彩票分给自己的不同工作。之后操作系统再自动将这种货币兑换为正确的全局彩票。
比如,假设用户 A 和用户 B 每人拥有 100 张彩票。用户 A 有两个工作 A1 和 A2,他以自己的货币,给每个工作 500 张彩票(共 1000 张)。用户 B 只运行一个工作,给它 10 张彩票(总共 10 张)。操作系统将进行兑换,将 A1 和 A2 拥有的 A 的货币 500 张,兑换成全局货币 50 张。类似地,兑换给 B1 的 10 张彩票兑换成 100 张。然后会对全局彩票货币(共 200张)举行抽奖,决定哪个工作运行。
4.2 彩票转让(ticket transfer)
通过转让,一个进程可以临时将自己的彩票交给另一个进程。这种机制在客户端/服务端交互的场景中尤其有用,在这种场景中,客户端进程向服务端发送消息,请求其按自己的需求执行工作,为了加速服务端的执行,客户端可以将自己的彩票转让给服务端,从而尽可能加速服务端执行自己请求的速度。服务端执行结束后会将这部分彩票归还给客户端。
4.3 彩票通胀(ticket inflation)
通胀可以用于进程之间相互信任的环境。在这种情况下,如果一个进程知道它需要更多 CPU 时间,就可以增加自己的彩票,从而将自己的需求告知操作系统,这一切不需要与任何其他进程通信。
5、实现
彩票调度中最不可思议的,或许就是实现简单。只需要一个不错的随机数生成器来选择中奖彩票和一个记录系统中所有进程的数据结构(一个列表),以及所有彩票的总数。
// 计数器 int counter = 0; // 随机数生成0到总数之间的值 int winner = getrandom(0, totaltickets); // 用来遍历工作列表的当前指针 node_t *current = head; while (current) { counter = counter + current->tickets; if (counter > winner) break; current = current->next; }
6、缺点
从上面的内容可以看出,虽然随机方式可以使得调度程序的实现简单(且大致正确),但偶尔并不能产生正确的比例,尤其在工作运行时间很短的情况下。
因此,我们还需要介绍一种确定性的公平分配算法——步长调度(stride scheduling)
7、步长调度(stride scheduling)
步长调度也很简单。系统中的每个工作都有自己的步长,这个值与票数值成反比。在上面的例子中,A、B、C 这 3 个工作的票数分别是 100、50 和 250,我们通过用一个大数分别除以他们的票数来获得每个进程的步长。比如用 10000 除以这些票数值,得到了 3 个进程的步长分别为 100、200 和 40。我们称这个值为每个进程的步长(stride)。每次进程运行后,我们会让它的计数器 [称为行程(pass)值] 增加它的步长,记录它的总体进展。
之后,调度程序使用进程的步长及行程值来确定调度哪个进程。基本思路很简单:当需要进行调度时,选择目前拥有最小行程值的进程,并且在运行之后将该进程的行程值增加一个步长。
在我们的例子中,3 个进程(A、B、C)的步长值分别为 100、200 和 40,初始行程值都为 0。因此,最初,所有进程都可能被选择执行。假设选择 A(任意的,所有具有同样低的行程值的进程,都可能被选中)。A 执行一个时间片后,更新它的行程值为 100。然后运行 B,并更新其行程值为 200。最后执行 C,C 的行程值变为 40。这时,算法选择最小的行程值,是 C,执行并增加为 80(C 的步长是 40)。然后 C 再次运行(依然行程值最小),行程值增加到 120。现在运行 A,更新它的行程值为 200(现在与 B 相同)。然后 C 再次连续运行两次,行程值也变为 200。此时,所有行程值再次相等,这个过程会无限地重复下去。
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
既然有了可以精确控制的步长调度算法,为什么还要彩票调度算法呢?好吧,彩票调度有一个步长调度没有的优势——不需要全局状态。假如一个新的进程在上面的步长调度执行过程中加入系统,应该怎么设置它的行程值呢?设置成 0 吗?这样的话,它就独占 CPU 了。而彩票调度算法不需要对每个进程记录全局状态,只需要用新进程的票数更新全局的总票数就可以了。因此彩票调度算法能够更合理地处理新加入的进程。