Linux 内核设计与实现(第二版)第4章

第4章.            进程调度

多任务系统可划分为两类:非抢占式(cooperative)和抢占式(preemptive)

41策略

411I/O消耗型和处理器消耗型:前者大部分时间用来提交I/O请求或是等待I/O请求,经常处于运行状态,但运行时间较短。相反,后者把时间大多用在执行代码上,除非被抢占,否则通常会不停运行。应尽量降低后者运行频率,适当延长其运行时间。主要是在响应时间和最大系统利用率中寻找平衡。

412进程优先级:根据进程价值和其对处理器时间的需求来对进程分级。优先级高的进程先运行,低的后运行,相同优先级的进程按轮转方式进行调度。在包括Linux的某些OS中,优先级高的进程时间片也较长。调度程序总是选择时间片未用尽而且优先级最高的进程运行。用户和系统都可以通过设置进程的优先级来影响系统的调度。

Linux根据以上思想实现了一种基于动态优先级的调试方法。一开始设置基本优先级,然而它允许调度程序根据需要来加减优先级。如:一个进程在I/O等待上消费的时间多于其运行时间,那么该进程明显属于I/O消费型的,它的优先级会被动态提高;如果一个进程的全部时间片一下就被耗尽,那么该进程属于处理器消费型的,它的优先级会被动态降低。

Linux提供了两种优先级范围:nice值;实时优先级。

413时间片:

进程不应当期盼能一次就执行完进程,用完所拥有的全部时间片。

时间片是一个数值,它表明进程在被抢占前所能持续运行的时间。时间片过长会导致系统交互的响应表现欠佳,让人觉得系统无法并发执行应用程序。时间片长度太短会明显增大进程切换带来的处理器耗时。20ms

Linux调度程序提高了交互式程序的优先级,让它们运行的更频繁。于是,调度程序提供较长的时间片给交互式程序。此外,Linux调度程序还能根据进程的优先级动态调整分配给它们的时间片的大小。

414进程抢占:Linux进程是抢占式的,当一个进程进入TASK_RUNNING,内核会首先检查它的优先级是否高于当前正在执行的进程。如果是,则调度程序会被唤醒,抢占当前正在运行进程并运行新的可运行进程。此外,当一个进程的时间片变为?,它会被抢占,调度程序被唤醒以选择一个新的进程。

 

42Linux调度算法

Linux的调度程序定义于kernel/sched.c中。O1)调度。

421可执行队列:调度程序中最基本的结构是运行队列(runqueue),由结构runqueue表示。它是给定处理器上的可执行进程的链表,每个处理器一个。每个可投入运行的进程都惟一属于一个可执行队列。此外,可执行队列还包含每个处理器的调度信息。结构如下:

struct runqueue{

spinlock_t                       lock;                    //保护可运行队列的自旋锁

unsigned long                    nr_running;              //可运行任务数目

unsigned long                    nr_switches;               //上下文切换数目

unsigned long                    expired_timestamp;         //队列最后被换出时间   

unsigned long                    nr_uninterruptible; //处于不可中断睡眠状态的任务数目

unsigned long long                timestamp_last_tick      //最后一个调度程序节拍

struct task_struct                  *curr;                    //当前运行任务

struct task_struct                  *idle;                    //该处理器的空任务

struct mm_struct                  *prev_mm;      //最后运行任务的mm_struct结构体

struct prio_array                  *active;           //活动优先级数列

struct prio_array                  *expired;          //超时优先级数列

struct prio_array                   arrays[2];         //实际优先级数组

struct task_struct                  *migration_thread;  //移出线程

struct list_head                    *migration_queue;   //移出队列

stomic_t                         nr_iowait;          //等待I/O操作的进程数目

}

由于可执行队列是调度程序的核心数据结构体,所以有一组宏定义用于获取与给定处理器或进程相关的可执行队列指针。如cpu_rqprocessor用于获取给定处理器可执行队列的指针。this_rq()返回当前处理器的可执行队列指针。最后,宏task_rqtask)返回给定任务的队列指针。

在对可执行队列进行操作之前,应该先锁住它.在其拥有者想读取或改写队列成员的时候,可执行队列包含的锁用来防止队列被其他代码改动.锁住运行队列的最常见情况发生在你想锁住的运行队列恰巧有一个特定的任务在运行。此时需要task_rq_locktask_rq_unlock函数:

