CPU调度
CPU调度是多道程序操作系统的基础。通过在进程之间切换CPU,操作系统可以提高计算机的吞吐率。
每当CPU空闲时,操作系统必须按照一定的策略从就绪队列当中选择一个进程来执行。
调度的对象:进程或线程。其方式与原则是一样的。所以经常以进程来说明。
那么说CPU调度<=>进程调度
调度算法的设计目标
面对客户:调度算法的设计目标应该是用户满意。
面对进程:CPU调度的目标应该是进程满意。
而让进程满意的关键就是时间,我们要做的事:
- 尽快结束任务:周转时间(从任务进入到任务 )短
- 用户操作尽快响应:响应时间(从操作发生到响应)短
- 系统内耗时间少:吞吐量(完成的任务量)大
一个系统中存在多个任务,会存在调度的矛盾。
吞吐量和响应时间有矛盾
响应时间小=>切换次数多=>系统内耗大=>吞吐量小前台任务和后台任务的关注点不同
前台任务关注响应时间,后台任务关注周转时间IO约束型程序和CPU约束型程序各有特点
CPU约束型程序以计算为主,CPU区间会较多,还会有少量长的CPU区间。如gcc。
I/O约束型程序以I/O为主,但配合I/O处理会有大量短的CPU区间。如word。
所以调度算法的设计需要折中,综合。
各种调度算法
(1)First Come,First Served(FCFS)先来先服务
平均周转时间:
(10+39+42+49+61)/5=40.2
(2)SJF短作业优先
平均周转时间:
(10+13+42+49+61)/5=35
短作业优先:如果存在i < j,而pi>pj,交换pi,pj。
短作业优先的周转时间是最小的。
如果调度结果为p1,p2,…,pn,则周转时间为:
p1+(p1+p2)+(p1+p2+p3)+…=∑(n+1-i)pi=n*p1+(n-1)*p2+…
p1乘的最大,如果按短作业优先来的话,调度的结果是p1最小,p2次之…
所以短作业优先的周转时间是最小的。
(3)round robin,RR轮转调度
按短作业优先:
但是此时就有一个问题了,P2用户的操作,要等前面所有的任务都执行完了,才会响应。这样响应时间就不能保证了。
所以有了轮转调度,按时间片来轮转调度。
时间片T=10
假设有n个,那么这里最长的响应时间为nT,这样响应时间就可以保证了。
对于轮转调度,时间片大,响应时间太长;时间片小,吞吐量小。折中的话,时间片10~100ms,切换时间0.1~1ms(1%)。
(4)优先级调度
系统中存在多种任务,比如Word关心响应时间,gcc关心周转时间,这两类任务同时存在怎么办呢。
直观的想法是:定义前台任务和后台任务队列,前台RR,后台SJF,在前台任务没有时才调度后台任务。
但是这样的绝对优先级调度问题很大啊,如果前台一直有任务,后台就一直不能被调度了。所以后台任务优先级必须动态升高。
但是后台任务(用SJF调度)一旦执行,前台的响应时间又不能保证了(后台任务 一般是CPU区间很长的任务,CPU在很长一段时间都不会被让出)。
所以说后台任务也要有时间片,如果前后台都单纯的用时间片,这又退化到了RR,后台任务的SJF又该怎么体现呢。
所以说我们的调度算法既要有轮转调度为核心,又要在轮转调度的基础上增加一些优先级,而这个优先级又要考虑到短作业优先,又要考虑到前台的任务先做。
CPU调度实例–schedule函数
Linux 0.11中的schedule()
void Schedule(void)
{
...
while(1)
{
c=-1;
next=0;
i=NR_TASKS;
p=&task[NR_TASKS];//把数组中的最后一个给p
while(--i)
{
if(!*--p)
{
continue;
}
//TASK_RUNNING代表就绪
if((*p)->state==TASK_RUNNING && (*p)->counter>c)
{
c=(*p)->counter;
next=i;
}
}//找到最大的counter,counter本身作为时间片,但又作为优先级
if(c) break;//找到就跳出
//如果就绪态的进程counter都用完了,那么执行下面的代码
for(p=&LAST_TASK;p>&FIRST_TASK;--p)
{
if(*p)
{
//(*p)->priority代表counter的初值
(*p)->counter=((*p)->counter>>1)+(*p)->priority;
}
}
}
switch_to(next);
}
for(p=&LAST_TASK;p>&FIRST_TASK;--p)
{
if(*p)
{
(*p)->counter=((*p)->counter>>1)+(*p)->priority;//①
}
}
对于这段代码,如果是就绪态的进程进来,此时就绪进程的counter都为0了,执行①处代码,相当于又重置回复了进程的counter。而如果是阻塞态的进程进来,当前counter/2后,又加上初值肯定比之前大,counter又作为优先级,一旦进程由阻塞态变成就绪态,那么它的优先级就会很高了。
下面具体的说一下counter的作用:
counter的作用:时间片
counter是典型的时间片,完成轮转调度,保证响应时间
void do_timer(...) //kernel/sched.c
{
if(--current->counter>0) return;
current->counter=0;
schedule();//时间片用完了,开始切换
}
_timer_interrupt: //kernel/system_call.s
...
call _do_timer
void sched_init(void)
{
set_intr_gate(0x20,&timer_interrupt);
}
counter的另一个作用:优先级
//找到counter最大的任务调度,counter代表优先级
while(--i)
{
if(!*--p)
{
continue;
}
if((*p)->state==TASK_RUNNING && (*p)->counter>c)
{
c=(*p)->counter;
next=i;
}
}
//counter代表的优先级进行动态调整
for(p=&LAST_TASK;p>&FIRST_TASK;--p)
{
if(*p)
{
(*p)->counter=((*p)->counter>>1)+(*p)->priority;//①
}
}
在I/O的进程的那些优先级会升高,而且I/O时间越长的,优先级越高,因为每次c=0了,都要执行counter/2+初值。
经过I/O进程的counter一定会比只执行CPU进程的counter大,所以I/O约束型任务的优先级就上去了,而I/O约束型正是前台进程的特征。即认为经过了I/O就具有前台进程的特征,那么优先级就比只执行CPU的后台进程高。在阻塞队列待得越久,优先级就越高。
综上整理counter的作用
counter保证了响应时间的界
经过IO后,counter就会变大,IO时间越长,counter越大,照顾了IO进程,变相的照顾了前台进程
后台进程一直按照counter轮转,近似SJF调度
- 每个进程只用维护一个counter变量,简单、高效