2.4 调度 Scheduling
在多任务系统中,如果在同一时刻有多个进程或者线程需要运行,但是只有有限的 CPU 资源,就需要决定接下来应当运行哪些进程(线程)。这种操作系统的机制就叫做调度,其对应的算法就叫做调度算法。
2.4.1 调度简介 Introduction to Scheduling
在一些早期的机器上,CPU 是稀缺资源,良好的调度可以平衡用户体验和进程性能。
随着 PC 的发展,情况在两方面发生变化。
- 同一时间只有一个活跃进程,调度器很容易就能找到哪个进程正在运行;
- CPU 速度越来越快,IO 成为限制瓶颈。调度不再那么重要。
但是在服务器上,有大量的 CPU 密集进程需要运行,调度仍然很重要。
在移动设备上,CPU 内存 电量等资源仍然是稀缺资源,需要通过调度来优化管理。
此外很重要的一点,进程上下文切换的开销很大。
进程行为 Process Behavior
IO: 指进程进入等待状态,需要等待其他进程(设备)完成工作。
CPU 受限:
占用 CPU 时间长,很少进入 IO 等待状态。
IO 受限:
占用 CPU 时间短,经常进入 IO 等待状态。
判断一个进程是 CPU 受限还是 IO 受限,关键在于占用 CPU 资源的多少(时间的长短)。IO 受限的进程之所以受限于 IO,是因为它在两次 IO 请求之间并没有多少 CPU 占用,而不是它有长时间的 IO 请求。
何时调度 When to Scheduling
调度的一个关键点在于,什么时候做调度。
- 当创建一个新进程时,调度决定先运行父进程还是子进程。
- 当一个进程退出时,调度决定下一个运行的进程。
- 当进程被 IO 阻塞、等待信号量或者因为一些其他原因,另一个进程需要运行时。
- 当 IO 完成时,设备向上抛出 IO 终端,等待 IO 完成的进程应当被调度。
硬件时钟会周期性地提供中断。当时钟中断发生时,调度算法需要处理时钟中断。根据如何处理时钟中断,调度算法被分为两类:
- 抢占式调度
- 非抢占式调度
非抢占式调度:
非抢占式式调度选择一个进程后,该进程会一直运行,直到被阻塞(IO 或者等待其他进程等)或者自愿放弃 CPU。在这一过程中如果发生了时钟中断,调度器不做调度。
抢占式调度:
抢占式调度下,进程有最大运行时间的限制。达到这个限制后,调度器会挂起这个进程并选择另一个可执行的进程运行。当时钟中断发生时,如果进程消耗完了它的时间片,CPU 的控制权会被交还给调度器。
调度算法分类 Categories of Scheduling Algorithms
不同的场景下需要不同的调度算法。
批处理系统:
无需快速响应。非抢占式调度或者长周期的抢占式调度都适合批处理系统。
交互式系统:
处于和用户交互的目的,抢占式的调度是必须的,可以防止一个进程长期占用 cpu。
实时系统:
在实时系统下,有时并不需要抢占机制。因为每个进程都不会运行很长时间,很快就完成了。
2.4.2 批处理系统的调度 Scheduling in Batch System
先到先服务 First-Come, First Served
使用队列维护将要执行的任务,新任务被添加到队尾,优先执行队头的任务,直到该任务完成或者阻塞。
当任务被阻塞时,调度执行下一个任务,等到被阻塞的任务再次就绪时,添加到队尾等待调度。、
先到先服务调度算法的劣势显而易见,短任务会被长任务阻塞,IO 任务会被 CPU 任务阻塞。
假设 IO 密集任务只占用很少的 CPU 时间,并且存在大量 IO 任务,而 CPU 密集任务需要占用大量 CPU 时间,偶尔会阻塞一下
当 IO 任务进入阻塞状态后,先到先服务调度算法会调度 CPU 密集任务
但是 CPU 密集任务会需要执行很长时间才会进入阻塞状态,而 IO 密集任务的 IO 请求早已经完成,但是不得不等待 CPU 任务调度结束
最短任务优先 Shortest Job First
调度开始时,从等待队列中挑选耗时最少的任务执行,并且不允许抢占。
注意,只有所有任务同时进入等待队列,最短任务优先调度才是最优的。
例:任务 A ~ E 在 分别在 0,0,3,3,3 时刻到达,分别耗时 2,4,1,1,1,那么最短任务优先调度的任务时间线如下:
| 时间线 | A | B | C | D | E |
|---|---|---|---|---|---|
| 0 | 2 | 4 | - | - | - |
| 1 | 1 | 4 | - | - | - |
| 2 | 0 | 4 | - | - | - |
| 3 | 0 | 3 | 1 | 1 | 1 |
| 4 | 0 | 2 | 1 | 1 | 1 |
| 5 | 0 | 1 | 1 | 1 | 1 |
| 6 | 0 | 0 | 1 | 1 | 1 |
| 7 | 0 | 0 | 0 | 1 | 1 |
| 8 | 0 | 0 | 0 | 0 | 1 |
| 9 | 0 | 0 | 0 | 0 | 0 |
但是如果这些任务同时进入等待队列,那么按照 C,D,E,A,B 的顺序执行:
本质上是一种贪心算法,只能做到局部最优。
最短剩余时间下一个执行 Shortest Remaining Time Next
将最短任务有限修改为可抢占的,那就是最短剩余时间的任务优先。
调度开始时,从等待队列中挑选耗时最少的任务执行,如果有新的任务进入队列,允许执行调度。
2.4.3 交互系统中的调度 Scheduling in Interactive Systems
轮转调度 Round-Robin Scheduling
给每个进程分配一个时间片,在这个时间片内,进程被允许执行,当时间片被消耗完或者进程进入阻塞状态时,调度其他进程继续执行。
轮转算法是很简单,唯一需要注意的点就是选取合适的时间片长度。
过短的时间片会导致进频繁的上下文切换,大量 CPU 资源消耗在了上下文切换上导致 CPU 资源浪费。
而过长的时间片会增加系统的响应时间。
Priority Scheduling 优先级调度
轮转调度对每个进程都是平等的,所有进程平等地共享 CPU 资源。但是在一些场景下,需要给一些进程更多的 CPU 资源。
给每个进程一个优先级,优先级高的进程先执行。为了防止高优先级的进程一直运行下去,每隔一段时间降低当前正在运行的进程的优先级,那么当这个进程的优先级低于第二高优先级时,调度第二高优先级的进程执行。可以给每个进程分配一个时间片,当时间片消耗完时,执行下一个进程。
进程的优先级可以被静态或者动态地赋予。
IO 密集任务应当被赋予高优先级,这样的话,当 IO 密集认为有需要使用 CPU 时,可以立即获取到,然后进行下一个 IO 请求。
一个简单的算法:IO 密集任务的优先级 = 分配给该任务的时间片 / 上次执行该任务时占用 CPU 的时间
进程占用 CPU 的时间越短,它的优先级就越高,越容易被调度。
一般根据优先级将进程划分为不同的分组,分组和分组之间使用优先级调度算法,而每个分组内部使用轮转调度算法调度每个进程。
多队列调度算法 Multiple Queues
这是一个早期的优先级调度算法,用于 CTSS(Compatible TimeSharing System)。
该系统同时只允许一个进程在内存中,所以进程切换很慢(涉及到磁盘 IO)。
显然,在执行 CPU 受限的任务时,长时间片更有利于提高效率(短时间片会导致更多的上下文切换,反而更慢)
算法如下:
- 使用优先级划分进程
- 给优先级最高的分组一个时间片,优先级第二高的分组两个时间片,第三高的分组四个时间片,八个,十六个……以此类推
- 当一个进程用完所有的时间片后,将它的优先级下调,移动到下一个分组
随着进程运行时间的增加,它的优先级会越来越低,被调度的次数也越来越少,从而节省 CPU 资源以用于短时间的交互进程。
最短进程优先 Shortest Process Next
基本思想和批处理系统的最短任务优先调度算法一样,需要注意的是,如何确定哪个任务是最短的?
假设一个任务第 n 次被执行时,使用的时间为 Tn,给定一个常数 \(a\in(0, 1)\),那么预测下一次执行该任务的时间为:
即:
该算法称之为老化算法(aging),a 为老化因子,一般取 \(\frac{1}{2}\) 即可。
保证调度 Guaranteed Scheduling
在一个系统中,有 n 个平等的进程同时运行,调度算法应当确保每个进程都分到 \(\frac{1}{n}\) 的 CPU 资源。
记录每个进程从被创建开始所使用的 CPU 时间 \(cpu\_time\_used\),以及应当分配给该进程的 CPU 时间 \(cpu\_time\_allocated\),以此计算一个 \(ratio\)
每次调度时,都选取 \(ratio\) 最小的进程执行,直到它不再是最小的。
彩票调度 Lottery Scheduling
保证调度难以实现,可以使用彩票调度平替。
给每一个进程分配一些“彩票号码”,当需要调度时,调度器从中随机选取一些“彩票”,持有这些“彩票”的进程可以获得相应的 CPU 资源。
如果需要给某些进程分配更多的资源,可以增加它们持有的“彩票”数量,提高“中奖”概率。
给一个新创建的进程一些“彩票”,当下一次调度发生时,这个进程就有机会被执行,所以彩票调度是高度响应的。
进程之间可以传递或者交换“彩票”。Client 进程发送请给 Server 后会被阻塞,等待请求完成,Client 可以将它持有的“彩票”交给 Server 增加 Sever 被调度的概率。Server 完成请求后,如法炮制将“彩票”交给 Client。
彩票调度可以解决其他调度算法难以解决的问题。如按比例分配 CPU 资源。
公平份额调度 Fair-Share Scheduling
此前我们调度是在进程层面的,没有考虑到用户层面。如果用户 1 有九个进程,而用户2只有一个进程,那么用户 2 只能分配到 10% 的 CPU 资源。
考虑到用户层面的公平,应当根据用户数量分配 CPU 资源。
假设存在两个用户,用户 1 有进程 A,B,C,用户 2 有进程 D,E
用户之间均分 CPU,那么交替调度每个用户下的进程,调度顺序:
| time | 1 | 2 |
|---|---|---|
| 0 | A | - |
| 1 | - | D |
| 2 | B | - |
| 3 | - | E |
| 4 | C | - |
假设给用户 1 分配的 CPU 资源是用户 2 的两倍,那么每调度 2 个用户 1 的进程,就调度 1 个用户 2 的进程,调度顺序:
| time | 1 | 2 |
|---|---|---|
| 0 | A | - |
| 1 | B | - |
| 2 | - | D |
| 3 | C | - |
| 4 | A | - |
| 5 | - | E |
2.4.4 实时系统中的调度 Scheduling in Real-Time Systems
实时系统对时间延迟极为敏感,即使进程运行正常,但是响应过慢也是问题。
hard real time: 不可超期的时限
soft real time: 允许偶尔超期的时限
可以根据上述特性,将实时系统中的程序划分为两种进程,每一个进程的行为都是可预测的。
当检测到外部事件时,调度器合理地调度进程,以满足每个进程的时限要求。
实时系统需要处理的事件可以划分周期性事件和非周期性事件,系统需要处理多个事件流,根据事件所消耗的时间的不同,不一定可以处理所有的事件。
假设存在 m$$ 个周期性事件流,事件 \(i\) 的周期是 \(P_i\),需要消耗 \(C_i\) 的 CPU 时间,
当且仅当这些事件满足如下条件时,才可以被全部处理,称之为可调度的(schedulable)。
举例:
假设存在两个事件流A,B,周期都是 500ms,每个时间需要消耗 400ms 的 CPU 时间,那么当 CPU 处理完\(A_0\),\(B_0\) 后,此时 \(A_1\) 已经等待了 300ms,继续往下执行,堆积的时间会越来越多,直到超过时间的处理时限。
实时系统的调度算法可静态可动态,静态算法在系统启动之前就定好了调度策略,事先就知道需要做的任务和必须要满足的时限要求。动态调度可以动态获取并计算信息,无需上述约束。
2.4.5 策略和机制 Policy Versus Mechanism
此前,我们默认认为所有的进程都属于不同的用户,所以都在竞争。但是考虑如下情况,一个数据进程下有很多子进程(执行查询、磁盘读写等操作)。主进程明确知道每个进程在做什么,但是调度器并不知道,所以无法对这些进程做出最优的调度。
解决方案就是将调度机制和调度策略分离开来,调度算法以某种形式参数化,而具体的参数由用户决定。调度机制在内核中,而调度策略由用户设置。
基于上述的例子,假设系统使用优先级调度,并且提供了一个系统调用可以设置子进程的优先级,那么父进程就可以控制子进程的调度。
2.4.6 线程调度 Thread Scheduling
内核并不知道用户态线程的存在,所以只会选择一个进程然后执行该进程。进程中的线程调度器决定执行哪个线程,由于没有时钟中断的存在,这个线程会一直运行下去,直到主动放弃 CPU。不过当进程消耗完时间片后,内核还是会调度其他进程。如果线程在消耗完进程的时间片前,先消耗完了自己的时间片,那么线程调度器会调用下一个(该进程的)线程。
此前的所有调度算法都可用于用户级线程的调度,一般是轮转调度和优先级调度。用户级线程调度的唯一限制是没有时钟中断导致线程长时间运行。
内核级线程调度时,内核会选择一个(内核)线程,并不关心它属于哪个进程(如果有需要,内核可以直到该线程属于哪个进程)。超过分配的时间片后,线程被挂起。
内核级线程的上下文切换远大于用户态线程,因为需要修改内存映射和使缓存无效,而用户级线程切换仅仅只需要几个机器指令。内核可以有选择地切换线程,比如从同一个进程的线程 A 切换到线程 B,这样无需修改内存映射和是缓存无效,极大地降低了调度的开销。
用户级线程可以知道每个线程的功能,有利于做出最优的调度。而内核级线程则不知道(可通过分配不同的优先级)。所以,一般来说针对应用程序自身的逻辑,使用用户级线程进行调度更优。

浙公网安备 33010602011771号