Linux是抢占式多任务系统,通过动态计算时间片的机制对任务进行强制切换。
1.1 策略
1.1.1.I/O消耗型和处理器消耗型的进程
前者大部分时间在提交和等待I/O请求,运行时间短,应该尽量缩短它们的响应时间,即优先调度它们。
后者大部分时间执行代码,应该尽量减小调度它们的频率,即优不优先无所谓,关键时延长它们的运行时间。
调度策略的两个矛盾点:进程响应速度和系统利用率
1.1.2.进程优先级
Linux使用动态优先级的调度方法,例如,如果系统发现某进程是I/O消耗型,就会动态提高其优先级,如果发现某进程是处理器消耗型的,就会降低其优先级。
Linux有两种独立的优先级范围:
1、-20<=nice<=19,默认nice为0.-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/O型30ms时间片,那么处理型很可能只有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);
如果prev和next相同,说明调度的和之前的一样,不用启用context_switch().
完全的O(1)算法
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()就会执行它
主要工作:1、switch_mm()//从虚拟内存上将进程映射到新的进程
2、switch_to()//将处理器切换到新进程的状态
need_resched标志是否需要执行调度程序
在某进程的时间片用尽时,schedule_tick()会标志它
当一个更高优先级的进程进入可执行队列时,try_to_wake_up()也会标志它
返回用户空间或者从中断返回时,就会检查need_resched
1.3.1.用户抢占
从系统调用或者从中断处理程序返回用户空间时,检查到了need_resched就发生了用户抢占
1.3.2.内核抢占
发生条件:
1、need_resched被设置。
2、thread_info里面的preemp_count为0,也就是进程拥有的锁的数量。
发生时机:
1、 当从中断处理程序返回内核空间之前
2、 内核进程显示调用schedule
3、 内核进程休眠
4、 内核代码再一次具有可抢占性的时候 ???【书上说在释放最后一个锁的时候会检查need_schedule的值,如果设置了,就执行调度,why?内核进程应该具有一种机制,在拥有锁时记录有没有其他进程要执行抢占,need_schedule可以记录,然后在释放锁时就有义务主动检查need_schedule,决定是否要执行调度】