《linux内核设计与实现》4进程调度

 

Linux是抢占式多任务系统,通过动态计算时间片的机制对任务进行强制切换。

1.1      策略

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

前者大部分时间在提交和等待I/O请求,运行时间短,应该尽量缩短它们的响应时间,即优先调度它们。

后者大部分时间执行代码,应该尽量减小调度它们的频率,即优不优先无所谓,关键时延长它们的运行时间。

调度策略的两个矛盾点:进程响应速度和系统利用率

1.1.2.进程优先级

Linux使用动态优先级的调度方法,例如,如果系统发现某进程是I/O消耗型,就会动态提高其优先级,如果发现某进程是处理器消耗型的,就会降低其优先级。

Linux有两种独立的优先级范围:

1-20<=nice<=19,默认nice0.-20优先级最高,时间片可能最多,19优先级最低,时间片可能最少。

2、实时优先级,0-99.任何实时进程优先级都高于普通进程。

1.1.3.时间片

时间片长——感觉系统没有并行执行任务,对交互响应表现欠佳

时间片短——明显增大了任务切换的处理器消耗

又显现了上面提及的矛盾:I/O型不需要长的时间片,处理型需要越长越好。【能保证缓存命中率】

Linux认为系统交互更为重要,所以默认的时间片都比较短,【如20ms】,但是它会动态的调整优先级和时间片,通常高优先级的时间片也越长。

进程并不是一次就要用完自己的时间片,有时分开调度效果更好,如交互式进程,多调度几次反而可保证长时间处于运行状态。

时间片一旦耗尽,就不会再被调度,直到所有进程的时间片都耗尽,再一起重新分配时间片。

1.1.4.进程抢占

当自己时间片用尽,就可能被抢占,当有优先级更高的任务进入TASK_RUNNING状态,也会被抢占。

1.1.5.调度策略的活动

如果linux有两个任务,一个是I/O型,一个是处理型。系统会给I/O型更高的优先级和更长的时间片。这样I/O型就能在你输入完之后马上抢占处理型任务让你的输入得到响应。而当I/O型被阻塞时,处理型就会独占处理机。

疑问:假设给I/O30ms时间片,那么处理型很可能只有10ms,而每次I/O型用的时间片又很少,比如只有5ms,之后马上就要被阻塞等待用户输入,处理型任务开始执行并且用尽10ms,那么此时I/O型还有25ms时间片,可以再被阻塞5次,再次阻塞时处理型任务已经用尽时间片,必须等到I/O型用尽25ms之后才会再次获得时间片岂不是很郁闷?

1.2      Linux调度算法

目标:

1、  充分实现O(1)调度【调度工作在恒定的时间内完成,不受任务数量的影响】

2、  全面实现SMP的可扩展性

3、  强化SMP的亲和力

4、  加强交互性能

5、  保证公平

6、  扩展优化

1.2.1.可执行队列

可执行队列每个处理器有一个,所有TASK_RUNNIG任务唯一的归属于一个可执行队列

得到可执行队列指针:cpu_rq(processer)               this_rq()                task_qr(task)

下图为runqueue的定义:

 

少数情况下会出现自己的可执行队列被其他处理器访问的情况,所以必须在访问可执行队列前后加解自旋锁,严防多个处理器同时访问一个可执行队列。

Struct runqueue *rq;

Unsigned long flags;

rq = task_rq_lock(task,&flags);

/*访问可执行队列*/

Task_rq_unlock(rq,&flags);

注意,如果一个处理器一次要接连访问两个可执行队列,自然必须对两个队列都加锁,重点是加锁的顺序必须有一致性约定。【linux下是以队列地址从低到高的顺序来加】

Why

AB处理器都要一次访问ab队列,如果没有约定,恰巧A锁了b的同时B锁了a,死锁!

所以规定大家都先锁a,再锁b,就避免了死锁

1.2.2.优先级数组

活动数组和过期数组

都是prio_array类型的结构体

Struct prio_array

{

       Int nr_active;                                                          /*任务数目*/

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

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

}

1.2.3.重新计算时间片

schedule()里:

Stuct prio_array * array=rq->active;

If(!array->nr_active)

{

       Rq->active=rq->expired;

       Rq->expired = array;

}

这里解答了以前的疑问,当I/O型在用了5ms之后休眠,已经不在rq->active->queue里了,当然就会直接把活动数组和过期数组对调重新分配时间片,此时处理机型就会继续运行。

1.2.4.Schedule()

关键工作:

Stuct task_struct *prev,*next;

Struct list_head queue;

Struct prio_array *array;

Int idx;

 

Prev=curren;

Array = rq->active;

Idx = sched_find_first_bit(array->bitmap);

Queue = array->queue+idx;

Next = list_entry(queue->next,struct task_struct,run_list);

 

 

