Skip to content

LV045-调度策略简介

从 Linux 2.6.23 开始,默认的调度器为 CFS,即 "完全公平调度器"(Completely Fair Scheduler)。CFS 调度器取代了之前的 "O(1)" 调度器。CFS 的实现细节可以参考 kernel.org/doc/Documentation/scheduler/sched-design-CFS.txt,cgroup 的 CPU 调度也属于 CFS 扩展的一部分。

主要参考 sched(7) - Linux manual page,执行 man 7 sched 就可以看到这部分手册啦。关于内核调度器,内核官网还有一篇文档:Linux 调度器 — The Linux Kernel documentation

一、调度策略

内核模块使用调度器来决定下一个 CPU 时钟周期执行的线程。每个线程都包含一个调度策略以及一个静态的调度优先级 sched_priority ,调度器根据系统上所有线程的调度策略和静态优先级来决定如何进行调度。

对于使用普通调度策略(SCHED_OTHER, SCHED_IDLE, SCHED_BATCH)的线程来说,sched_priority 并不会影响调度结果,且 必须设置为 0

对于使用实时策略(SCHED_FIFO, SCHED_RR)的进程,其 sched_priority 取值为 1 到 99(1 为最低值)。实时线程的调度优先级总是高于普通线程。注:POSIX.1 的系统在实现中,会要求实时调度策略有 32 个优先级设置,因此,为了可移植性,可以使用 sched_get_priority_minsched_get_priority_max 来查找调度策略所支持的优先级范围。

调度器会为每个 sched_priority 值维护一个可运行的线程列表。调度器通过查看非空且静态优先级最高的列表,并选择该列表首部的元素作为下一个运行的线程。

线程的调度策略决定了如何根据静态优先级来将一个线程插入到同静态优先级的线程列表(list of runnable threads)中,以及如何在该列表中调整线程的位置。

所有的调度都具有抢占性:如果一个具有更高静态优先级的线程准备运行,当前运行的线程会被抢占并返回到其静态优先级对应的等待列表中。调度策略仅根据具有相同静态优先级的可运行线程列表来决定调度顺序。

进程调度中使用了 2 个队列:进程一开始会进入 ready 队列等待调度;当进程执行中遇到 I/O 阻塞,等待子进程结束或软中断等原因会进入 wait 队列,等阻塞结束后会返回到 ready 队列。

1. SCHED_FIFO: First in-first out scheduling(实时线程)

SCHED_FIFO 仅适用于静态优先级大于 0 的线程,即当一个 SCHED_FIFO 的线程变为可运行(runnable)状态时,它会立即抢占所有当前运行的 SCHED_OTHER, SCHED_BATCHSCHED_IDLE 线程。SCHED_FIFO 不使用时间片进行调度,所有使用 SCHED_FIFO 调度策略的线程应该遵守如下规则:

  • 当一个运行中的 SCHED_FIFO 线程被其他有更高优先级的线程抢占后,该线程会返回到其优先级对应的列表的首部,当所有更高优先级的线程阻塞后,该线程将会立即恢复运行;
  • 当一个阻塞的 SCHED_FIFO 线程变为可运行状态时,该线程会返回到其优先级对应的列表末尾;
  • 如果调用 sched_setscheduler(2)sched_setparam(2)sched_setattr(2)pthread_setschedparam(3)pthread_setschedprio(3) (通过 pid)修改了正在运行或可运行状态的 SCHED_FIFO 线程的优先级时,该线程在列表中的位置取决于优先级的变动:

(a) 如果线程优先级增加了,它将会放置到新优先级对应的列表末尾,同时可能抢占正在运行的具有相同优先级的线程;

(b) 如果线程优先级没变,其在运行列表中的位置不变;

(c) 如果线程优先级减小了,它将会放置到新优先级对应的列表的前面。

根据 POSIX.1-2008,通过非 pthread_setschedprio(3) 方式来修改线程的优先级,可能会导致其放置到对应优先级列表的末尾。

  • 调用了 sched_yield(2) (用于释放 CPU) 的线程将会放置到列表末尾

SCHED_FIFO 线程将会一直运行,直到被更高优先级的线程抢占,或调用了 sched_yield(2)

2. SCHED_RR: Round-robin scheduling(轮询调度)

SCHED_RRSCHED_FIFO 做了简单增强。除每个线程仅允许运行在一个最大时间段下外,SCHED_FIFO 中的所有规则都适用于 SCHED_RR。如果一个 SCHED_RR 线程已经运行了等于或大于该最大时间段时,该线程会被放置到其优先级列表的末尾。当一个 SCHED_RR 线程被更高优先级的线程抢占,并在后续恢复运行后,会在先前未过期的时间段下运行。最大时间段可以通过 sched_rr_get_interval(2) 获得。

