转自 http://hi.baidu.com/mychaoyue2011/blog/item/6df45895c3d63243d0135e01.html
本报告分析Linux 2.6内核的调度策略及算法。Linux内核支持多CPU,每个CPU执行类似的策略,但在必要时(例如CPU负载不平衡等时刻),还会进行更高层的CPU间调度,由此还会引出CPU间的同步等问题。为了简化问题,此处仅讨论单CPU的情况。
Linux中的调度对象为task,此处仍称做进程,每个task有如下五个状态:
1. TASK_RUNNING: 这里表示进程没有被阻塞,即READY和RUNNING的结合体。至于具体是READY还是 RUNNING,在需要的时候可以做如下判断:调度程序取得该进程的进程状态为TASK_RUNNING且其所在CPU的current_task就是该进程,则认定他是RUNNING,否则就是READY。需要做如上判断的情况其实并不多,所以统以TASK_RUNNING描述便已足够。
2. TASK_INTERRUPTIBLE: 进程处于BLOCK状态且可以被SIGNAL唤醒
3. TASK_UNINTERRUPTIBLE: 进程处于BLOCK状态但不能被SIGNAL唤醒
4. TASK_ZOMBIE: 进程已经结束,但描述符还没有被回收。
5. TASK_STOPPED: 进程停止运行。通常在收到SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU等信号时发生;或者在调试时收到任何信号都会发生。可向其发送SIGCONT使其继续运行。
task的状态记录在task_struct结构的state成员中。
1. 调度策略
Linux进程调度策略是以优先级调度为基础的,即优先运行优先级最高的进程。在优先级调度的基础上,通过被分配的优先级的范围,又可以把进程分为实时进程(这里的实时是软实时)和一般进程。实时进程优先于一般进程,并由特殊的调度策略来保证它们的(软)实时性。
·优先级系统
系统中所有进程的优先级在[0..MAX_PRIO-1]之间,数值越低优先级越高。其中,实时进程的优先级范围在 [0..MAX_RT_PRIO-1],一般进程的优先级在[MAX_RT_PRIO..MAX_PRIO]. 当前内核中的默认配置是:进程优先级在 [0..139], 其中实时进程占用[0..99],一般进程占用[100..139]。
实时进程的优先级从创立之初便已固定,不会改变,以保证给定优先级别的实时进程总能抢占优先级比它低的进程。
与此相对地,一般进程的优先级分为静态和动态两方面。静态优先级在进程产生的时候确定,而动态优先级则会在运行时会随着进程状态而动态变化。
它们的静态优先级直接由nice值来确定:static_prio = MAX_RT_PRIO + nice + 20,其中nice值的范围在[-20..19]。
而它们的动态优先级使用函数effective_prio来计算得到,交互性强的优先级会在静态优先级的基础上得到最多-5的额外优先级,而CPU占用率高的则会被扣除最多+5的优先权,但调整后的优先级必须被保持在[100..139]之间:
static int effective_prio(task_t *p)
{
int bonus, prio;
if (rt_task(p)) /* 如果是实时进程 */
return p->prio; /* 不作调整 */
bonus = CURRENT_BONUS(p) - MAX_BONUS / 2; /* 计算奖励值 */
prio = p->static_prio - bonus; /* 奖励 */
if (prio < MAX_RT_PRIO) /* 防止优先级越界 */
prio = MAX_RT_PRIO;
if (prio > MAX_PRIO-1)
prio = MAX_PRIO-1;
return prio;
}
其中的CURRENT_BOUNS宏根具当前进程的sleep_avg按比例缩放成它的奖励优先级:
#define CURRENT_BONUS(p) \
(NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / \
MAX_SLEEP_AVG)
进程的sleep_avg记录着一个进程用于休眠和用于执行的时间,范围从0到 NS_MAX_SLEEP_AVG (NS_MAX_SLEEP_AVG与MAX_SLEEP_AVG单位不同。前者一ns纳秒为单位,而后者以 jiffy为单位,一般为5ms. sleep_avg的单位是ns), sleep_avg的默认值为10ms(2个jiffy). 它的值在进程休眠结束时根据休眠时间增大,并在进程运行时更具运行时间减小。
2 Linux内核调度策略与算法分析
另外,还有一个宏来判断某个进程是否是交互性强的进程:
#define SCALE(v1,v1_max,v2_max) \
(v1) * (v2_max) / (v1_max)
#define DELTA(p) \
(SCALE(TASK_NICE(p), 40, MAX_BONUS) + INTERACTIVE_DELTA)
#define TASK_INTERACTIVE(p) \
((p)->prio <= (p)->static_prio - DELTA(p))
后面借助上面的代码来判断是否一个进程是交互性的。这里根据奖励值连同nice值来判断进程是否是交互性的: 一个nice值为+19的进程永远也不会被标记为交互性的(否则把nice值设置到+19的意图可能就无法达到了);而一个nice值为-20的进程则必须大量吞食CPU资源才可能会被标识成非交互的。而默认nice值的情况介于其间。在下面可以看到,交互性进程在某些时候享有一定的调度特权。
·调度策略概览
先定义活跃进程和过期进程:
·对于实时进程,所有处于TASK_RUNNING的实时进程都是活跃进程;
·对于一般进程,每个进程都拥有一定的时间片,优先级越高时间片越长。进程的运行会消耗时间片。处于TASK_RUNNING状态并且时间片没有用完的一般进程是活跃进程,而那些处于TASK_RUNNING状态但已经用完时间片的进程称为过期进程。
从上面可以看出,无论活跃进程还是过期进程都是处于TASK_RUNNING状态的进程,而那些不处于此状态的进程由于当前无法执行自然也不需要被调度。
对于所有处于TASK_RUNNING的进程,linux按照优先级将它们分组,每一个优先级对应一个进程组。在调度时,系统总是首先选取具有最高优先级的并且拥有活跃进程的进程组,然后进行相同优先级下的进程调度。
从活跃进程的定义可以知道,对于实时进程组这便意味着如果不出现级别更高的实时进程,并且该组中的进程没有全部结束,那么每次调度的结果都将是上次调度后的选择实时进程组,而后面的优先级较低的实时和一般进程都不会有机会在该CPU上运行。而对于非实时优先级组,当组中进程的时间片全部用完时,下次调度便会选择优先级低于它但还有活跃进程的组进行调度。
当通过上述途径找不到活跃进程时,将所有的过期进程重新激活(注意,所有的过期进程的状态都是TASK_RUNNING的),然后从新执行上述调度过程。
如果系统中连过期进程都没有,那么就选择idle进程作为当前进程。
·相同优先级下的进程调度
a) 实时进程
相同优先级的实时进程的调度策略有两种:
SCHED_RR: 通过时间片轮转的方式调度的实时进程。在运行了指定的时间片后会被抢占并重新调度。但如果没有其他优先级高于或等于它的实时进程与其竞争,它还会得到继续运行的机会。
SCHED_FIFO: 先入先出方式调度的实时进程,即该进程一旦执行便一直运行到结束。
b) 一般进程
一般进程的调度策略为SCHED_OTHER,此调度策略以时间片轮转为基础,并会根据每个进程的情况进行一定的优化使得进程调度可以公平有效而又不损失响应时间。具体策略如下:
首先,选择处于头上的进程运行。
其次,当这个进程用完时间片时,它便成了过期进程,此时需要重新调度。然而,为了获得更高的响应速度,对于交互性进程,它并非必须等时间片用净才会引发调度,而是运行一段较小的时间后让出给的其他进程运行。如果在此期间没有新的高优先级进程出现,其效果相当于在相同优先级的其他进程进行第二层时间片轮转。这个小时间片会更具进程的交互程度计算得到:
/* 下面这个宏计算小时间片长度 */
#define TIMESLICE_GRANULARITY(p) (GRANULARITY * \
(1 << (((MAX_BONUS - CURRENT_BONUS(p)) ? : 1) - 1)))
其中GRANULARITY默认为10ms. 得到的奖励越多,则单个小时间片越短。
另外,对于交互性进程还有另一项优化:当交互性进程的时间片用完后,他还有机会立刻重新成为活跃进程,并再度运行,条件是过期进程们没有等得太久(或者说没有饥饿).
3 Linux内核调度策略与算法分析
·进程的睡眠和唤醒
休眠进程会处于TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE状态,并且不参与调度,等待某件事情的发生。当等待条件可能发生时,系统会调用try_to_wake_up()来唤醒进程,即将进程状态置于TASK_RUNNING并重新使其参与调度。对于 TASK_INTERRUPTIBLE, 还可能存在收到SIGNAL而提前被唤醒的情况。
·关于策略对于交互性进程效果的一些说明
第一眼看到这个策略中对于交互进程的优化感觉有些奇怪,它提高了交互作业的响应度是显然的,但这是否会使得非交互进程饥饿呢?交互作业享有太多的特权了: 更多的运行时间片、更高的优先级,甚至时间片用完了还可以继续保持活跃。但以上都忽略了一个重要的事实:它是交互性的作业,从而会经常睡眠,进而不参与调度。所以它的这个时间片可能够睡上好几次才会用完。而当这个交互进程结束了交互阶段开始变得贪婪以后,动态优先级计算会剥夺它的上述特权,而加入到非交互进程行列中。上述策略中对于交互进程采取第二层轮转也可以使得更多交互进程尽早睡眠,从而空出时间让非交互进程使用。由此可以看出通过以上策略在提高了系统的响应的同时也不失策略的公平性和有效性。
·关于策略中两层时间片轮转的不同点
第一层时间片轮转根据进程的动态优先级来计算时间片,对于非活跃进程可以起到了控制其相对于其他进程的CPU占用率的作用(且对于非交互进程,也不存在第二层轮转);第二层时间片轮转如上所述,主要起到让更多交互进程尽早睡眠的作用。两者的作用各不相同,但由于后者包含于前者之中,所以才把后者称为第二层轮转(这个词是自己编的,不知道是否合适,可能叫第一类、第二类轮转更好些,以上的解释也只是我的理解)。
2. 调度算法
Linux 2.6内核中实现了一套O(1)的调度算法,也就是说每一次调度所需要的时间与该CPU内的总进程数无关。相比于以前的linux内核调度算法最坏情况O(n)的复杂度要高效、精巧许多。而且由此也可以使得实时进程的实时性得到更加充分的保证。设想一个实时进程被调度前恰巧 schedule函数在重新计算时间片从而需要O(n)的时间才能完成,系统中又有许多过期进程从而n很大,那么实时进程运行时可能已经过了比较长的时间了。而现在每次调度所花的时间几乎相同,实时进程一旦有需要会很快得到调度并投入运行。
首先介绍几个内核中用于实现该算法的几个数据结构,以及他们所涉及的一些核心算法:
·优先进程队列 prio_array
Linux定义了一个struct prio_array:
struct prio_array {
unsigned int nr_active; /* 当前活跃的进程总数 */
unsigned long bitmap[BITMAP_SIZE]; /* 活跃进程的位图 */
struct list_head queue[MAX_PRIO]; /* 各个优先级队列的头指针组成的数组*/
};
这个struct给出了系统中可供调度的所有进程的信息,其中第二个成员bitmap非常有趣,它是一个二进制串(b0, b1, … bn),当bi == 1时,表示优先级i的队列queue[i]中存在活跃进程。因此, 第一个使得bi == 1的i便对应当前活跃进程中的最高优先级。而该 进程必在queue[i]中。(TODO: Add some reason),所以调度程序schedule()选取下一个运行的进程时只需要取得 queue[i]中第一个进程来运行即可:
idx = sched_find_first_bit(array->bitmap); /* 取得第一个i, s.t. bi == 1 */
queue = array->queue + idx; /* 相当于queue[i] */
next = list_entry(queue->next, task_t, run_list); /* 取得第一个成员 */
这便已经实现了上述的优先级调度策略选择下一活跃进程的操作。
作为对比,看一下linux 2.4内核调度算法中的这一段用来选择下一个进程的循环:
next = idle_task(this_cpu); /* 默认选择的进程 */