如果prevnext相同,说明调度的和之前的一样,不用启用context_switch().

完全的O1)算法

1.2.5.计算优先级和时间片

优先级的计算:

task_struct->static_prio;          //是静态的nice值,一旦用户定下就不能更改

task_struct->prio;                        //动态计算得到的nice

effective_prio();                          //返回动态计算nice

系统经过运行一段时间之后推断进程是I/O型还是处理型对优先级进行动态的增减

task_struct->sleep_avg; //存放一个最近休眠时间,范围是0-MAX_SLEEP_AVG,运行时递减,休眠时递增,系统通过该值判断优先级该增还是该减。

时间片的计算:

子进程被创建时平分父进程的时间片

task_timeslice()给任务返回一个新的时间片,原则就是优先级越高,时间片越多。

通常19级时间片为5ms-20级时间片为800ms

scheduler_tick():

该函数在定时器中断中调用,作用是把时间片用完的“交互性十足”进程直接再次放到活动数组。定时器中断很重要,时间片的计算就在里面,每ms都会调用,时cpu处于中断上下文。

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(task)为真代表该进程“交互性十足”

       EXPIRED_STARVING(rq)过期数组的进程是否处于饥饿状态

1.2.6.睡眠和唤醒

休眠的原因是等待一些事件。

过程:

把自己标记为休眠

移出运行队列

移入等待队列

调用schedule();

DECLARE_WAITQUEUE(wait,current);//申明一个等待队列项wait

add_wait_queue(q,&wait);//wait加入名为等待队列q

while(!condition)//如果条件不成立,这里用while是为了避免伪唤醒

{

       set_current_state(TASK_INTERRUPTIBLE); //设置为可中断的休眠状态

       if(signal_pending(current))//检查信号

      

处理信号;

if(检查信号得到的结果是事件已成立)

break  //因为如果此时事件发生了,而进程却被休眠了,其结果是要么进程不会被唤醒,要么就进程错过了这个时间,书上没有写,但是逻辑肯定是有的

schedule();//开始调度,期间会把它从运行队列移出。事实上这就是该进程真正被休眠的点,当该进程被waik_up()唤醒的时候,就继续往下走。

 

             

       }

set_current_state(TASK_RUNNING);//设置运行状态

remove_wait_queue(q,&wait);//从等待队列移出

 

wakeup()唤醒指定等待队列的所有进程,他调用try_to_wake_up()设置进程状态为TASK_RUNNING,调用active_task()把进程移入运行队列。

通常哪段代码促使了事件成立,它就负责对相应的等待队列执行wake_up()操作。

 

内核代码在循环体内常常需要执行一些其他的代码,如在schedule之前释放锁,而在之后再获取他们。why?

因为有时是要必要在schedule之前释放锁的,这个schedule是用户自己主动调用的,而不是被动抢占的,所以最好在休眠之前释放锁。

1.2.7.负载平衡程序

load_balance()

调度的两种情况:1、执行schedule()的时候,发现可执行队列为空,此时找到一些进程插入它就行了。

                            2、被定时器调用,要么在系统空闲时没ms调用一次,要么在其他情况下每200ms调用一次。要解决所有运行队列的失衡。

调用时要锁住可执行队列并且屏蔽中断。

执行过程:

1、  find_busiest_queue();

2、  首选抽取过期数组里的进程,如果没有,就抽活动数组

3、  首选抽取优先级高的

4、  找到能够抽取的进程【和处理器不相关,没有正在运行,不在高速缓存中】,pull_task()抽取到当前进程。

5、  检查如果还是不平衡,重复,直到平衡,退出。

1.3      抢占和上下文切换

context_switch(),当要执行新的进程的时候,schedule()就会执行它

主要工作:1switch_mm()//从虚拟内存上将进程映射到新的进程

                2switch_to()//将处理器切换到新进程的状态

need_resched标志是否需要执行调度程序

在某进程的时间片用尽时,schedule_tick()会标志它

当一个更高优先级的进程进入可执行队列时,try_to_wake_up()也会标志它

返回用户空间或者从中断返回时,就会检查need_resched

1.3.1.用户抢占

从系统调用或者从中断处理程序返回用户空间时,检查到了need_resched就发生了用户抢占

1.3.2.内核抢占

发生条件:

1need_resched被设置。

2thread_info里面的preemp_count0,也就是进程拥有的锁的数量。

发生时机:

1、  当从中断处理程序返回内核空间之前

2、  内核进程显示调用schedule

3、  内核进程休眠

4、  内核代码再一次具有可抢占性的时候  ???【书上说在释放最后一个锁的时候会检查need_schedule的值,如果设置了,就执行调度,why?内核进程应该具有一种机制,在拥有锁时记录有没有其他进程要执行抢占,need_schedule可以记录,然后在释放锁时就有义务主动检查need_schedule,决定是否要执行调度】

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值