3. SCHED_DEADLINE: Sporadic task model deadline scheduling

3.14 版本之后的 Linux 提供了一个新的调度策略 SCHED_DEADLINE。该策略结合了 GEDF(Global Earliest Deadline First)和 CBS (Constant Bandwidth Server)。必须通 sched_setattr(2)sched_getattr(2) 来设置和获取该策略。

一个 Sporadic task 被定义为一系列任务,且每个任务每次仅激活一次。每个任务都有一个 relative deadline(该任务应该在该相对时间前停止运行),以及一个 computation time(执行该任务需要的 CPU 时间,对应下图的 comp. time)。一个新的任务开始执行时会唤醒(wakeup)一个 Sporadic task,该时间点被称为 arrival timestart time 为一个任务开始执行的时间,absolute deadline(绝对截止时间)为 arrival time 加上 relative deadline 的时间点。

text
           arrival/wakeup                    absolute deadline
                |    start time                    |
                |        |                         |
                v        v                         v
           -----x--------xooooooooooooooooo--------x--------x---
                         |<- comp. time ->|
                |<------- relative deadline ------>|
                |<-------------- period ------------------->|

当使用 sched_setattr(2) 给一个线程设置 SCHED_DEADLINE 策略时,可以设置 3 个参数:Runtime, Dead‐linePeriod,对于上面提到的场景来说,通常的做法是将 Runtime 设置为大于平均计算时间的值(或更坏的场景下,设置为硬实时任务的执行时间);将 Deadline 设置为对应的 dead-line,将 Period 设置为任务的周期,此时对于 SCHED_DEADLINE 的调度如下:

text
           arrival/wakeup                    absolute deadline
                |    start time                    |
                |        |                         |
                v        v                         v
           -----x--------xooooooooooooooooo--------x--------x---
                         |<-- Runtime ------->|
                |<----------- Deadline ----------->|
                |<-------------- Period ------------------->|

Tips:Runtime 对应上图中的 comp.timeDead-line 对应上图的 relative deadline

3 个 deadline 调度参数对应 sched_attr 结构体中的 sched_run‐time, sched_deadline, 和 sched_period 字段,参见 sched_setattr(2)。这些字段的单位为纳秒。如果 sched_period 的值为 0,则它与 sched_deadline 相同。

内核要求:

text
           sched_runtime <= sched_deadline <= sched_period

此外,在当前实现中,所有参数的值至少为 1024(即,大于 1 微秒),小于 2^63。如果有效性校验失败,sched_setattr(2) 返回 EINVAL 错误。

CBS 通过阻止线程超出其运行时间 Runtime 来保证任务间不互相干扰。

为了确保 deadline 调度,当 SCHED_DEADLINE 线程在给定的条件下不可运行时,此时内核必须阻止这些线程的运行。内核必须在设置或修改 SCHED_DEADLINE 策略和属性时执行准入测试。准入测试用于计算这些修改是否可行,如果不可行,sched_setattr(2) 将返回 EBUSY 错误。

例如,总的 CPU 利用率应该小于或等于总的可用的 CPU。由于每个线程可以在每个 Period 中最大运行 Runtime 时间,线程的 CPU 时间片使用率为 Runtime 除以 Period

为了满足 SCHED_DEADLINE 的条件,使用 SCHED_DEADLINE 策略的线程的优先级是系统中最高的。当一个 SCHED_DEADLINE 线程运行时,该线程会抢占其他策略下调度的线程。

SCHED_DEADLINE 策略调度的线程调用 fork(2) 会返回 EAGAIN 错误(除非该线程设置了 reset-on-fork 标记)。

当一个 SCHED_DEADLINE 线程调用了 sched_yield(2) 将会停止当前任务,并等待新的周期。

4. SCHED_OTHER: Default Linux time-sharing scheduling(默认策略)

SCHED_OTHER 只能在静态优先级为 0 时使用 (普通线程)SCHED_OTHER 是标准的 Linux 分时调度策略(不需要实时机制)。

如何从静态优先级为 0 的列表中选择运行的线程取决于列表中的 dynamic 优先级。dynamic 优先级基于 nice 值,且在每次线程准备运行时增加。这种机制保证公平处理所有的 SCHED_OTHER 线程。

在 Linux 内核源码中,SCHED_OTHER 被称为 SCHED_NORMAL

The nice value

nice 值用于影响 CPU 调度器对进程的调度偏好。适用于 SCHED_OTHER 和 SCHED_BATCH 调度处理。可以通过 nice(2)setpriority(2)sched_setattr(2) 修改 nice 值。

