转自 http://hi.baidu.com/mychaoyue2011/blog/item/6df45895c3d63243d0135e01.html
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). 它的值在进程休眠 结束时根据休眠时间增大,并在进程运行时更具运行时间减小。
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); /* 默认选择的进程 */