您的位置:首页 > 运维架构 > Linux

[Linux内核设计与实现]Linux进程调度

2013-01-01 13:41 169 查看
进程调度可以看作在可运行态进程之间分配有限处理器时间资源的内核子系统。最大限度利用处理器时间的原则是,只要有可以执行的进程,那么总会有进程在运行。但是,只要系统中可运行状态的进程数量大于处理器个数,就会有进程不能运行,这些进程在等待运行。在一组处于可运行状态的进程中选择一个来执行,是调度程序所需要完成的基本任务。多任务系统可以分为两类:非抢占式多任务(cooperative multitasking)和抢占式多任务(preemptive multitasking)。Linux提供抢占式多任务模式。

策略

策略决定调度程序在何时让什么进程运行。调度器的策略往往决定系统的总体印象,并负责优化使用处理器时间。

I/O消耗型和处理器消耗型进程

进程可以分为I/O消耗型和处理器消耗型。前者指进程的大部分时间用来提交I/O请求或是等待I/O请求。因此这样的进程经常处于可运行状态,但通常都是运行短短的一会,因为它在等等更多的I/O请求时最后总会阻塞(所有I/O操作,比如键盘活动,磁盘I/O等)。处理器消耗型进程把大量时间用在执行代码上,对这类进程,调度策略是尽量降低它们的运行频率,对他们而言,延长其运行时间会更合适。调度策略通常要在两个矛盾的目标中间寻求平衡:进程响应迅速(响应时间短)和最大系统利用率(吞吐量)。Linux为了保证交互式应用,更倾向于优先调度I/O消耗型进程。

进程优先级

调度算法最基本的一类就是基于优先级的调度。这是一种根据进程的价值和对处理器时间的需求来对进程分级的方法。优先级高的进程先运行,低的后运行,相同优先级的进程按论转方式进程调度。在包括Linux在内的系统中,优先级高的进程使用的时间片(timeslice)也较长。调度程序总是选择时间片未用完而且优先级高的进程来运行。Linux系统实现了一种基于动态优先级的调度方法。Linux内核提供了两组独立的优先级范围。一种是nice值,范围从-20到19,默认值是0。nice的值越大优先级越低。第二种是实时优先级,其值是可配置的,默认情况下它的变化范围是从0到99。任何实时进程的优先级都高于普通进程。Linux提供对POSIX实时优先级的支持。

时间片与内核抢占

时间片表明进程在被抢占之前所能持续运行的时间。调度策略必须规定一个默认的时间片,但这并不是一件简单的事情:时间片过长会导致系统交互的响应欠佳,让人觉得系统无法并发执行应用程序;时间片过短会明显增大进程切换带来的处理器耗时,因为肯定会有相当一部分系统时间用于进程切换上,而这些进程能够用来运行的时间片却很短。Linux系统提高交互式程序的优先级,让它们运行的更频繁,于是调度程序提供较长的默认时间片给交互式程序。此外,Linux调度程序还能根据进程的优先级动态调整分配给它的时间片。从而保证优先级高的进程,假定也是重要性高的进程,执行的频率高,执行时间长。进程并不一定非要一次就消耗完它所有的时间片。当一个进程时间片耗尽时,就认为进程到期了。没有时间片的进程不会再投入运行,除非等到其它所有的进程都耗尽了它们的时间片。这时,所有进程的时间片会被重新计算。另外,当进程运行时,如果有其它进程进入TASK_RUNNING状态,内核就会检查它的优先级是否高于当前正在执行的进程。如果高于,调度程序就会被唤醒,抢占当前正在执行的进程并运行新的可运行进程。

Linux调度算法

Linux的调度程序定义在kernel/sched.c中。2.6内核的调度算法实现了以下目标:

充分实现O(1)调度。

全面实现SMP的可扩展性。每个处理器拥有自己的锁和自己的可执行队列。

强化SMP亲和力。尽量将相关一组的任务分配给一个cpu进行连续的执行。只有在需要平衡任务队列的大小时才能在cpu之间移动进程。

保证公平。在合理设定的时间范围内,没有进程会处于饥饿状态。同样的,也没有进程能够显失公平的得到大量时间片。

虽然最常见的优化情况是系统只有1~2个可运行进程,但是优化也完全有能力扩展到具有多处理器且每个处理器上运行多个进程的系统中。

可执行队列