根据 POSIX.1,nice 值是一个单进程属性,即进程中的所有线程共享一个 nice 值。然而,在 Linux 中,nice 值是一个单线程属性,相同进程中的不同线程可能使用不同的 nice 值。

nice 值的取值范围根据 UNIX 系统的不同而不同。在现代 Linux 系统中,取值为-20(高优先级)到+19(低优先级),而一些系统中的取值为-20..20。在一些非常早期的 Linux 内核(Linux 2.0 之前)中的取值为-infinity..15。

nice 值对相应的 SCHED_OTHER 进程的影响根据 UNIX 系统和 Linux 内核版本的不同而不同。

2.6.23 版本的 Linux 内核中引入了 CFS 调度,并采用了一种能根据 nice 的差值产生更显著影响的算法。在当前的实现下,两个进程的 nice 差值中,每单位的 nice 差值对 CFS 调度的影响因子为 1.25 (参见 how-is-nice-working,CFS 根据 vruntime 进行 CPU 调度:vruntime = 实际运行时间 * 1024 / 进程权重进程权重1.25 ^ nice_value)。这种算法使得在有高优先级负载运行的情况下,只能给低 nice 值(+19)的负载提供很小的 CPU;而为高 nice 值(-20)的负载提供其运行应用需要的绝大部分 CPU(如音频应用)。

Tips:

权重表示是该程序需要的 cpu 时间的一种表现,如果一个程序需要大量 cpu 进行处理,可以提高其权重,反之减小其权重。CFS 的思想就是让每个调度实体的 vruntime 互相追赶,而每个线程的 vruntime 增加速度不同,权重越大的增加的越慢,这样就能获得更多的 cpu 执行时间。系统将会根据每个线程的 vruntime 排序(实际上是基于红黑树算法),vruntime 最小的线程会最早获得调度。而一旦 vruntime 的次序发生变化(vruntime 的大小与实际运行时间有关,运行时间越长,其值越大),系统将尝试触发下一次调度。也就是说 调度器尽可能的保证所有线程的 vruntime 都一致,权重高的线程 vruntime 提升的慢(进程权重小,即分子小),容易被优先调度;权重低,同样的时间上 vruntime 上升的快,反而容易被轮空。一个线程的 vruntime 可以通过 /proc/<PID>/sched 中的 se.vruntime 选项查看:grep vruntime /proc/<PID>/sched。vruntime 的增加与进程占用的 CPU 有关,如果一个线程一直处于 sleep 状态,其 vruntime 是不会增加的。那么如果一个 sleep 的线程被唤醒之后,是否会立即抢占 vruntime 比它大的线程?答案是否定的,其最少需要在 cpu 的 run 队列中等待 sched_min_granularity_ns 的时间,sched_min_granularity_ns 的计算方式如下:

c
If number of runnable tasks does not exceed sched_latency_ns/sched_min_granularity_ns
scheduler period = sched_latency_ns
else
scheduler period = number_of_running_tasks * sched_min_granularity_ns

可以使用 top 命令查看系统上的 nice 值和优先级。如下 PR 表示优先级,NI 表示 nice 值,前者为内核角度看的进程的实际优先级,后者为用户空间看到的进程的 nice 值。两者的关系为:PR = 20 + NI

shell
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
1145 root      20   0  602236  60888  25096 S   0.7  1.2   0:09.70 containerd
  61 root      20   0       0      0      0 I   0.3  0.0   0:02.91 kworker/1:1-eve
   1 root      20   0  193736   8288   5644 S   0.0  0.2   0:01.15 systemd
   2 root      20   0       0      0      0 S   0.0  0.0   0:00.01 kthreadd
   3 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 rcu_gp
   4 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 rcu_par_gp
   5 root      20   0       0      0      0 I   0.0  0.0   0:00.23 kworker/0:0-ata
   6 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 kworker/0:0H-ev
   7 root      20   0       0      0      0 I   0.0  0.0   0:00.17 kworker/u256:0-
   8 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 mm_percpu_wq
   9 root      20   0       0      0      0 S   0.0  0.0   0:00.00 ksoftirqd/0
  10 root      20   0       0      0      0 I   0.0  0.0   0:00.42 rcu_sched
  11 root      rt   0       0      0      0 S   0.0  0.0   0:00.00 migration/0
 ...

Linux 系统可以使用 RLIMIT_NICE 资源来限制非特权进程的 nice 值的上限,参见 setrlimit(2)

更多 nice 值的用法,参见下面的 autogroup。

5. SCHED_BATCH: Scheduling batch processes

