linux进程调度

来源:互联网 发布:社交网络 mp4 编辑:程序博客网 时间:2024/06/06 14:25
一,进程调度的作用:
顾名思义,进程调度就是对进程进行调度,即负责选择下一个要运行的进程.通过合理的调度,系统资源才能最大限度地发挥作用,多进程才会有并发执行的效果.

二,进度调度的目标和基本工作:
进程调度最终要完成的目标就是为了最大限度的利用处理器时间.
即,只要有可以执行的进程,那么就总会有进程正在执行.当进程数大于处理器个数时,某一时刻总会有一些进程进程不能执行.这些进程等待运行.在这些等待运行的进程中选择一个合适的来执行,是调度程序所需完成的基本工作.

三,调度策略
1,考虑到进程类型时:I/O消耗型进程 pk 处理器消耗型进程.
I/O消耗型进程:指进程大部分时间用来提交I/O请求或者是等待I/O请求.
处理器消耗型进程:与I/O消耗型相反,此类进程把时间大多用在执行代码上.
此时调度策略通常要在两个矛盾的目标中寻找平衡:进程响应时间短(优先I/O消耗型进程)和最大系统利用率(优先处理器消耗型进程).
linux为了保证交互式应用,所以对进程的响应做了优化,即更倾向于优先调度I/O消耗型进程.
2,考虑到进程优先级时:
调度算法中最基本的一类就是基于优先级的调度.调度程序总是选择时间片未用尽而且优先级最高的进程运行.
linux实现了一种基于动态优先级的调度方法.即:一开始,先设置基本的优先级,然后它允许调度程序根据需要加,减优先级.
eg:如果一个进程在I/O等待上消耗的时间多于运行时间,则明显属于I/O消耗型进程,那么根据1中的考虑,应该动态提高其优先级.
linux提供了两组独立的优先级范围:
1)nice值:范围从-20到+19.默认值是0,值越小,优先级越高.nice值也用来决定分配给进程的时间片的长短.
2)实时优先级:范围为0到99.注意,任何实时进程的优先级都高于普通的进程.
3,考虑到进程时间片时:
时间片是一个数值,它表明进程在被抢占前所能持续运行的时间.调度策略必须规定一个默认的时间片.时间片过长,则会影响系统的交互性.时间片过短,则会明显增大因进程频繁切换所耗费的时间.
调度程度提供较长的默认时间片给交互式程序.此外,linux调度程序还能根据进程的优先级动态调整分配给它的时间片,从而保证了优先级高的进程,执行的频率高,执行时间长.
当一个进程的时间片耗尽时,则认为进程到期了,此时不能在运行.除非所有进程都耗尽了他们的时间片,此时系统会给所有进程重新计算时间片.

四,linux调度算法
下面为实现调度算法时的一些相关知识点和注意事项.
1,可执行队列(runqueue)
调度程序中最基本的数据结构是可执行队列(runqueue),其结构定义于kernel/sched.c中.可执行队列是给定处理器上的可执行进程的链表,每个处理器一个.
获得可执行队列:
cpu_rq(processor):    获得相关处理器的可执行队列.
this_rq():            获得当前处理器的可执行队列.
task_rq(task):        返回给定任务所在的可执行队列.
在对可执行队列进行操作前,应该先锁住它.锁住运行队列的最常见情况发生在你想锁住的运行队列上恰巧有一个特定的任务在运行.如下:
task_rq_lock();
task_rq_unlock();

struct runqueue *rq;
unsigned long flags;

rq = task_rq_lock(task,&flags); /*获得特定任务所对应的可执行队列*/
/*对可执行队列rq进行操作*/
task_rq_unlock(rq,&flags);

锁住当前可执行队列:
this_rq_lock();
rq_unlock();
注意:为了避免死锁,要锁住多个运行队列的代码必须总是按照同样的顺序获取这些锁.
比如:给锁编号1,2,3,4,5.每次申请锁的次序都是1,2,3,4,5.在没取得1锁之前不可以申请2锁.(这个属于个人理解,不知道正确与否,如有错误,希望看到错误的大侠们给予指正.)
2,优先级数组.
每个可运行队列都有两个优先级数组,一个活跃的,一个过期的.优先级数组定义在kernel/sched.c中.
优先级数组使得:每个优先级都对应一个队列,这个队列上包含着对应优先级的可执行进程链表.
优先级数组由结构体prio_array描速.

struct prio_array{
  int nr_active;    /*任务数目*/
  unsigned long bitmap[BITMAP_SIZE];   /*优先级位图*/
  struct list_head queue[MAX_PRIO];    /*优先级队列*/
};