struct runqueue *rq;

unsigned long flags;

rq=task_rq_locktask&flags;      //对任务的队列rq进行操作

task_rq_unlockrq&flags);

如果是对当前可执行队列,则:

struct runqueue *rq;

rq=this_rq_lock();

rq_unlock(rq);

为了避免死锁,要锁住多个运行队列的代码必须总是按照同样的顺序获取这些锁:按照可执行队列地址从低到高的顺序。

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);

自旋锁用以防止多个任务同时对可执行队列执行操作。(通过一个循环来检查令牌是否可以得到,直到拿到后可以进行要求的操作)

422优先级数组

每个运行队列都有两个优先级数组,一个活跃的和一个过期的。优先级数组在kernet/shed.c中被定义,它是prio_array类型的结构体。

struct prio_array{

      int               nr_active:             //任务数目

      unsigned long      bitmap[BITMAP_SIZE]; //优先级位图

      struct list_head     queue[MAX_PRIO];    //优先级队列

}

MAX_PRIO定义了系统拥有的优先级数,默认值为140。每个优先级都有一个struct list_head结构体。可通过位图查找具有最高优先级的可执行队列。事实上,每个链表与一个给定的优先级相对应,每个链表都包含该处理器队列上相应优先级的全部可运行进程,所以找到下一个可运行进程很简单。

423重新计算时间片

通过对每个处理器维护两个优先级数组,既有活动数组也有过期数组。活动数组内的可执行队列上的进程都还有时间片剩余;而过期数组内的可执行队列上的进程都耗尽了时间片。当一个进程的时间片耗尽时,它会被移至过期数组。但在此之前,时间片已经给它重新计算好了。重新计算时间片现在变得非常简单,只要在活动和过期数组之间来回切换就行了。因为数组是通过指针访问的,所以交换它们用的时间就是交换指针需要的时间。这个动作由shedule()来完成:

struct prio_array* array = rq->active;

if(!array->nr_active){

        rq->active = rq->expired;

        rq->expired = array

}

这种交换是O1)级调度程序的核心。

424 shedule()

选定下一个进程并切换到它去执行是通过shedule()函数实现的。当内核代码想要休眠时,会直接调用该函数,另外,如果有哪个进程将被抢占,那么该函数也会被唤起执行。    shedule()函数独立于每个处理器运行。因此,每个CPU都要对下一次该运行哪个进程做出自己的判断。

下面的代码用来判断谁是优先级最高的进程:

struct task_struct *prev,*next;

struct list_head *queue;

struct prio_array *array;

int idx;

 

prev=current;

array=rq->active;//活动优先级队列

idx=shed_find_first_bitarray->bitmap);

queue=array->queue+idx;

next=list_entryqueue->next, struct task_struct,  run_list;

首先,在活动优先数组中寻找第一个被设置的位。该位对应着优先级最高的可执行进程。然后,调度程序选择这个级别链表里的头一个进程。这就是系统中优先级最高的可执行程序,也是马上会被调度执行的进程。

如果prevnext不等,说明被选中的进程不是当前进程。此时函数context_switch()被调用,负责从prev切换到next

425计算优先级和时间片

如上述,我们看到了如何利用优先级和时间片来影响调度程序做出决定。另外还知道,I/O消耗型和处理器消耗型进程以及为什么提高I/O消耗型进程的优先级会有好处。现在我们来看实际代码是如何实现这些设计的。

进程拥有一个初始的优先级,叫做nice值。变化范围为-20+19,默认值为019优先级最低,-20最高。进程task_structstatic_prio域就存放这个值,且是静态优先级,不能改变。而调度程序要用到的动态优先级存放在prio域里。动态优先级由一个关于静态优先级和进程交互性的函数关系计算而来。

effective_prio()函数可以返回一个进程的动态优先级。这个函数以nice为基数,再加上-5+5之间的进程交互性的奖励或罚分。判断一个进程是不是I/O消耗型的交互性强的进程,最明显的标准莫过于进程休眠的时间长短了。如果一个进程的大部分时间都在休眠,那么它就是I/O消耗型的。相反,如果一个进程几乎所有时间都在执行,那么它就是纯粹的处理器消耗型进程。

