2.4 调度 Scheduling

在多任务系统中,如果在同一时刻有多个进程或者线程需要运行,但是只有有限的 CPU 资源,就需要决定接下来应当运行哪些进程(线程)。这种操作系统的机制就叫做调度,其对应的算法就叫做调度算法。

2.4.1 调度简介 Introduction to Scheduling

在一些早期的机器上,CPU 是稀缺资源,良好的调度可以平衡用户体验和进程性能。

随着 PC 的发展,情况在两方面发生变化。

  1. 同一时间只有一个活跃进程,调度器很容易就能找到哪个进程正在运行;
  2. CPU 速度越来越快,IO 成为限制瓶颈。调度不再那么重要。

但是在服务器上,有大量的 CPU 密集进程需要运行,调度仍然很重要。

在移动设备上,CPU 内存 电量等资源仍然是稀缺资源,需要通过调度来优化管理。

此外很重要的一点,进程上下文切换的开销很大。

进程行为 Process Behavior

IO: 指进程进入等待状态,需要等待其他进程(设备)完成工作。

CPU 受限:
占用 CPU 时间长,很少进入 IO 等待状态。

IO 受限:
占用 CPU 时间短,经常进入 IO 等待状态。

判断一个进程是 CPU 受限还是 IO 受限,关键在于占用 CPU 资源的多少(时间的长短)。IO 受限的进程之所以受限于 IO,是因为它在两次 IO 请求之间并没有多少 CPU 占用,而不是它有长时间的 IO 请求。

何时调度 When to Scheduling

调度的一个关键点在于,什么时候做调度

  1. 当创建一个新进程时,调度决定先运行父进程还是子进程。
  2. 当一个进程退出时,调度决定下一个运行的进程。
  3. 当进程被 IO 阻塞、等待信号量或者因为一些其他原因,另一个进程需要运行时。
  4. 当 IO 完成时,设备向上抛出 IO 终端,等待 IO 完成的进程应当被调度。

硬件时钟会周期性地提供中断。当时钟中断发生时,调度算法需要处理时钟中断。根据如何处理时钟中断,调度算法被分为两类:

  1. 抢占式调度
  2. 非抢占式调度

非抢占式调度:
非抢占式式调度选择一个进程后,该进程会一直运行,直到被阻塞(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

\[平均等待时间 = \frac{0 + 2 + 6 + 7 + 8}{5} = 4.6 \]

但是如果这些任务同时进入等待队列,那么按照 C,D,E,A,B 的顺序执行:

\[ 平均等待时间 = \frac{0 + 1 + 2 + 4 + 8}{5} = 3 \]

本质上是一种贪心算法,只能做到局部最优。

最短剩余时间下一个执行 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 受限的任务时,长时间片更有利于提高效率(短时间片会导致更多的上下文切换,反而更慢)

算法如下:

  1. 使用优先级划分进程
  2. 给优先级最高的分组一个时间片,优先级第二高的分组两个时间片,第三高的分组四个时间片,八个,十六个……以此类推
  3. 当一个进程用完所有的时间片后,将它的优先级下调,移动到下一个分组

随着进程运行时间的增加,它的优先级会越来越低,被调度的次数也越来越少,从而节省 CPU 资源以用于短时间的交互进程。

最短进程优先 Shortest Process Next

基本思想和批处理系统的最短任务优先调度算法一样,需要注意的是,如何确定哪个任务是最短的?

假设一个任务第 n 次被执行时,使用的时间为 Tn,给定一个常数 \(a\in(0, 1)\),那么预测下一次执行该任务的时间为:

\[T_{n+1} = aT_n + a^2T_{n-1} + a^3T_{n-2} + ... + a^n T_1 + a^n T_0 \]

即:

\[ T_{n+1} = \sum_{i=1}^n(a^i T_{n-i+1}) + a^n T_{0} \]

该算法称之为老化算法(aging),a 为老化因子,一般取 \(\frac{1}{2}\) 即可。

保证调度 Guaranteed Scheduling

在一个系统中,有 n 个平等的进程同时运行,调度算法应当确保每个进程都分到 \(\frac{1}{n}\) 的 CPU 资源。

记录每个进程从被创建开始所使用的 CPU 时间 \(cpu\_time\_used\),以及应当分配给该进程的 CPU 时间 \(cpu\_time\_allocated\),以此计算一个 \(ratio\)

\[ ratio = \frac{cpu\_time\_used}{cpu\_time\_allocated,} \]

每次调度时,都选取 \(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)

\[\sum_{i=1}^{m} \frac{C_i}{P_i} \le 1 \]

举例:
假设存在两个事件流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,这样无需修改内存映射和是缓存无效,极大地降低了调度的开销。

用户级线程可以知道每个线程的功能,有利于做出最优的调度。而内核级线程则不知道(可通过分配不同的优先级)。所以,一般来说针对应用程序自身的逻辑,使用用户级线程进行调度更优。

posted @ 2025-04-02 20:45  DantalianLib  阅读(77)  评论(0)    收藏  举报