每个优先级数组都要包含一个这样的位图成员,至少为每个优先级准备一位.一开始,所有的位都被置为0,当某个拥有一定优先级的进程开始准备执行时,图中相应的位就会被置为1.这样,查找系统中最高优先级就变成查找位图中被设置的第一个位.次功能可以由函数sched_find_first_bit()完成.
3,重新计算时间片
新的linux调度程序减少了对循环的依赖.取而代之的是为每个处理器维护两个优先级数组:活动数组和过期数组.活动数组内的进程都还有时间片剩余,而过期数组中的是时间片已经耗尽的进程.当一个进程的时间片耗尽时,它会被移至过期的数组,但在此之前,时间片已经重新计算好了.这样,当活动数组中的进程都已经消耗完时间片后,只需把活动数组和过期数组的指针互换就可以了,非常简单.
4,schedule()
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->bitmap);  /*找到位图中第一个被设置的位*/queue = array->queue + idx;  /*最高位对应的进程链表*/next = list_entry(queue->next, struct task_struct, run_list);
可以看到,schedule()首先在活动优先级数组中找到第一个被设置的位.该位对应着优先级最高的可执行进程列表,然后调度程序选择这个级别链表里的头一个进程运行.因为这个动作根本没用到循环来搜索链表里最适宜的进程,所以实际上,不存在任何影响schedule()执行瞬间长短的因素,它所用的时间是恒定的.(非常好)
相关图如下:
5,计算优先级和时间片
前面说到,优先级和时间片可以影响调度程序作出决定.下面看看这些是如何设计的.
进程拥有一个初始的优先级,即前面所说到的nice值.默认0,-20最高,+19最低.保存在进程task_struct的static_prio域中.起名为静态优先级.即,一开始由用户指定后,就不能改变.调度程序要用到的动态优先级存放在prio域里.
可以通过effective_prio()函数返回一个进程的动态优先级.实现如下:
动态优先级 = nice + (-5 到 +5)的进程交互性的奖励或罚分.
那么这里涉及到一个判断进程的交互性强弱的问题.最明显的标准莫过于进程休眠的时间长短.如果一个进程的大部分时间是在休眠,则它就是I/O消耗型的.反之,如果一个进程执行的时间比休眠的时间长,那么它就是处理器消耗型的.
为了支持这种机制,linux用task_struct中的sleep_avg域来记录一个进程用于休眠和用于执行的时间.范围从0到MAX_SLEEP_AVG.默认为10ms.当一个进程从休眠状态恢复到执行状态时,sleep_avg会根据它休眠的时间长短而增加.直到达到MAX_SLEEP_AVG为止.相反,进程每运行一个时钟节拍,sleep_avg就做相应的递减,到0为止.
当一个任务的时间片用完之后,就要根据任务的静态优先级重新计算时间片.task_timeslice()函数为给定任务返回一个新的时间片.时间片的计算只需要把优先级按比例缩放,使其符合时间片的数值范围就可以了.
调度程序还提供了另外一种机制以支持交互进程:如果一个进程的交互性非常强,那么当它时间片用完后,它会被再放置到活动数组而不是过期数组中.该逻辑在scheduler_tick()中实现:
struct task_struct *task;
struct runqueue *rq;

task = current;
rq = this_rq();

if (!--task->time_slice) {
        if (!TASK_INTERACTIVE(task) || EXPIRED_STARVING(rq))
                enqueue_task(task, rq->expired);
        else
                enqueue_task(task, rq->active);
}
宏TASK_INTERACTIVE()根据进程的nice值来判定它是不是一个交互性特别强的进程(nice值越小,交互性越强).
宏EXPIRED_STARVING()负责检查过期的数组内的进程是否处于饥饿状态,即是否已经较长时间没有切换数组了.如果有,则将进程放入过期数组中,以防止饥饿加重.
6,休眠和唤醒
休眠过程:进程把自己标记成休眠状态,把自己从可执行队列移出,放入等待队列,然后调用schedule()选择和执行一个其他进程.
唤醒的过程刚好相反,即进程被设置为可执行状态,然后从等待队列中移至可执行队列中.
等待队列的创建:
静态:DECLARE_WAITQUEUE()
动态:init_waitqueue_head()
睡眠的简单代码如下:
/* 'q' is the wait queue we wish to sleep on */
DECLARE_WAITQUEUE(wait, current);

add_wait_queue(q, &wait);
while (!condition) {     /* condition is the event that we are waiting for */
        set_current_state(TASK_INTERRUPTIBLE); /* or TASK_UNINTERRUPTIBLE */
        if (signal_pending(current))
                /* handle signal */
        schedule();
}
set_current_state(TASK_RUNNING);
remove_wait_queue(q, &wait);
唤醒的操作是通过函数wake_up进行的.它会唤醒指定的等待队列上的所有进程.通常哪段代码促使等待条件达成,它就要负责随后调用的wake_up()函数.
休眠和唤醒之间的关系如图所示:
7,负载平衡程序.
针对对称多处理系统,调度程序通过负载平衡程序来提升整体的调度.负载平衡程序保证了每个处理器对应的可执行队列之间的负载处于均衡状态.注意,这是在SMP的情况下,在单处理器下,不会被调用.
负载平衡程序由kernel/sched.c中的函数load_balance()来实现.
load_balance()的调用时刻:  1) schedule().当前可执行队列为空时,调用. 2) 每隔一段时间一调用.
负载平衡程序所做的工作大体可如下图所示.

五,用户抢占与内核抢占
用户抢占时:检查need_resched.如果设置了该位,则重新调度.否则返回执行当前进程.
用户抢占可在以下情况时发生:
1,从系统调用返回用户空间时.
2,从中断处理程序返回用户空间时.
内核抢占时:检查need_resched和preempt_count.如果need_resched被设置,preempt_count为0,则调用schedule().如果need_resched被设置,preempt_count非0,则返回执行当前进程.
内核抢占可能会发生在以下情况时:
1,当中断处理程序正在执行,且返回内核空间之前.
2,当内核代码再一次具有可抢占性的时候.
3,内核中的任务显式调用schedule()时.
4,如果内核中的任务阻塞(这也同样会导致调用schedule()).
0 0
原创粉丝点击