调度程序中最基本的数据结构是运行队列(runqueue)。可执行队列定义于kernel/sched.c中,由结构runqueue表示。可执行队列是给定处理器上的可执行进程的链表,每个处理器一个。每个可投入运行的进程都惟一的归属于一个可执行队列。此外,可执行队列中还包含每个处理器的调度信息。

struct runqueue {
spinlock_t          lock;   /* spin lock that protects this runqueue */
unsigned long       nr_running;         /* number of runnable tasks */
unsigned long       nr_switches;        /* context switch count */
unsigned long       expired_timestamp;    /* time of last array swap */
unsigned long       nr_uninterruptible;   /* uninterruptible tasks */
unsigned long long  timestamp_last_tick;  /* last scheduler tick */
struct task_struct  *curr;                /* currently running task */
struct task_struct  *idle;           /* this processor's idle task */
struct mm_struct    *prev_mm;        /* mm_struct of last ran task */
struct prio_array   *active;         /* active priority array */
struct prio_array   *expired;        /* the expired priority array */
struct prio_array   arrays[2];       /* the actual priority arrays */
struct task_struct  *migration_thread; /* migration thread */
struct list_head    migration_queue;   /* migration queue*/
atomic_t            nr_iowait; /* number of tasks waiting on I/O */
};


对可运行队列进程操作时,要首先锁定队列,然后操作队列,最后释放锁。当对多个运行队列进行操作时要注意锁的顺序,否则可能会造成死锁。

优先级数组

每个运行队列都有两个优先级数组,一个活跃的,一个过期的。优先级数组在kernel/sched.c中定义,它是prio_array类型的结构体。优先级数组是一种能够提供O(1)级算法复杂度的数据结构体。优先级数组使可运行处理器的每一种优先级都包含一个相应的队列,而这些队列包含对应优先级上的可执行进程链表。优先级数组还拥有一个优先级位图,当需要查找当前系统内拥有最高优先级的可执行进程时,它可以帮助提高效率。

struct prio_array {
int               nr_active;         /* number of tasks in the queues */
unsigned long     bitmap[BITMAP_SIZE];  /* priority bitmap */
struct list_head  queue[MAX_PRIO];      /* priority queues */
};


MAX_PRIO定义了系统拥有的优先级个数,默认值是140。每个优先级数组都包含一个位图成员,每个优先级准备一位。一开始所有的位都被置为0,当某个拥有一定优先级的进程开始准备执行时(进程状态变为TASK_RUNNING),位图中相应的位就会被置为1。每个优先级数组还包含一个struct list_head的队列,其中每个元素都是一个struct list_head类型的链表。每个链表与一个给定的优先级对应,包含该处理器队列上相应优先级的全部可运行进程。

重新计算时间片

在决定新的时间片长短时会用到进程的优先级和其它一些属性。这种实现有一些缺点:

可能会耗费相当长的时间。最坏情况下,有n个进程的系统复杂度可能达到O(n)。
重算时必须靠锁的形式来保护运行队列和每个进程描述符。这样做可能加剧对锁的争用。
重新计算时间片时间是不确定的,这会给时间确定性要求很高的实时进程带来麻烦。
实现很粗糙

Linux调度程序减少对循环的依赖,它为每个处理器维护两个优先级数组:活动数组和过期数组。活动数组内的可执行队列上的进程都有时间片剩余;过期数组内的可执行队列上的进程都耗尽了时间片。当一个进程的时间片耗尽时,它会被移到过期数字中。重新计算时间片就发生在进程在活动数组和过期数组之间切换时。

进程时间片大小是根据进程优先级来决定的。进程优先级包括两部分:静态优先级(nice值)和动态优先级(根据是I/O消耗型还是处理器消耗型)。计算时间片时,以静态优先级为基础,通过计算进程动态优先级来最终决定进程时间片大小。举例来说,如果一个进程的初始时间片大小为100ms,当该进程交互性很强时,进程就会获得时间奖励,最终获得的时间片就会大于100ms。反之,如果进程交互性很弱,那么就会得到惩罚,进程最终获得的时间片会小于100ms。

调度程序还提供了另外一种机制来支持交互进程:如果一个进程的交互性非常强,那么当它的时间片用完之后,它会被再次放置到活动数组而不是过期数组中。这样可以避免交互性强的进程处于过期数组,当它需要交互时,却不能执行。对于处于饥饿状态的进程,它也会被放置在活动数组中,避免其进一步饥饿。

