从运行队列的结构中,我们发现了两个有关优先级的数组,定义在kernel/sched.c中,名字叫prio_array,活跃的和过去的。
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO];
};
#define DECLARE_BITMAP(name,bits) \
unsigned long name[BITS_TO_LONGS(bits)]
#define BITS_TO_LONGS(nr) DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long))
#define DIV_ROUND_UP(x,y) (((x) + ((y) - 1)) / (y))
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1);
等价于:unsigned longbitmap[MAX_RT_PRIO/8 + 1];
这两个数组是实现O(1)级算法的数据结构。优先级数组使可运行处理器的每一种优先级都包含一个相应的队列,而这些队列包含对应优先级上的可执行进程链表。结构中的优先级位图主要是为了帮助提高查找当前系统内拥有最高优先级的可执行进程的效率而设计的。关于结构中的定义常量,MAX_PRIO定义了系统拥有的优先级个数,默认值是140,BITMAP_SIZE是优先级位图数组的大小,值是5,这个是可以计算出来的。比如:位图数组的类型是unsigned long类型,长是32位,假定每一位代表一个优先级(4*32 = 128<140),就需要5个这样的长整型才能表示。所以,bitmap就正好含有5个数组项。
开始时,位图成员的所有位都被清0,当某一优先级的进程开始准备执行时,位图中对应的位就置1,这样,查找系统中最高的优先级就变成了查找位图中被设置的第一位。鉴于优先级个数的定值性,查找的时间就不受系统到底有多少可执行进程的影响,是个定值。关于对位图的快速查找算法对应的函数是sched_find_first_bit().
优先级数组的list_head队列是一个链表,这个链表包含了该处理器上相应优先级的全部可运行线程。要找到下一个要运行的任务,直接从该链表中选择下一个元素。对于给定的优先级,按轮转方式调度任务。优先级数组的计数器nr_active,它保存了该优先级数组内可执行进程的数目。
下面是时间片的话题,一旦所有进程的时间片都用完会怎样了,总不能系统宕掉吧,好在操作系统提供了一种显式的方法来重新计算每个进程的时间片。典型就是循环访问每个进程。借用书上的一个例子:
for (系统中的每个任务){ 重新计算优先级 重新计算时间片 } |
这种方法简单,但弊端很多:
1.可能耗费相当长的时间。 2.为了保护任务队列和每个进程的描述符,必要要用到锁的机制。这样做很明显会加剧对锁的争用,影响系统性能。 3.重算时间片的时机不确定。 4.实现显得很粗糙。 |
现在采用的是新的调度方法,每个处理器拥有上面提到的两个优先级数组,活动数组上的进程都有时间片剩余,而过期数组中的进程都耗尽了时间片,当一个进程的时间片耗尽时,它会移至过期数组,但在此之前,时间片已经给它重新计算好了,重新计算时间片现在变的非常简单,只要在活动和过期数组之间来回切换就可以了。又因为数组是通过指针进行访问的,所以交换它们用的时间就是交换指针需要的时间。这个动作由schedule()完成:
struct prio_array *array = rq->active; if(!array->nr_active){ rq->active = rq->expired; rq->expired = array; } |
这种交换是O(1)级调度程序的核心。O(1)级调度程序根本不需要从头到尾都忙着重新计算时间片,它只要完成一个两个步骤就能实现数组的切换,这种实现完美地解决了前面列举的所有弊端。linux的调度程序是在schedule()函数实现的。当内核代码想要休眠时,会直接调用该函数,如果哪个进程将被抢占,也会被唤起执行。调度的第一步是判断谁是优先级最高的进程:
struct task_struct *prev, *next; struct list_head *queue; struct prio_array array; int idx; prev = current; array = rq->active; idx = sched_find_first_bit(array->bitmpa); queue = array->queue + idx; next = list_entry(queue->next, struct task_struct, run_list); |
分析上面这段代码,首先在活动优先级数组中找到第一个被设置的bit位,该位对应着最高的可执行进程。然后,调度程序选择这个级别链表里的头一个进程。这个进程就是系统中优先级最高的可执行程序,也是马上就会被调度执行的进程。上面中的prev和next不等,说明被选中的进程不是当前进程,这时就要调用context_switch来进程切换。最后强调一点,不存在任何影响schedule()时间长短的因素,因为前边说过,所用的时间是恒定的。