从 Linux 2.6.16 开始,SCHED_BATCH 可以用于静态优先级为 0 的线程。该策略类似 SCHED_OTHER,并根据动态优先级(nice 值)进行调度。区别是使用该策略时,调度器会假设线程是 CPU 密集型的,因此,该调度器会根据线程的唤醒行为施加调度惩罚,因此这种调度策略比较不受欢迎。

该策略比较适用于非交互且不期望降低 nice 值的负载,以及需要不因为交互而(在负载之间)造成额外抢占的调度策略的负载。

下面引用自 PHP-FPM on Linux, SCHED_BATCH or SCHED_OTHER?,更多参见 [batch/idle priority scheduling, SCHED_BATCH](batch/idle priority scheduling, SCHED_BATCH)

SCHED_BATCH was clearly designed for very long running (hours or even days) compute-intensive jobs. Your jobs are only compute-intensive for seconds or fractions of seconds.

This pretty much makes it a no-go for a web server. And it would be worse if the database is on the same machine, as they might contend for one of those extra-long timeslices.

6. SCHED_IDLE: Scheduling very low priority jobs

从 Linux 2.6.23 开始,SCHED_IDLE 可以用于静态优先级为 0 的线程。nice 值不会影响该策略。

该策略用于运行非常低优先级的任务(低于 nice 值为+19 的 SCHED_OTHERSCHED_BATCH 策略)。

二、重置子进程的调度策略

每个线程都有一个 reset-on-fork 调度标识。当设置该标识后,使用 fork(2) 创建的子进程不会继承特权调度策略。可以通过如下方式设置 reset-on-fork:

注意上面两个函数的常量名称不一样。使用 sched_getscheduler(2)sched_getattr(2) 获取 reset-on-fork 状态的用法与上面类似。

reset-on-fork 特性用于媒体播放的应用,可以防止应用在创建多个子进程时规避 RLIMIT_RTTIME 设置的资源限制。

更精确地讲,如果设置了 reset-on-fork,后续创建的子进程会遵循下面规则:

  • 如果正在运行的线程使用了 SCHED_FIFOSCHED_RR 调度策略,子进程地策略或被设置为 SCHED_OTHER
  • 如果正在运行的进程的 nice 值为负值,子进程的 nice 值会被设置为 0。

在设置 reset-on-fork 之后,只有线程拥有 CAP_SYS_NICE 的 capability 时才能重置 reset-on-fork。使用 fork(2) 创建的子进程会 disable reset-on-fork 标识。

三、特权与资源限制

在 Linux 2.6.12 之前,只有拥有特权(CAP_SYS_NICE)的线程才能设置非 0 的静态优先级(即设置实时调度策略)。后续版本对如下实现进行了修改:非特权的线程仅在调用者的 effective user ID(EID)与目标线程的 real 或 effective user ID 相同的情况下才能且仅能设置 SCHED_OTHER 策略。

为了设置或修改 SCHED_DEADLINE 策略。线程必须是特权(CAP_SYS_NICE)的。

从 Linux 2.6.12 开始,RLIMIT_RTPRIO(可以使用 ulimit -e 设置)资源限制定义了非特权线程设置 SCHED_RR 和 SCHED_FIFIO 策略的静态优先级的上限。修改调度策略和优先级的规则如下:

  • 如果非特权线程有一个非 0 的 RLIMIT_RTPRIO 软限制(soft limit),则该线程对调度策略和优先级的修改限制为:优先级的不能高于当前优先级且不能高于 RLIMIT_RTPRIO。
  • 如果 RLIMIT_RTPRIO 为 0,则仅允许降低优先级,或切换到非实时策略。
  • 遵从上述规则的前提下,只要执行修改的线程的 effective user ID 等于目标线程的 effective user ID 就可以执行相应的修改。
  • SCHED_IDLE 策略有特殊的约束。在 Linux 2.6.39 之前,在该策略下创建的非特权线程无法修改该策略(与 RLIMIT_RTPRIO 资源限制无关)。从 Linux 2.6.39 开始,只要 nice 值在 RLIMIT_RTPRIO 资源限制所允许的范围内,非特权线程可以切换到 SCHED_BATCH 或 SCHED_OTHER 策略。

特权(CAP_SYS_NICE)线程会忽略 RLIMIT_RTPRIO 限制。在一些老的内核中,特权线程可以任意修改策略和优先级。参见 getrlimit(2) 获取更多信息。

四、限制实时进程或 deadline 进程

SCHED_FIFO, SCHED_RR 或 SCHED_DEADLINE 策略下调度的线程中的非阻塞无限循环处理可能会阻塞其他线程获取 CPU。在 Linux 2.6.25 之前,阻止实时进程冻结系统的唯一方式是通过 shell 启动一个静态优先级更高的程序,如通过这种方式来停止实施程序,并释放 CPU 资源。