睡眠和唤醒

休眠(被阻塞)状态的进程处于一个特殊的不可执行状态。休眠通过等待队列进程处理。等待队列是由等待某些事件发生的进程组成的简单链表。



Figure 4.3. Sleeping and waking up.

负载平衡程序

负载平衡程序由kernel/sched.c中的函数load_balance()来实现。它有两种调用方法:在schedule()执行的时候,只要当前的可执行队列为空,它就会被调用;定时器调用,系统空闲1ms时调用一次或者其它情况下每隔200ms调用一次。单处理器系统中该函数不会被调用,因为系统只有一个可执行队列。

load_balance()函数执行过程如下:

首先,load_balance()调用find_busiest_queue(),找到最繁忙的可执行队列。
其次,load_balance()从最繁忙的运行队列中选择一些优先级数组以便抽取进程。最好是过期数组,因为这些进程已经有一段时间没有运行。
接着,load_balance()寻找到含有进程并且优先级最高的链表,因为把优先级高的进程平均分散开来才是最重要的。
分析找到的所有这些优先级相同的进程,选择一个不是正在,执行,也不会因为处理器相关性而不可移动,并且不在高速缓存中的进程。

只要执行队列之间仍然不均衡,就重复上面步骤,继续从繁忙队列中抽取进程到当前队列。

抢占和上下文切换

上下文切换,也就是从一个可执行进程切换到另外一个可执行进程。由定义在kernel/sched.c中的context_switch()函数负责处理。每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。它完成了两项基本的工作:

调用定义在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中。
调用定义在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复桟信息和寄存器信息。

用户抢占

内核即将返回用户空间的时候,如果need_resched标志,会导致schedule()被调用,此时就会发生用户抢占。在内核返回用户空间的时候,它知道自己是安全的,因为既然它可以继续去执行当前的进程,那么它当然可以再选择一个新的进程去执行。所以,内核无论是在从中断处理程序还是在系统调用后返回,都会检查need_resched标志。如果它被设置了,那么内核会选择一个其它进程投入运行。

用户抢占在以下情况时发生:

从系统调用返回用户空间。
从中断处理程序返回用户空间。

内核抢占

Linux完整的支持内核抢占。如果没有持有锁,内涵就可以进行抢占(重新调度是安安的)。为了支持内核抢占所做的第一处变动就是为每个进程的thread_info引入preempt_count计数器。该计数器初始值为0,每当使用锁的时候数值加1,释放锁的时候数值减1。如果need_resched被设置,并且preempt_count为0,这说明有一个更为重要的任务需要执行并可以安全的抢占。如果preempt_count不为0,说明当前任务持有锁,所以抢占是不安全的。如果内核中的进程被阻塞了,或它显式的调用了schedule(),内核抢占也会显式的发生。

内核抢占会发生在:

当从中断处理程序正在执行,且返回内核空间之前。
当内核代码再一次具有可抢占性的时候。
如果内核中的任务显式的调用了schedule()。
如果内核中的任务阻塞。

实时

Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR。而普通的、非实时的调度策略是SCHED_NORMAL。SCHED_FIFO是一种简单的、先入先出的调度算法,它不使用时间片。SCHED_FIFO级的进程会比任何SCHED_NORMAL级的进程都先得到调度。一旦一个SCHED_FIFO级进程处于可执行状态,它就会一直执行下去,直到它自己受阻塞或显式的释放处理器为止。只有较高优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。如果有两个或者更多SCHED_FIFO级进程,它们会轮流执行。只要有SCHED_FIFO级进程在执行,其它级别较低的进程就只能等待它结束之后才会有机会执行。

SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间之后就不能再继续执行。SCHED_RR是带有时间片的SCHED_FIFO--一种实时轮流调度算法。对于SCHED_FIFO进程,高优先级的进程总是立即抢占低优先级进程,但低优先级进程决不能抢占SCHED_RR任务。

Linux的实时调度算法提供了一种软实时工作方式。软实时的含义是,内核调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求。相反,硬实时系统保证在一定条件下,可以满足任何调度的要求。Linux对实时任务的调度不做任何保证。虽然不能保证硬实时工作方式,但Linux的实时调度算法的性能还是很不错的。

参考资料:

《Linux内核设计与实现》,第二版
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: