本文引用图片均来自 李治军: 操作系统32讲
其实只要给定起始执行地址CPU就能自动的不断往下取指执行,但正如 操作系统(3) 多进程图像 所介绍过的那样,CPU以这种方式工作效率是很低的,所以我们要人为的介入CPU的工作,也就是调度CPU
调度要素
如下图,当正在执行的进程因为读写磁盘阻塞住了,对于CPU来说在磁盘访问结束前它都是无法工作的因为可能进程后面的代码就需要利用从磁盘读取的数据。如果此时不对CPU进行调度,那么CPU在这段时间的等待就会造成资源浪费
此时可以让CPU先执行另一个任务,等IO结束后再回来执行当前任务,这样就避免了等待提高了CPU利用率
调度中如何决定接下来执行哪一个任务呢?我们先看看不同的任务关心什么样的指标
- 对于后台任务,比如GCC编译器,期望在准备就绪到最终被调用执行完成的时间短一些
- 对于前台任务,比如Word,期望响应用户操作的时间短一些
- 对于操作系统,期望时间能够尽量用来执行任务而不是花在诸如切换任务等操作上
这些指标之间相互影响
要想响应时间小就要在不同任务间频繁切换,切换多了系统花在切换上的时间就多,那么用来执行任务的时间也就相应的变少
不同的任务有不同的特点,比如GCC编译器需要进行大量运算是CPU约束型,Word经常要保存文件访问磁盘是IO约束型等等
基于以上我们可以知道调度要考虑的要素很多,侧重这方面就会忽略另一方面,所以在决定下一个执行进程的时候各个要素要综合,折中,因此人们发明了各种各样的调度算法
其实调度算法没有最好的,只有最适合的,比如说个人用操作系统和工业用操作系统要考虑的调度要素就不一样,针对不同使用场景"最好"的调度算法也必然不同
简单调度算法
本节简单介绍几个基本的调度算法
FCFS
FCFS(First Come First Served)先来先服务,非抢占式(当前任务释放CPU其他任务才能获得CPU),队列中的任务谁先来谁先执行
FCFS很简单很容易实现,但缺点也很明显。如上图,只是将P2和P3的顺序调换就将平均周转时间从40.2降到35,但就连这点变化FCFS都做不到
SJF
SJF(Shortest Job First)短作业优先,非抢占式,队列中的任务执行时间短的优先,追求最短平均周转时间
最优性的证明感兴趣可自行搜索相关资料,本文不做介绍
RR
RR(Round Robin)时间片轮转,抢占式(时间片结束CPU会被强制释放),每个任务轮流执行,时间结束就到下一个任务
如上图时间片为10ms,P1执行10ms后轮到P2,P2执行10ms后虽然还未执行完但CPU仍然要转移去执行P3,以此类推
RR算法照顾了每一个任务,避免了任务长时间等待保证了响应时间
以上都是基本的调度算法,只考虑了响应时间或周转时间,实际上我们还要考虑任务是不是等待了很久没执行?是不是紧急任务无论时间长短都优先执行?等等。调度算法还有最高优先级优先、高响应比优先和多级反馈队列等,本文不做一一介绍
Linux中的调度函数
Linux 0.11中的schedule函数代码少却又综合了优先级,时间片和等待时间等因素
schedule (void) {
int i, next, c;
struct task_struct **p;
// 省略信号相关的十来行代码
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c)
break;
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}
switch_to (next);
}
1. 首先从就绪队列末尾往前遍历,找到counter最大(此处表示优先级)的就绪任务,如果能找到则直接跳出循环执行任务
2. 如果所有就绪任务时间片耗尽(此处counter表示时间片),那么更新队列中所有任务的时间片
2.1. 对于就绪任务,当前时间片为0,右移一位(除以2)后仍为0,重置时间片为初始值(priority)
2.2. 对于阻塞任务,当前时间片不为0,右移一位后加上初值,counter增大
对于阻塞任务来说,重新就绪之后会比时间片耗尽后重置counter的任务优先级更高,且阻塞越久优先级越高(上限为2倍初始值)
对于就绪任务来说,当前调度被选择执行后时间片减小,下次调度大概率会选择另一个任务,相当于轮转调度