从 Linux 2.6.25 开始,引进了其他技术手段来处理实时(SCHED_FIFO,SCHED_RR)和 deadline(SCHED_DEADLINE)进程。一种方式是通过 RLIMIT_RTTIME 来限制实时进程可能使用到的 CPU 的上限。参见 getrlimit(2) 获取更多信息。

从 Linux 2.6.25 开始,Linux 提供了 2 个 /proc 文件来为非实时进程保留 CPU 时间。保留的 CPU 也可以为 shell 预留资源来停止正在允许的进程。两个文件中的值对应的单位为微秒:

  • /proc/sys/kernel/sched_rt_period_us

该文件中的值指定了等同于 100% CPU 的调度周期。取值范围为 1 到 INT_MAX,即 1 微秒到 35 分钟。默认值为 1000,000(1 秒)。定义了一个 CPU 使用周期,周期越短,可以越快开始下一个周期

  • /proc/sys/kernel/sched_rt_runtime_us

该文件中的值指定了实时和 deadline 调度的进程可以使用的 "period"。取值范围为-1 到 INT_MAX-1,设置为-1 标识运行时间等同于周期,即没有给非实时进程预留任何 CPU。默认值为 950,000(0.95 秒),表示给非实时或 deadline 调度策略保留 5%的 CPU。该参数需要结合 sched_rt_period_us 使用

五、Response time(响应时间)

一个阻塞的高优先级的线程(在调度前)等待 I/O 时会有一个确定的响应时间。设备驱动作者可以使用 "slow interrupt" 中断句柄来减少响应时间

六、Miscellaneous(杂项)

子进程会通过 fork(2) 继承调度策略和参数。可以使用 execve(2) 来保存调度策略和参数。

实时进程通常会使用 memory locking 特性来防止内存页的延迟。可以使用 mlock(2)mlockall(2) 设置 memory locking。

七、The autogroup feature(自动分组功能)

从 Linux 2.6.38 开始,内核提供了一种被称为 autogrouping 的特性来为多进程和 CPU 密集型负载(如 Linux 内核中的大量并行进程)提升交互式桌面性能。

该特性结合 CFS 调度策略,需要内核设置 CONFIG_SCHED_AUTOGROUP。在一个运行的系统中,该特性可以通过文件 /proc/sys/kernel/sched_autogroup_enabled 使能或去使能,值 0 表示去使能,1 表示使能。默认值为 1(除非内核使用 noautogroup 参数启动内核)。

当通过 setsid(2) (setsid 会将一个进程脱离父进程)创建一个新的会话时会创建一个新的 autogroup,这种情况可能发生在一个新的终端窗口启动时。使用 fork(2) 创建的进程会继承父辈的 autogroup 成员。因此,一个会话中的所有进程都属于同一个 autogroup。当最后一个进程结束后,autogroup 会被自动销毁。

当使能 autogrouping 时,一个 autogroup 中的所有成员都属于同一个内核调度器 "任务组"。CFS 调度器使用了在任务组间均衡分配 CPU 时钟周期的算法。可以使用下面例子进行展示提升交互式桌面性能的好处。

假设有 2 个竞争相同 CPU 的 autogroup(即,单核系统或使用 taskset 设置所有 SMP 系统的进程使用相同的 CPU),第一个 group 包含 10 个用于构建内核的 CPU 密集型进程 make -j10CPU;另外一个包含一个 CPU 密集型的视频播放器进程。autogrouping 的影响为:每个 group 各自分配到一半的 CPU 时钟周期,即视频播放器会分配到 50%的 CPU 时钟周期,而非 9%的时钟周期(该情况下可能会导致降低视频播放质量)。在 SMP 系统上会更加复杂,但整体的表现是一样的:调度器会在任务组之间分配 CPU 时钟周期,包含大量 CPU 密集型进程的 autogroup 并不会以牺牲系统上的其他任务为代价占用 CPU 周期。

进程的 autogroup 成员可以通过 /proc/[pid]/autogroup 查看:(下面进程隶属于 autogroup-1,autogroup-1 的 nice 值为 0)

shell
$ cat /proc/1/autogroup
/autogroup-1 nice 0

该文件可以通过为 autogroup 设置 nice 值来修改分配给一个 autogroup 的 CPU 带宽(bandwidth,即可使用的 CPU 时间),nice 值范围为+19(低优先级)到-20(高优先级),设置越界的值会导致 write(2) 返回 EINVAL 错误。

