进程调度基本概念
多道程序的目标就是始终允许某个进程运行以最大化CPU利用率,多个进程通时存在于内存中,操作系统通过进程调度程序按特定的调度算法来调度就绪队列中的进程到CPU,从而最大限度的利用CPU。
需要进行CPU调度的情况可以分为四种:
- 当一个进程从运行状态切换到等待状态时(如I/O请求,wait()调用以便等待一个子进程的结束)
- 当一个进程从运行状态切换到就绪状态时(如出现了中断)
- 当一个进程从等待状态切换到就绪状态时(如I/O完成)
- 当一个进程终止时
如果调度只能发生在第一种和第四种情况下,那么调度方案称为非抢占的或协作的;否则调度方案就是抢占的。
调度准则:
- CPU使用率
- 吞吐量
- 周转时间
- 响应时间
- 等待时间
进程调度算法
先到先服务调度(FCFS)
先请求CPU的进程首先分配到CPU。FCFS策略可以通过FIFO队列容易的实现,当一个进程进入就绪队列时它的PCB被链接到队列尾部。当CPU空闲时,它会分配给位于就绪队列头部的进程,并将这个进程从队列中移除。
缺点:平均等待时间很长
进程 | 执行时间 |
---|---|
24 | |
3 | |
3 |
如果上面三个进程顺序到达,且按FCFS顺序处理,那么平均等待时间为(0+24+27)/3=17(ms)
如果是:
进程 | 执行时间 |
---|---|
3 | |
3 | |
24 |
那么平均等待时间就是(6+0+3)/3=3(ms),所以进程的CPU执行时间变化波动很大的话,那么采用这种调度策略的平均等待时间变化也会很大。
另外,考虑动态情况下的FCFS调度性能,假设一个CPU密集型进程和多个I/O密集型进程随着进程在系统中的运行,可能发生:CPU密集进程得到CPU,并使用。在这段时间中,所有其他进程会处理他们的I/O,并转移到就绪队列来进行等待CPU。当这些进程在等待时,I/O设备一直处于空闲状态。当CPU密集型进程完成CPU执行并移到I/O设备,所有I/O密集型进程,由于只有很短的cpu执行,所以很快执行完并一会到I/O队列。这时CPU处于空闲。这种由于其他进程都等待一个大进程释放CPU的现象叫做护航效果。
注意,FCFS调度算法是非抢占式的,一旦CPU分配给了一个进程,该进程就会使用CPU直到释放为止,所以FCFS不适用于分时系统。
最短作业优先调度(SJF)
该算法将每个进程与下次CPU执行的长度关联起来。当CPU空闲时,它会被赋给具有最短CPU执行的进程。如果两个进程具有同样长度的CPU执行,可以有FCFS来处理。
可以证明SJF调度算法是最优的。因为对于给定的一组进程,SJF算法的平均等待时间最小。通过将短进程移到长进程之前,短进程的等待时间减少大于长进程的等待时间增加,所以平均等待时间减少。
但是如何才能知道下次CPU执行的长度?对于批处理系统的长期调度,可将用户提交作业时指定的进程时限作为长度。SJF调度常用于长期调度。
虽然SJF是最优的,但是它不能在短期CPU调度级别上加以实现,因为没有办法知道下次CPU执行的长度。一种方法是试图近似SJF调度,通过计算下一个CPU执行长度的近似值,可以选择具有预测最短CPU执行的进程来运行。
下次CPU执行通常预测为以前CPU执行的测量长度的指数平均。设为第n个CPU执行长度,设为下次CPU执行预测值。对于,0 1,定义:
= + (1 - )
值包括最近信息,而存储了过去历史,参数控制最近和过去历史在预测中的权重。如果=0,那么 =,最近历史没有影响(当前情形是瞬态);如果=1,那么= ,只有最近CPU执行才重要(过去历史被认为是陈旧的,无关的)。一般情况下,定义=。
为了理解指数平均行为,通过替换,可以展开,从而得到:
= + (1 - ) + … + β + …+
通常和(1 - )小于1,所以后面项的权重值比前面项的权重要小。
SJF算法可以是抢占的也可以是非抢占的。新进程的下次CPU执行,与当前运行进程的尚未完成的CPU执行相比,可能还要小,抢占SJF算法会抢占当前运行进程,而非抢占SJF算法会允许当前运行进程以先完成CPU执行。抢占SJF调度有时称为最短剩余时间优先调度
例子
进程 | 到达时间 | 执行时间 |
---|---|---|
0 | 8 | |
1 | 4 | |
2 | 9 | |
3 | 5 |
按照SJF调度,这个例子的平均等待时间是[(10 - 1) + (1 - 1) + (17 - 2) + (5 - 3)]/4=26/4=6.5ms。如果使用非抢占SJF调度算法,平均等待时间为7.75ms。
优先级调度
SJF算法就是通过优先级调度算法的一个特例,每个进程都有一个优先级与之关联,而具有最高优先级的进程会被分配到CPU,具有相同优先级的进程按照FCFS顺序调度。
优先级的定义可以是分为内部的或外部的。内部定义的优先级采用一些测量数据来计算进程优先级。外部定义的优先级采用操作系统之外的准则,如进程重要性等。
优先级调用可以是抢占式的也可以是非抢占式的。对于抢占式的那么较高优先级进程来时会抢占CPU;对于非抢占式的,只是将新进程加到就绪队列的头部。
优先级调度算法的一个主要问题就是无穷堵塞或饥饿。即让某个低优先级进程无穷等待CPU。常见的解决方式是老化,老化逐渐增加在系统中等待很长时间的进程的优先级。
轮转调度(RR)
轮转调度算法是专门为分时系统设计德尔,类似与FCFS调度,并增加了抢占以切换进程。将一个较小的时间单元定义为时间量或时间片。时间片的大小通常为10ms-100ms。就绪队列作为一个循环队列,CPU调度程序循环整个就绪队列,为每个进程分配不超过一个时间片的CPU。
在RR调度算法中,没有进程会被连续分配超过一个时间片的CPU(除非是唯一可运行的进程),如果进程的CPU执行超过了一个时间片,那么该进程会被抢占,并被放回就绪队列中,所以RR调度算法是抢占的。
RR调度算法的性能很大程度上取决于时间片的大小,如果时间片非常大,那么RR算法与FCFS算法一样,如果时间片很小,那么会造成大量的上下文切换。如果上下文切换时间约为时间片的10%,那么越10%的CPU时间会被浪费在上下文切换上,上下文切换的时间一般少于10ms。
多级队列调度
多级队列调度算法就是将就绪队列分成多个单独队列,根据进程属性(如内存大小,进程优先级等)将每个进程永久分到一个队列中,每个队列有自己的调度算法。此外,队列之间应该页存在调度,通常采用固定优先级抢占调度。
多级反馈队列调度
在多级队列调度算法中,进程被永久的分配给一个队列,这种设置的优点是调度开销小,缺点是不够灵活。
多级反馈队列调度算法允许进程在队列之间迁移。这种想法是根据不同CPU执行的特点来区分进程,如果进程使用过多的CPU时间,那么将会被迁移到更低优先级的队列。这种方案将I/O密集型和交互进程放在更高优先级队列上。此外在较低优先级队列中等待过长的进程会被一到更高优先级队列中,这种形式的老化阻止饥饿的发生。
通常情况下,多级反馈调度程序可由下列参数来进行定义:
队列的数量
每个队列的调度算法
用以确定何时升级到更高优先级队列的方法
用以确定何时降级到更低优先级队列的方法
用以确定进程在需要服务时将会进入哪个队列中的方法
线程调度
在支持线程的操作系统上,内核级线程才是操作系统所调度的。用户级线程是由线程库来进行管理的,而内核并不知道他们。用户级线程为了运行在CPU上最终应映射到相关的内核级线程上,这种映射不是直接的,可能采用轻量级进程(LWP)。
用户级线程和内核级线程的一个区别就是他们如何调度的。
对于多对一和多对多模型的系统线程库会调用用户级线程,以便在可用的LWP上运行。这种方案成为进程竞争范围(PCS),因为竞争CPU是发生在同一进程的线程之间。
为了决定哪个内核级线程调度到一个处理器上,内核采用系统竞争范围(SCS)。采用SCS调度来竞争CPU,发生在系统内所有线程之间。采用一对一模型的系统,如Window、Linux、Solaris,只采用SCS调度。
通常情况下,PCS通常采用优先级调用,即调度程序选择具有最高优先级的,可运行的线程,且允许一个更高优先级的线程来抢占当前运行的线程。
Pthreads调度
在通过POSIX Pthreads来创建线程时允许指定PCS或SCS。Pthreads采用如下竞争范围的值:
PTHREAD_SCOPE_PROCESS :按PCS来调度线程
PTHREAD_SCOPE_SYSTEM:按SCS来调度线程
对于实现多对多模型的系统,PTHREAD_SCOPE_PROCESS策略调度用户级线程可用LWP,LWP的数量通过线程库来维护。PTHREAD_SCOPE_SYSTEM调度策略会创建一个LWP,并将多对多模型系统的每个用户级线程绑定到LWP,实际采用一对一策略来映射线程。
Pthreads IPC提供两个函数,用来获取和设置竞争范围策略:
pthread_attr_setscope(pthread_attr_t *attr, int scope)
pthread_attr_getscope(pthread_attr_t *attr, int *scope)
这两个函数第一个参数是包含线程属性值的指针,pthread_attr_setscope()第二个参数的值是PTHREAD_SCOPE_PROCESS或PTHREAD_SCOPE_SYSTEM
,pthread_attr_getscope()第二个参数的值是int值的指针,用于获取竞争范围的当前值。
示例:
#include <stdio.h>
#include <pthread.h>
#define NUM_THREADS 5
void *runner(void *param);
int main(int argc, char *argv[])
{
int i, scope;
pthread_t tid[NUM_THREADS];
pthread_attr_t attr;
pthread_attr_init(&attr);
if(pthread_attr_getscope(&attr, &scope) != 0) {
fprintf(stderr, "Unable to get scheduling scope
");
}
else {
if(scope == PTHREAD_SCOPE_PROCESS) {
printf("PTHREAD_SCOPE_PROCESS
");
}
else if(scope == PTHREAD_SCOPE_SYSTEM) {
printf("PTHREAD_SCOPE_SYSTEM
");
}
else {
fprintf(stderr, "Illegal scope value.
");
}
}
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
for(i = 0; i < NUM_THREADS; i++) {
pthread_create(&tid[i], &attr, runner, NULL);
}
for(i = 0; i < NUM_THREADS; i++) {
pthread_join(tid[i], NULL);
}
return 0;
}
void *runner(void *param)
{
int i;
for(i = 0; i<500000; i++) {};
printf("this is thread
");
pthread_exit(0);
}