简介

进程调度器是操作系统的一部分,决定了何时运行什么进程。它通常能够暂停一个运行中的进程,将它放回到运行队列当中,并运行一个新进程,我们把这样的调度器叫做抢占调度器。否则,它就是协同调度器。

不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。

  • 批处理系统

    批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从 提交到终止的时间)。

    • 先来先服务 first-come first-serverd(FCFS) 非抢占式的调度算法,按照请求的顺序进行调度。 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行, 而长作业又需要执行很长时间,造成了短作业等待时间过长。
    • 短作业优先 shortest job first(SJF) 非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来, 那么长作业永远得不到调度。
    • 最短剩余时间优先 shortest remaining time next(SRTN) 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时, 其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前 进程,运行新的进程。否则新的进程等待。
  • 交互式系统

    交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。

    • 时间片轮转,将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程, 该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该 进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 时间片轮转算法的效率和时间片的大小有很大关系: 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 而如果时间片过长,那么实时性就不能得到保证。

    • 优先级调度,为每个进程分配一个优先级,按优先级进行调度。 为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

    • 多级反馈队列, 一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。 多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间 片大小都不同,例如 1,2,4,8,…。进程在第一个队列没执行完,就会被移到下一个队列。这种 方式下,之前的进程只需要交换 7 次。 每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能 调度当前队列上的进程。 可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

短作业优先调度算法模拟

预期目标和分析

短作业优先,顾名思义,会按估计运行时间最短的顺序进行调度,当多个进程竞争资源时,我们会让需要运行时间最短的进程优先,如果有多个耗时最短的进程,那我们就运行最早进入的那一个。

在单处理机系统中,根据题目要求,我们需要预先输入各个进程的状态,例如名称、到达时间、需要时间、已用CPU时间等,随后我们根据给定的信息,求出执行的先后顺序、各个进程的到达运行开始顺序、周转时间等相关信息。

由于一个进程中存储了名称、运行时间等不同类型的信息,我们决定还是将其封装成一个单独的对象处理起来会更加直观一些。

此外,短作业优先调度虽然原理很简单,但是却不能用较为简单的排序一次做出答案,每个进程的执行顺序和其进程的开始时间、耗时都有关系,而且无法直接通过两两比较来确定优先。例如有三个进程其开始时间和耗时分别为:[0, 10], [3, 7], [9, 1],在第一个任务运行期间第二第三个任务分别进入等待列表,但是当一号任务结束时,却是三号任务被优先执行,虽然二号先进入,但是仍然无法获得分配,如果此类事件再叠加嵌套问题就更复杂了,单纯的比较耗时和开始时间是无法解决这类问题的。所以我们最后还是选择使用模拟的方式来解决问题,维护一个优先队列,每当上一个任务执行完毕,我们就根据时间戳将等待着的任务加入队列,并从中取出最符合目标的进程对象,随后重复上述步骤。

为了避免处理下标,也为了符合先来后到的顺序,我们可以直接将所以任务以队列的形式存储,每次更新curTime后按照时间顺序,向优先队列中存入正在等待的任务。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.company;

import java.util.*;

public class sjf {
static class process {
String name;
int startTime;
int costTime;

public process(String name, int startTime, int costTime) {
this.name = name;
this.startTime = startTime;
this.costTime = costTime;
}

@Override
public String toString() {
return "process{" +
"name='" + name + '\'' +
", startTime=" + startTime +
", costTime=" + costTime +
'}';
}
}

public process[] getInput(Scanner scanner) { //输入相关信息
System.out.println("需要添加的进程数量:");
int n = scanner.nextInt();
process[] list = new process[n];
System.out.println("接下请输入进程名称、开始时间、耗时。\n e.g. a 1 3");
for (int i = 1; i <= n; i++) {
System.out.println("请输入第" + i + "个进程相关信息");
String name = scanner.next();
int startTime = scanner.nextInt();
int costTime = scanner.nextInt();
list[i - 1] = new process(name, startTime, costTime);
}
return list;
}

public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
process[] list = new sjf().getInput(scanner);
Queue<process> queue = new ArrayDeque<>();
for (process x : list) queue.offer(x); //创建任务队列
Arrays.sort(list, Comparator.comparingInt(o -> o.startTime)); //根据开始时间进行排序
PriorityQueue<process> pq = new PriorityQueue<>((o1, o2) -> o1.costTime == o2.costTime ? o1.startTime - o2.startTime : o1.costTime - o2.costTime);
//优先队列中优先耗时,如果耗时相同再考虑开始时间
int curTime = 0;
while (!pq.isEmpty() || !queue.isEmpty()) {
while (!queue.isEmpty() && queue.peek().startTime <= curTime) {
//每一轮任务结束后,将开始时间小于当前时间也就是正在等到中的任务从queue中取出,放入优先队列中
pq.offer(queue.poll());
}
if (pq.isEmpty()) { //如果优先队列没有取到任何等待任务,说明上一轮endTime和下一轮的任务startTime断开了,我们手动补上
curTime = queue.peek().startTime;
while (!queue.isEmpty() && queue.peek().startTime == curTime) {
pq.offer(queue.poll());
}
}
//接下来从优先队列按照时间最短原则,取出任务,开始处理
process curProcess = pq.poll();
System.out.println("处理任务:" + curProcess.name + ",开始时间:" + curTime + ",完成时间:" + (curTime + curProcess.costTime));
System.out.println(curProcess);
curTime += curProcess.costTime;
}

}
}