autogroup 的 nice 值的意义与进程的 nice 值意义相同,区别是前者为将 autogroup 作为一个整体,并基于相对其他 autogroups 设置的 nice 值来分配 CPU 时钟周期。对于一个 autogroup 内的进程,其 CPU 时钟周期为 autogroup(相对于其他 autogroups)的 nice 值和进程的 nice 值(相对于其他进程)的产物(即首先根据 autogroup 的 nice 值计算该 autogroup 所占用的 CPU,然后根据进程的 nice 值计算该进程所占用的(属于其 autogroup 的)CPU)。

可以使用 cgroups(7) CPU 控制器来设置(非 root CPU cgroup 的)cgroups 中的进程所占用的 CPU,该设置会覆盖掉 autogrouping

Tips:

所有的 cgroup 都由可选择的内核配置 CONFIG_CGROUPS 控制。在 Linux 3.2 引入了 CPU 带宽控制(bandwidth control)。cgroup 对 CFS 的扩展有如下三种:

text
CONFIG_CGROUP_SCHED :运行任务以组的方式(在组之间)公平使用CPU
CONFIG_RT_GROUP_SCHED :用于支持实时任务组(SCHED_FIFO和SCHED_RR)
CONFIG_FAIR_GROUP_SCHED :用于支持CFS任务组(SCHED_NORMAL和SCHED_BATCH)

可以用如下方式查看是否启用了 cgroup

makefile
# zgrep -i cgroup /boot/config-3.10.0-693.el7.x86_64
CONFIG_CGROUPS=y
# CONFIG_CGROUP_DEBUG is not set
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_PIDS=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_HUGETLB=y
CONFIG_CGROUP_PERF=y
CONFIG_CGROUP_SCHED=y
CONFIG_BLK_CGROUP=y
...

autogroup 特性仅用于非实时调度策略(SCHED_OTHER, SCHED_BATCH 和 SCHED_IDLE)。它不会为实时和 deadline 策略分组。

The nice value and group scheduling

当调度非实时进程时,CFS 调度器会使用一种称为 "group scheduling" 的技术(如果内核设置了 CONFIG_FAIR_GROUP_SCHED 选项)

在 group scheduling 下,进程以 "任务组" 方式进行调度。任务组间有继承关系,会继承系统上被称为 "root 任务组" 的初始化任务组。任务组遵循以下条件(按顺序):

  • CPU cgroup 中的所有线程为一个任务组。该任务组的父辈为对应的父 cgroup
  • 如果使能了 autogrouping,则一个 autogroup(即,使用 setsid(2) 创建的相同的会话)中的所有线程为一个任务组。每个新的 autogrouping 为独立的任务组。root 任务组为所有任务组的父辈。
  • 如果使能了 autogrouping,那么包含所有 root CPU cgroup 进程的 root 任务组不会(隐式地)放到一个新的 autogroup 中。
  • 如果使能了 autogrouping,那么 root 任务组包含所有 root CPU croup 中的进程。
  • 如果去使能 autogrouping(即内核不配置 CONFIG_FAIR_GROUP_SCHED),那么系统中所有的进程都会被放到一个任务组中

在 group 调度下,线程的 nice 值仅会影响到相同任务组的其他线程的调度。这会在一些使用传统 nice 语义的 UNIX 系统上会导致惊人的后果。实践中,如果使能了 autogrouping,则会使用 setpriority(2)nice(1) 来影响相同会话(通常为相同的终端窗口)中的一个进程相对于其他进程的调度。

相反的,对于不同会话(如,不同的终端窗口,这些任务都绑定到不同的 autogroups)中绑定了唯一的 CPU 的 2 个进程,修改一个会话中的进程的 nice 值不会影响其他会话中的进程的调度。使用如下命令可以修改一个终端会话中所有进程对于的 autogroup nice 值。

shell
$ echo 10 > /proc/self/autogroup

autogroup 和进程都有一个 nice 值,autogroup 的 nice 值用于在 autogroup 之间分配 CPU;autogroup 内的进程的 nice 值用于在进程间分配 autogroup 的 CPU。cgroup 的配置会覆盖 autogroup

八、主线版 Linux 内核中的实时功能

从 Linux 2.6.18 开始,Linux 逐渐具备实时功能,其中大部分来源于 realtime-preempt 补丁集。在这些补丁最终合并到内核主线之前,它们必须通过安装才能达到实时性能。这些补丁命名为:

text
patch-kernelversion-rtpatchversion

可以从 这里 下载或者从 kernel/git/rt/linux-stable-rt.git 克隆。

