35 进程优先级与实时调度

Nice 值

范围是 +19~-20,越低优先级越高,默认值为 0。它会影响时间片轮转算法下进程分到的时间长度。

  1. 资源限制参数、系统调用的返回值等场景可能会对 nice 值做一些简单的变换,而不是直接使用 +19~-20 这个范围。
  2. Nice 值可以通过系统调用设置、也有相关的命令。

实时优先级

调度策略如下:

  1. SCHED_OTHER
  2. SCHED_RR
  3. SCHED_FIFO
  4. SCHED_BATCH(Linux 2.6 增加,非标准)
  5. SCHED_IDLE(Linux 2.6 增加,非标准)

第 1 种是默认的时间片轮转策略,2、3 是实时调度策略,优先级都大于 SCHED_OTHERSCHED_BATCHSCHED_IDLE 也不是实时调度策略,调度方式和 SCHED_OTHER 类似:SCHED_BATCH 会导致频繁唤醒的任务调度次数减少,SCHED_IDLE 等价于一个非常低的 nice 值(比 +19 的优先级还要低)。

Linux 上实时优先级从 1 到 99 供 99 种,越高越优先(其他 UNIX 实现中的实时优先级取值范围可能不同,而且也不要求 SCHED_RRSCHED_FIFO 使用相同的范围,但 Linux 等多数 UNIX 实现用的是同一个范围)。不管进程的调度策略是 SCHED_RR 还是 SCHED_FIFO,只要实时优先级相同,那它们就有相同的调度优先级。系统总是会优先调度高实时优先级的策略(可能有饥饿现象,而且存在抢占式调度),并为每个优先级都维护了专门的队列。

在多核操作系统中,不同的 CPU 核上有不同的调度队列,因而在 CPU 0 上调度的程序即便是比 CPU 1 上调度的程序更加优先,也必须在 CPU 0 上继续等待前面的程序。

SCHED_RR 调度策略

还是用的时间片轮转,区别是有高实时优先级。区别:如果都使用 SCHED_OTHER 调度策略,那么优先级较低的进程也总能有机会获取到 CPU。

使用 SCHED_RR 调度策略的进程会失去 CPU 的情况:

  1. 时间片用完
  2. 自愿放弃 CPU(可能是由于执行了一个阻塞式系统调用或调用了 sched_yield() 系统调用)
  3. 终止
  4. 被优先级更高的进程抢占

在前两种情况下,进程会回到同一个优先级的调度队列的队尾。在被高优先级进程抢占的时候,进程会保留其位置,等高优先级进程让出 CPU 的时候接着执行(沿用之前剩下的时间片)。

一个进程可能会被其他进程抢占 CPU 的情况:

  1. 更高优先级的进程阻塞解除(比如 I/O 结束)
  2. 另外一个进程的优先级被提高,超过了当前进程
  3. 当前进程的优先级被降低,低于了某个可运行的其他进程

SCHED_FIFO 调度策略

SCHED_RR 类似,区别是没有时间片的概念,因此会一直运行直到:

  1. 自愿放弃 CPU
  2. 终止
  3. 被抢占

防止实时进程锁住系统

因为高实时优先级的进程可能会导致其他进程饥饿甚至无法运行,所以我们需要用一些手段防治异常的实时进程:

  1. 创建一个有高实时优先级的看门狗进程,获取实时进程的状态信息并决定是否需要终止它们。
  2. 创建实时进程时,使用 alarm() 为实时进程创建计时。
  3. setrlimit() 设置资源限制 RLIMIT_CPU,如果 CPU 用量超过这个值,则进程会收到 SIGXCPU 信号而终止。
  4. setrlimit() 设置资源限制 RLIMIT_RTTIME,如果一个实时调度策略的进程在不被阻塞的情况下连续运行超过这个时间(时间片用完、被抢占、调用 sched_yield 都不能使这个统计重置,只有因系统调用而阻塞可以),则进程会收到 SIGXCPU 信号而终止。这个资源限制是 Linux 提供的、非标准的。

在达到 RLIMIT_CPU 或者 RLIMIT_RTTIME 软限制值之 后,Linux 内核会在进程每消耗一秒钟的 CPU 时间后向其发送一个 SIGXCPU 信号(可以通过注册信号处理器来处理)。 当进程持续执行直至达到硬 CPU 限制时,内核会向其发送一个 SIGKILL 信号,该信号总是会终止进程。

避免子进程继承实时调度策略

Linux 2.6.32 增加了一个 SCHED_RESET_ON_FORK,在调用 sched_setscheduler() 时可以将其作为 policy 的标志。使用时需要将这个标志位和 SCHED_OTHERSCHED_RRSCHED_FIFO……之一做按位与运算。

效果是 fork() 时:

  1. 如果当前进程用的是实时调度策略,则重置子进程的调度策略为 SCHED_OTHER
  2. 如果当前进程的 nice 值是负数(高优先级),则重置为 0。
  3. 子进程的 SCHED_RESET_ON_FORK 会被清掉。

一点进程被设置了这个标志,只有特权进程能够将其撤下。

这个标志的好处:有些进程被设置了实时资源限制(RLIMIT_RTTIME),可能会使用疯狂创建子进程的方式来尝试逃脱限制,但如果有这个标志,创建出来的子进程就不会使用实时调度策略了。

释放 CPU

可以使用 sched_yield() 来主动释放 CPU(如果使用了阻塞的系统调用也是会释放 CPU 的)。如果当前进程所在的调度队列还有别的进程,那么调用进程会来到队尾。否则,调用进程继续使用 CPU。

非实时进程使用 sched_yield() 的结果是未定义的

实时时间片

通过 sched_rr_get_interval() 可以找出调度策略为 SCHED_RR 的进程可以分到的时间片长度。在 Linux 2.6 中,这个值是 0.1s。

CPU 亲和力

Linux 提供了非标准的系统调用来修改 CPU 亲和力。CPU 亲和力是线程级属性。

用一个 cpu_set_t * 类型的参数设置亲和力,含义为要求线程只能在集合中的 CPU 上运行。默认的 CPU 亲和力是可以在任何一个 CPU 上运行(不过系统一般会优先保持任务原先的 CPU 以减少 cache misses)。

sched_getaffinity() 执行时不会进行权限检查,非特权进程能够获取系统上所有进程的 CPU 亲和力掩码。

通过 fork() 创建的子进程会继承其父进程的 CPU 亲和力掩码并且在 exec() 调用之间掩码会得以保留。