Linux进程调度
1. 早期调度算法及其缺陷
在早期内核版本,Linux的调度算法很简单。当需要调度时调度函数就扫描整个可运行队列并计算进程的优先级,从中选出优先级最高的进程以替代当前占用CPU的进程。这种调度方式在设计和实现上非常简单,只需将可运行进程组织成一个队列,然后遍历就可以了。但它有一个非常致命的缺陷,随着进程数量的增加,扫描所花费的时间也随之增加。
2. O(1)算法
为了解决以前版本的缺陷,2.6版本内核采用了一种较为复杂的调度策略。这种策略解决了进程调度时间随进程数量增加的问题。它在一个固定时间内找到最佳进程。所以也被称为O(1)算法。
2.1与调度相关的数据结构
进程优先级
linux中进程的优先级范围为0~MAX_PRIO-1,其中实时进程优先级范围是0~MAX_RT_PRIO-1,非实时进程优先为MAX_RT_PRIO~MAX_PRIO,优先级越小表示优先级越高。实时进程的优先级总高于普通进程。
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + 40)
runqueue
每个CPU都有一个可运行队列,调度程序从中选取需要占用CPU的进程。可运行队列又根据时间片是否用完分为活动进程集合和过期进程集合。分别用active,expired指向他们。runqueue中有这样一个字段:prio_array_t *active, *expired, arrays[2]。arrays为活动进程和过期进程的两个集合。
prio_array_t 结构定义为:
prio_array_t
struct prio_array {
unsigned int nr_active;
unsigned long bitmap[BITMAP_SIZE];
struct list_head queue[MAX_PRIO];
};
typedef struct prio_array prio_array_t;
nr_active:链表中进程描述符的数量。
bitmap[BITMAP_SIZE]:优先级位图。当且仅当某个优先权的进程链表不为空时设置相应的位标志。
queue[MAX_PRIO]:140个优先权队列的头结点。
2.2 O(1)算法分析
算法之所以能实现调度时间复杂度为O(1)。主要是在两个方面进行了改进:
(1)在早期内核版本中,当所有进程的时间片用完后,调度程序会扫描整个可运行队列计算进程优先级并根据优先级分配时间片。而O(1)算法采用分散计算时间片的方法。O(1)算法中,过期进程集合中的进程时间片已用完,活动进程集合中进程时间片未用完,当一个进程用完其时间片后,它就会被放入过期进程集合中,在移动之前调度程序会重新对其分配时间片。
(2)O(1)算法也不需要扫描整个可运行队列以寻找最佳进程,在O(1)算法中不管是活动进程集合还是过期进程集合,各自都有140个按照优先级所划分的进程链表,并且两个集合还各有一个优先级位图用来表示每个优先级链表是否为空,当调度程序寻找最佳next进程时,会在活动进程集合中利用优先级位图从高到低找到第一个为不为0的位,从而对应到相应的进程链表,链表中的进程具有相同的优先级,取出第一个就是最佳进程也就是优先级最高的进程。当活动进程集合为空时只需交换两个集合,也就是交换active指针和expired指针就可以了。由于进程的优先级只有140个,所以寻找最佳进程的时间是不会随着进程数量增加而增加。
3.schedule分析(2.6.11内核)
next指向被选中的进程,这个进程将取代当前进程在CPU上执行。如果系统中没有优先级高于当前进程,那么next会和current相等。不发生任何切换。
task_t *prev, *next;
runqueue_t *rq;
prio_array_t *array;
struct list_head *queue;
unsigned long long now;
unsigned long run_time;
int cpu, idx;
先禁止抢占,prev指向当前进程,释放大内核锁并让rq指向本地cpu的运行队列
preempt_disable();
prev = current;
release_kernel_lock(prev);
rq = this_rq();
检查可运行队列是否为空,如果为空就调用idle_balance从另外一个运行队列迁移一些可运行进程到本地运行队列中
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
检查活动进程集合中是否为空。若为空则交换活动集合和过期集合。
array = rq->active;
if (unlikely(!array->nr_active)) {
schedstat_inc(rq, sched_switch);
rq->active = rq->expired;
rq->expired = array;
array = rq->active;
rq->expired_timestamp = 0;
rq->best_expired_prio = MAX_PRIO;
} else
schedstat_inc(rq, sched_noswitch);
在活动集合中搜索一个可运行的进程。首先搜索优先级位图第一个非0位,并找到对应的链表。将next指向链表中第一个进程
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
next = list_entry(queue->next, task_t, run_list);
根据进程类型及activated值更新进程的平均睡眠时间和动态优先级,activated有以下取值:
0:进程处于TASK_RUNNING状态。
1:进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,而且正在被系统调用服务例程或内核线程唤醒。
2:进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,而且正在被ISR或者可延迟函数唤醒。
-1:表示从UNINTERRUPTIBLE状态被唤醒
if (!rt_task(next) && next->activated > 0) {
unsigned long long delta = now - next->timestamp;
if (next->activated == 1)
delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;
array = next->array;
dequeue_task(next, array);
recalc_task_prio(next, next->timestamp + delta);
enqueue_task(next, array);
}
next->activated = 0;
如果prev和next是同一个进程不需要切换,否则使用函数context_switch(rq, prev, next)切换进程,最后调用finish_task_switch(prev)做一些收尾工作。
if (likely(prev != next))
next->timestamp = now;
rq->nr_switches++;
rq->curr = next;
++*switch_count;
prepare_arch_switch(rq, next);
prev = context_switch(rq, prev, next);
barrier();
finish_task_switch(prev);
}