自 Linux 6.13 版本起,实时功能变为可选选项而非抢占模式。有了这一变化,以下抢占模式可供选择:CONFIG_PREEMPT_NONE、CONFIG_PREEMPT_VOLUNTARY、CONFIG_PREEMPT 和 CONFIG_PREEMPT_LAZY。可以通过选项 CONFIG_PREEMPT_RT 启用实时功能,并可将抢占模式设置为 CONFIG_PREEMPT 或 CONFIG_PREEMPT_LAZY。后一种模式在试图减少持有锁任务的抢占行为时会较为迟缓,但不会影响实时任务。

当启用 CONFIG_PREEMPT_RT 选项时,Linux 将转变为一个常规的实时操作系统。随后会使用 SCHED_FIFO、SCHED_RR 和 SCHED_DEADLINE 这些调度策略来运行具有真正实时优先级和最小最坏情况调度延迟的线程。

九、其他内容

1. ps 命令

使用 ps -eLfc 可以在 CLS 一栏中查看进程的调度策略:

text
TS  SCHED_OTHER
FF  SCHED_FIFO
RR  SCHED_RR
B   SCHED_BATCH
ISO SCHED_ISO
IDL SCHED_IDLE

这些宏都定义在 sched.h - include/uapi/linux/sched.h - Linux source code v6.1

c
/*
 * Scheduling policies
 */
#define SCHED_NORMAL    0
#define SCHED_FIFO		1
#define SCHED_RR		2
#define SCHED_BATCH		3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE		5
#define SCHED_DEADLINE  6

2. /proc/sched_debug

可以在 /proc/sched_debug 中查看各个 cpu core 的调度情况,其中包含每个 cgroup 服务的调度情况,如下面为 docker 服务在 core1 上的调度情况:

shell
cfs_rq[1]:/system.slice/docker.service
  .exec_clock                    : 0.000000
  .MIN_vruntime                  : 0.000001
  .min_vruntime                  : 2723538.752185
  .max_vruntime                  : 0.000001
  .spread                        : 0.000000
  .spread0                       : -780278343.308552
  .nr_spread_over                : 0
  .nr_running                    : 0
  .load                          : 0
  .runnable_load_avg             : 0
  .blocked_load_avg              : 1
  .tg_load_avg                   : 1
  .tg_load_contrib               : 1
  .tg_runnable_contrib           : 3
  .tg->runnable_avg              : 8
  .tg->cfs_bandwidth.timer_active: 0
  .throttled                     : 0
  .throttle_count                : 0
  .se->exec_start                : 7432565479.124290
  .se->vruntime                  : 560032308.234830
  .se->sum_exec_runtime          : 7762399.141979
  .se->load.weight               : 2
  .se->avg.runnable_avg_sum      : 147
  .se->avg.runnable_avg_period   : 47729
  .se->avg.load_avg_contrib      : 1
  .se->avg.decay_count           : 7088246803

3. /proc/$pid/sched

可以在 /proc/$pid/sched 中查看特定进程的调度情况:

