linux 进程调度

来源:互联网 发布:网络电视怎么搜频道 编辑:程序博客网 时间:2024/05/16 14:22
  • 可执行队列

调度程序中最基本的数据结构是可运行队列(runqueue)。可执行队列定义与kernel/sched.c,由结构runqueue表示。可执行队列是给定处理器上的可执行进程的链表,每个处理器只有一个,而且每个进程都唯一的归属于其中的一个可运行队列。

 

 

由于可执行队列是调度程序的核心数据结构体,所有有一组宏定义用于获取与给定处理器或进程相关的可执行队列。

cpu_rq(processor) --- 返回给定处理器可执行队列的指针
this_rq(processor) --- 返回当前处理器可执行队列的指针
task_rq(task) --- 给定任务所在的队列指针

对可执行队列操作前,应该先锁住它:
struct rq *task_rq_lock(struct task_struct *p, unsigned long *flags)
void task_rq_unlock(struct rq *rq, unsigned long *flags)
eg:
struct rq *rq;
unsigned long flags;

rq = task_rq_lock(task, &flags);
...
task_rq_lock(task, &flags);

为避免死锁,要锁住多个运行队列必须按照同样的顺序获取这些锁:
if (rq1 == rq2) {
    spinlock(&rq1->lock);
} else {
    if (rq1 < rq2) {
        spin_lock(&rq1->lock);
        spin_lock(&rq2->lock);
    } else {
        spin_lock(&rq2->lock);
        spin_lock(&rq1->lock);
    }
}
......
spin_unlock(&rq1->lock);
if (rq1 != rq2){
    spin_unlock(&rq2->lock);
}
或者简化为:
double_rq_lock(rq1, rq2);
......
double_rq_unlock(rq1, rq2);

  • 优先级数组

每个运行队列(runqueue)有两个优先级数组(active和expired),一个活跃的,一个过期的。优先级数据在kernel/sched.c中被定义,它是prio_array类型的结构体。优先级数组可使可运行处理器的每一种优先级都包含一个相应的队列,而这些队列包含对应优先级的可执行进程链表。优先级数组还拥有一个优先级位图。它的结构如下:

MAX_PRIO定义了系统所拥有的优先级个数,默认值是140。每个优先级都有一个相应的可执行链表。BITMAP_SIZE是优先级位图数组的大小,因为unsigned long是32bit,因此需要5个unsigned long即160位才能表示140个优先级,即每一位代表一个优先级(最左边优先级最高)

 

一开始,优先级数组中的每一位都被置为0,当某个优先级的可执行开始执行时,相应的位会被置1。查找系统中最高的优先级就是在查找位图中从左到右第一个被设置的位。因为优先级个数是个定值,所以查找时间恒定,并不受系统到底有多少可执行进程的影响。此外,linux对它支持的每一种体系结构都提供了对应的快速查找算法,以保证对位图的快速查找。如:sched_find_first_bit()。

 

每个优先级还拥有一个相应的可执行进程的链表(struct list_head),优先级数组还包含一个计数器nr_active,它保存了优先级数组内可执行进程的数目。

 

  • 重新计算时间片

许多操作系统在所有进程的时间片都用完时,都采用一种显式的方法来重新计算每个进程的时间片。典型的实现是循环访问每个进程,像这样:

for(系统中的每个任务){

    重新计算优先级

    重新计算优先级

}

 

这不仅耗费了相当长的时间,而且实现也很粗糙,新的调度程序如下:

struct prio_array *array = rq->active;

if( !array->nr_active){

    rq->active = rq->expired;

    rq->expired = rq->array;

}

 

新的调用算法用两个优先级数组(参考struct runqueue, active, expired),active数组内的进程都有时间片剩余,expired数组内的进程的时间片都已经耗尽,当active数组内进程用完时间片时,就会从active数组移到expired数组。此调度程序在没有active进程存在时,交换active与expired两个数组。实现了重新调度。

  • schedule()

当内核代码想要休眠时,或者有哪个进程被抢占,该函数都会被执行。该函数独立于每个处理器运行。schedule首先取出active数组内的进程,在位图中查找最高的优先级,并从相应优先级的链表中取出相应的task_struct,如果它不是当前进程,则进行调度

 

  • 计算优先级与时间片

进程拥有一个初始的优先级,叫做nice值(-20 - 19),存放在task_struct的static_prio,而且这个值从一开始指定后,就不能改变。调度程序要用到的动态优先级存放在prio域里。动态优先级通过一个关于静态优先级和进程交互性的函数关系计算而来。effective_prio()函数可以返回一个进程的动态优先级。这个函数以nice值为基数,再加上-5到+5之间的进程交互性的奖励或罚分。交互性补强不弱的进行--位于I/O消耗型进程与处理器消耗进程之间,不会得到优先级的奖励,同样也不会罚分,所以它的动态优先级与nice值相等。

 

而进程的交互性怎么算呢,它通过进程休眠的时间长短来反映进程是I/O消耗型进程还是处理器消耗型进程,如果一个进程大部分时间都在休眠,那么它就是I/O消耗型的,如果进程的执行时间比休眠的时间长,那么它就是处理器消耗型的。Linux记录了一个进程用于休眠和用于执行的时间,该值存放在task_struct的sleep_avg域中(0-MAX_SLEEP_AVG),它的默认值是10ms。当一个进程从休眠状态恢复到执行状态时,sleep_avg会根据它休眠的时间的长短而增长,直到达到MAX_SLEEP_AVG,相反,进程每运行一个时钟节拍,sleep_avg就做相应的递减,直到0。

 

调度程序还提供了另外一种机制以支持交互进程:如果一个进程的交互性非常强,那么当它的时间片用完后,它会被放到active数组而不是expired数组中。之前重新计算时间片是通过交换active和expired数组得以实现的,但如果交互性强的进程用完时间片后,处于expired数组,而当它需要交互的时候,却无法执行,必须等到交换数组的时候才能执行。将交互式的进程重新插入到活动数组可以避免这种问题。但该进程不会被立即执行,它会和优先级相同的进行轮流着被调度和执行。该逻辑在scheduler_tick()中实现,该函数会被定时器中断调用。

 

首先,先减小时间片的值,再判断时间片是否耗尽,如果是,则先通过TASK_INTERACTIVE()宏查看这个进程是不是交互型进程。该宏主要基于进程的nice值来判断它是不是一个“交互型十足”的进程。nice越小(优先级越高),越能说明该进程交互性越高。EXPIRED_STARVING()宏负责检查expired数组内的进程是否处于饥饿状态——是否已经有相当长的时间没有发生数组切换了。如果最近一直没有发生切换,那么再把当前的进程放置到active数组会进一步拖延切换,expired数组内的进程会越来越饥饿。只要不发生这种情况,进程就会被重新放置在active数组里,否则,会被放置在expired数组里。