具体执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
需要添加的进程数量:
4
接下请输入进程名称、开始时间、耗时。
e.g. a 1 3
请输入第1个进程相关信息
a 0 10
请输入第2个进程相关信息
b 1 3
请输入第3个进程相关信息
c 9 1
请输入第4个进程相关信息
d 100 200
处理任务:a,开始时间:0,完成时间:10
process{name='a', startTime=0, costTime=10}
处理任务:c,开始时间:10,完成时间:11
process{name='c', startTime=9, costTime=1}
处理任务:b,开始时间:11,完成时间:14
process{name='b', startTime=1, costTime=3}
处理任务:d,开始时间:100,完成时间:300
process{name='d', startTime=100, costTime=200}

时间片调度算法模拟

预期目标和分析

很容易的看出来,时间片调度算法就是当进程消耗完了时间片,就搁置当前任务,并进入到队列的下一个任务,我们可以直接将当前任务放到队尾,以便下次分配时间片,同时也保证了公平。总的来看,队列这一数据结构就很符合我们的需要。但是同时我们也要注意到随着任务的消化,仍然会有新的任务加入进来,所以要注意顺序。

代码

代码就更加简单了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.company;

import java.util.*;

public class timeSlice {
static class process{
String name;
int startTime;
int costTime;
int remainTime;
int times = 0;
public process(String name, int startTime, int costTime){
this.name = name;
this.startTime = startTime;
this.costTime = costTime;
remainTime = costTime;
times = 0;
}
}

public process[] getInput(Scanner scanner) { //输入相关信息
System.out.println("需要添加的进程数量:");
int n = scanner.nextInt();
process[] list = new process[n];
System.out.println("接下请输入进程名称、开始时间、耗时。\n e.g. a 1 3");
for (int i = 1; i <= n; i++) {
System.out.println("请输入第" + i + "个进程相关信息");
String name = scanner.next();
int startTime = scanner.nextInt();
int costTime = scanner.nextInt();
list[i - 1] = new process(name, startTime, costTime);
}
return list;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
process[] list = new timeSlice().getInput(scanner);
System.out.println("请指定时间片的长度:");
int slice = scanner.nextInt();
Arrays.sort(list, Comparator.comparingInt(o -> o.startTime));
Queue<process> queue = new ArrayDeque<>();
for(process x: list) queue.offer(x);
Queue<process> taskQueue = new ArrayDeque<>();
int curTime = 0;
while(!queue.isEmpty() || !taskQueue.isEmpty()){
if(taskQueue.isEmpty()){ //如果区间断开了,我们自己手动补上
curTime = queue.peek().startTime;
while(!queue.isEmpty() && curTime == queue.peek().startTime){
taskQueue.offer(queue.poll());
}
}
//处理队首任务
process curProcess = taskQueue.poll();
curProcess.remainTime = curProcess.remainTime > slice? curProcess.remainTime - slice: 0;
curProcess.times++;
System.out.println("第" + curProcess.times + "次处理任务" + curProcess.name + ",开始时间:" + curTime +
",结束时间:" + (curTime + Math.min(curProcess.remainTime, slice)) + ",目前还需时间:" + curProcess.remainTime);
curTime += Math.min(curProcess.remainTime, slice);
//更新完成后时间
while(!queue.isEmpty() && queue.peek().startTime <= curTime){
//更新完成后的任务队列,有新的任务可能进来了
taskQueue.offer(queue.poll());
}
//最后有可能当前任务还没有完成,如果没有完成就放回队尾,这里注意和上方更新操作的顺序
if(curProcess.remainTime > 0) taskQueue.offer(curProcess);
}
}
}

执行情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
需要添加的进程数量:
2
接下请输入进程名称、开始时间、耗时。
e.g. a 1 3
请输入第1个进程相关信息
a 100 100
请输入第2个进程相关信息
b 130 100
请指定时间片的长度:
50
第1次处理任务a,开始时间:100,结束时间:150,目前还需时间:50
第1次处理任务b,开始时间:150,结束时间:200,目前还需时间:50
第2次处理任务a,开始时间:200,结束时间:200,目前还需时间:0
第2次处理任务b,开始时间:200,结束时间:200,目前还需时间:0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
需要添加的进程数量:
2
接下请输入进程名称、开始时间、耗时。
e.g. a 1 3
请输入第1个进程相关信息
a 100 100
请输入第2个进程相关信息
b 300 100
请指定时间片的长度:
50
第1次处理任务a,开始时间:100,结束时间:150,目前还需时间:50
第2次处理任务a,开始时间:150,结束时间:150,目前还需时间:0
第1次处理任务b,开始时间:300,结束时间:350,目前还需时间:50
第2次处理任务b,开始时间:350,结束时间:350,目前还需时间:0

Process finished with exit code 0

总结

遇到类似问题,在时间充裕的情况下还是使用模拟会更加好一些。

在本文中,我使用了Java语言配合优先队列完成了SJF的调度策略,随后利用了队列自身的特性完成了时间片调度的实现,相关策略本身实现并不难,但是如果在执行任务的过程中不断有新的任务的加入,相关的处理逻辑就会更加复杂一些,尤其是部分操作的先后顺序需要把握好。此外如果单纯使用按照刻度的最小划分一秒一秒的去刷新检查模拟,对资源是一种巨大的浪费,虽然可以简化逻辑,但是不应当被使用。我们还是需要利用区间自带的开始结束时间,有选择的进行跳跃。