shell
# cat sched
docker-proxy-cu (77992, #threads: 8)
-------------------------------------------------------------------
se.exec_start                                :    7179182946.125343
se.vruntime                                  :       1843988.364695
se.sum_exec_runtime                          :             6.017643
se.nr_migrations                             :                    2
nr_switches                                  :                    6
nr_voluntary_switches                        :                    4
nr_involuntary_switches                      :                    2
se.load.weight                               :                 1024
policy                                       :                    0
prio                                         :                  120
clock-delta                                  :                   34
mm->numa_scan_seq                            :                    0
numa_migrations, 0
numa_faults_memory, 0, 0, 1, 0, -1
numa_faults_memory, 1, 0, 0, 0, -1

4. /proc/$pid/status

一个线程可以通过系统分配的时间片在各个 CPU core 上允许,但一个线程不能同时在多个 core 上运行。如果一个系统有 N 个 core,那么可以同时在这些 core 上允许 N 个线程。将 CPU core 上线程切换到另外一个线程时,会涉及到上下文切换,上下文切换时需要保存 PCB 中的 CPU 寄存器信息,进程状态以及内存管理等信息,在进程恢复时还原这些信息。

img

可以通过 /proc/$pid/status 查看进程上下文切换的情况,如下表示自发(如 I/O 等待)的上下文切换为 1,非自发(如时间片超时会被更高优先级进程抢占)的上下文切换为 10。

text
voluntary_ctxt_switches:        1
nonvoluntary_ctxt_switches:     10

5. 切分 CPU

可以将 1 个 CPU 分为 1000 份,即 1 CPU = 1000m。但在 CFS 中,会将 1 CPU 分为 1024 份,称为 1024 CPU shares,即分配 500m 的 CPU 等同于分配了 512 CPU shares。

CFS 的调度周期由 cpu.cfs_period_us 定义,默认 100m。意味着,如果 cpu.cfs_period_us 为 100m,则 1CPU 就是 100ms,而 2500m CPU 就是 250ms,CFS 会将一个周期切分为多个时间片,最小时间片由 sched_cfs_bandwidth_slice_us 定义,默认 5ms。

十、小结

1. 三个调度策略

其实,我们平时接触到的大概就是三类调度策略:

  • SCHED_OTHER:分时调度策略

  • SCHED_FIFO:实时调度策略,先到先服务。一旦占用 cpu 则一直运行。一直运行直到有更高优先级任务到达或自己放弃。

  • SCHED_RR:实时调度策略,时间片轮转。当进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的 RR 任务的调度公平。

实时进程将得到优先调用,实时进程根据实时优先级决定调度权值。分时进程则通过 nice 和 counter 值决定权值,nice 越小,counter 越大,被调度的概率越大,也就是曾经使用了 cpu 最少的进程将会得到优先调度。

如果有相同优先级的实时进程(根据优先级计算的调度权值是一样的)已经准备好,FIFO 时必须等待该进程主动放弃后才可以运行这个优先级相同的任务。而 RR 可以让每个任务都执行一段时间。

其中,RR 和 FIFO 都只用于实时任务,他们创建时优先级大于 0(1-99),按照可抢占优先级调度算法进行。就绪态的实时任务立即抢占非实时任务。

2. 所有任务都使用同一种策略?

2.1 所有任务都采用 SCHED_OTHER

(1)创建任务指定采用分时调度策略,并指定优先级 nice 值(-20~19)

(2)将根据每个任务的 nice 值确定在 cpu 上的执行时间(counter)。

(3)如果没有等待资源,则将该任务加入到就绪队列中。

(4)调度程序遍历就绪队列中的任务,通过对每个任务动态优先级的计算权值(counter+20-nice)结果,选择计算结果最大的一个去运行,当这个时间片用完后(counter 减至 0)或者主动放弃 cpu 时,该任务将被放在就绪队列末尾(时间片用完)或等待队列(因等待资源而放弃 cpu)中。

(5)此时调度程序重复上面计算过程,转到第(4)步。

(6)当调度程序发现所有就绪任务计算所得的权值都为不大于 0 时,重复第(2)步。

2.2 所有任务都采用 SCHED_FIFO

(1)创建进程时指定采用 FIFO,并设置实时优先级 rt_priority(1-99)。

(2)如果没有等待资源,则将该任务加入到就绪队列中。

(3)调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority), 选择权值最高的任务使用 cpu,该 FIFO 任务将一直占有 cpu 直到有优先级更高的任务就绪(即使优先级相同也不行)或者主动放弃(等待资源)。

(4)调度程序发现有优先级更高的任务到达(高优先级任务可能被中断或定时器任务唤醒,再或被当前运行的任务唤醒,等等),则调度程序立即在当前任务堆栈中保存当前 cpu 寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到 cpu,此时高优先级的任务开始运行。重复第 3 步。

(5)如果当前任务因等待资源而主动放弃 cpu 使用权,则该任务将从就绪队列中删除,加入等待队列,此时重复第 3 步。

2.3 所有任务都采用 SCHED_RR

(1)创建任务时指定调度参数为 RR,并设置任务的实时优先级和 nice 值(nice 值将会转换为该任务的时间片的长度)。

(2)如果没有等待资源,则将该任务加入到就绪队列中。

(3)调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority), 选择权值最高的任务使用 cpu。

(4)如果就绪队列中的 RR 任务时间片为 0,则会根据 nice 值设置该任务的时间片,同时将该任务放入就绪队列的末尾。重复步骤 3。

(5)当前任务由于等待资源而主动退出 cpu,则其加入等待队列中。重复步骤 3。

2.4 全都有的时候

(1)RR 调度和 FIFO 调度的进程属于实时进程,以分时调度的进程是非实时进程。

(2)当实时进程准备就绪后,如果当前 cpu 正在运行非实时进程,则实时进程立即抢占非实时进程。

(3)RR 进程和 FIFO 进程都采用实时优先级做为调度的权值标准,RR 是 FIFO 的一个延伸。FIFO 时,如果两个进程的优先级一样,则这两个优先级一样的进程具体执行哪一个是由其在队列中的未知决定的,这样导致一些不公正性(优先级是一样的,为什么要让你一直运行?),如果将两个优先级一样的任务的调度策略都设为 RR, 则保证了这两个任务可以循环执行,保证了公平。

参考资料:

linux 线程调度策略-阿里云开发者社区

linux 线程调度策略 - charlieroro - 博客园

(13 封私信) 【纯干货】linux调度策略(最透彻的一篇!) - 知乎