为了支持这种推断机制,Linux把一个进程用于休眠和用于执行的时间记录在task_struct sleep_avg域中。它的范围从0MAX_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_STAVING(rq))

                     enqueue_task(task, rq->expired);

              else

                     enqueue_task(task, rq->active); }

这段代码首先减小进程时间片的值,再看它是否为0。如果是就说明进程的时间片已经用完,需要把它插入到一个数组中,所以该代码先通过TASK——INTERACTIVE()宏来查看这个进程是不是交互型的进程。如果不是,直接插入过期数组。接着上,EXPIRED_STARVING()宏负责检查过期数组内的进程是否处于状态。如果有过期数组中进程处于饥饿状态,那么也将当前进程插入过期数组,否则,插入到活动数组中。

4.2.6睡眠和唤醒:有两种相关状态, TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE。休眠通过等待队列处理。等待队列是由等待某些事件发生的进程组成的简单链表,内核用wake_queue_head_t来代表等待队列。它可以通过DECLARE_WAITQUEUE()静态创建,也可由init_waitqueue_head()动态创建。进程把自己放入等待队列并设置成不可执行状态。与等待队列相关的事件发生的进修,队列上的进程会被唤醒。

q是我们希望睡眠的等待队列。

DECLARE_WAITQUEUE(wait, current);

add_wait_queue(q, &wait);

while(!condition){

       set_current_state(TASK_INTERRUPTIBLE);//OR TASK_UNINTERRUPTIBLE

       if(signal_pending(current))//虚假唤醒

              schedule();

}

set_current_state(TASK_RUNNING);

remove_wait_queue(q,&wait);

唤醒操作通过wake_up()进行,它会唤醒指定的等待队列上的所有进程。它调用函数try_to_wake_up(),该函数负责将进程设置为TASK——RUNNING状态,调用activate_task()将此进程放入可执行队列,如果被唤醒进程的优先级比当前进程的优先级高,还要设置need_resched标志。通常哪段代码促成等待条件达成,它就要负责随后调用wake_up(),以便唤醒队列中的等待这些数据的进程。

427负载平衡程序

负载平衡程序由kernel/sched.c中的函数load_balance()来实现。它有两种调用方法。在schedule()执行的时候,只要当前的可执行队列为空,它就会被调用。此外,它还会被定时器调用。其过程如下:

1)首先,load_balance()调用find_busiest_queue(),找到最繁忙的可执行队列。

2)其次,load_balance(0从最繁忙的运行队列中选择一个优先级数组以便抽取进程。最好是过期数组,如果为空,那就只能是活动数组。

3)接着,load_balance()寻找含有进程并且优先级最高的链表,因为把优先级高的进程平均分散开来才是最重要的。

4)分析找到的所有这些优先级相同的进程,选择一个不是正在执行,也不会处理器相关性而不可移到,并且不在高速缓存中的进程。如果有进程满足这些条件,调用pull_task(0将其从最繁忙的队列中抽取到当前队列。

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

 

43抢占和上下文切换

从一个可执行进程切换到另一个可执行进程,由定义在kernel/sched.c中的context_switch()函数负责处理。每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。它完成两项基本的,调用定义在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中;调用定义在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态,这包括保存、恢复栈信息和寄存器信息。

用户抢占:从系统调用返回用户空间;从中断处理程序返回用户空间,都会检查need_resched标志,如果被设置了,内核就会选择一个更合适的进程投入运行。

内核抢占:只要没有持有锁,内核就可以抢占。这是通过在thread_info中引入了preempt_count计数器。该计数器初始值为0,每当使用锁的时候数值加1,释放锁时数值减1。当数值为0的时候内核就可以抢占。从中断返回内核空间的时候,内核会检查need_resched

preempt_count的值。如果need_resched被设置,并且preempt_count0的话,这说明有一个更为重要的进程需要执行并且可以安全的抢占,此时调度程序会被执行。如果preempt_count不为0,说明有当前任务持有锁,所以抢占是不安全的,此时就会像通常那样直接从中断返回当前进程。内核抢占会发生在:当从中断处理程序正在执行,且返回内核空间之前;当内核代码再一次具有可抢占性的进修;如果内核中的任务显式的调用schedule()

如果内核中的任务阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值