13 微调任务调度程序 #
新式操作系统(例如 SUSE® Linux Enterprise Server)往往会同时运行许多任务。例如,您可以在搜索文本文件的同时接收电子邮件以及将大型文件复制到外部硬盘。这些简单任务要求系统运行许多额外的进程。为了给每个任务提供所需的系统资源,Linux 内核需要使用一种工具向各个任务分发可用系统资源。这恰恰是任务调度程序的作用所在。
下列章节解释了与进程调度相关的最重要术语。其中还介绍了有关任务调度程序策略和调度算法的信息,描述了 SUSE Linux Enterprise Server 使用的任务调度程序,并提供了其他相关信息来源的参考。
13.1 简介 #
Linux 内核用于控制在系统上管理任务(或进程)的方式。任务调度程序(有时称作进程调度程序)是决定下一次要运行哪个任务的内核组件。它负责以最佳方式使用系统资源,以保证能够同时执行多个任务。因此,它是任何多任务操作系统的核心组件。
13.1.1 抢占 #
任务调度背后的理论非常简单。如果系统中存在多个可运行的进程,必须始终至少有一个进程正在运行。如果可运行进程的数目超过了系统中处理器的数目,则并非所有进程都能始终运行。
因此,某些进程需要暂时停止(或挂起),使其他进程能够再次运行。由调度程序决定下一次运行队列中的哪个进程。
如前所述,与其他所有 Unix 变体类似,Linux 也是多任务操作系统。即,多个任务可以同时运行。Linux 提供所谓的抢占式多任务,让调度程序决定何时挂起某个进程。这种强制挂起称为抢占。从一开始就为所有 Unix 风格提供了抢占式多任务。
13.1.2 时间片 #
提前定义某个进程在被抢占之前的运行时长。此时长称为进程的时间片,表示提供给每个进程的处理器时间长短。通过指派时间片,调度程序可以针对运行中的系统做出全局决策,并防止单个进程占据处理器资源。
13.1.3 进程优先级 #
调度程序基于进程的优先级评估进程。任务调度程序使用复杂算法计算进程的当前优先级。最后会为每个进程指定一个值,根据该值“允许”进程在处理器上运行。
13.2 进程分类 #
通常根据进程的用途和行为对进程分类。尽管有时界线区分不够清晰,但一般会采用两条准则进行分类。这两条准则彼此独立,但并非互不包容。
一种方法是将进程分类为 I/O 密集型或处理器密集型。
- I/O 密集型
I/O 代表输入/输出设备,例如键盘、鼠标或者光盘和硬盘。I/O 密集型进程将大部分时间花费在提交和等待请求上。它们的运行频率极高,但时间间隔较短,不会阻塞等待 I/O 请求的其他进程。
- 处理器密集型
相比之下,处理器密集型任务将时间用在执行代码上,通常会运行到被调度程序抢占为止。它们不会阻塞等待 I/O 请求的进程,因此其运行频率可以更低,但时间间隔更长。
另一种方法是按类型将进程划分为交互式、批处理和实时进程。
交互式进程将大量时间花费在等待 I/O 请求(例如键盘或鼠标操作)上。调度程序必须按用户请求快速唤醒此类进程,否则用户会发现环境无响应。典型延迟大约为 100 毫秒。办公应用程序、文本编辑器或图像处理程序都是典型的交互式进程。
批处理进程通常在后台运行,不需要做出响应。调度程序往往会为它们分配较低的优先级。多媒体转换器、数据库搜索引擎或日志文件分析器都是批处理进程的典型示例。
实时进程永远不可被低优先级进程阻塞,调度程序需保证这些进程在很短时间获得响应。用于编辑多媒体内容的应用程序就是实时进程的典型示例。
13.3 完全公平调度程序 #
从 Linux 内核版本 2.6.23 开始,采用了一种新方法来调度可运行的进程。完全公平调度程序 (CFS) 成为了默认 Linux 内核调度程序。自此以后发生了重大变化和改进。本章中的信息适用于采用 2.6.32 和更高内核版本(包括 3.x 内核)的 SUSE Linux Enterprise Server。调度程序环境分为多个部分,引入了三项主要新功能:
- 模块化调度程序核心
调度程序的核心已通过调度类得以增强。这些类是模块化的,代表调度策略。
- 完全公平调度程序
在内核 2.6.23 中引入并在 2.6.24 中进行了扩展的 CFS 尝试确保每个进程获得其“公平”份额的处理器时间。
- 组安排
例如,如果您根据运行进程的用户将进程分组,则 CFS 会尝试为其中每个组提供等量的处理器时间。
因此,CFS 能够针对服务器和桌面优化调度。
13.3.1 CFS 的工作原理 #
CFS 尝试为每个可运行的任务保证公平性。为了找出最平衡的任务调度方式,它使用了红黑树的概念。红黑树是一种自我平衡式数据搜索树,能够以合理方式提供插入项和去除项,使其自身保持适当的平衡。
当 CFS 调度任务时,它会累加“虚拟运行时” (vruntime)。选择运行的下一个任务始终是迄今为止累加 vruntime 最小的那个任务。在将任务插入运行队列(下次要执行的进程的规划时间线)时通过平衡红黑树,可保证 vruntime 最小的任务始终是红黑树中的第一个项。
任务的累计 vruntime 与该任务的优先级相关。高优先级任务的 vruntime 的累加速度比低优先级任务要慢,导致更频繁地选择在处理器上运行高优先级任务。
13.3.2 将进程分组 #
从 Linux 内核版本 2.6.24 开始,可对 CFS 进行微调,以保证为组而不仅仅是任务提供公平性。为此,将可运行的任务分组以构成实体,而 CFS 会尝试为这些实体而不是各个可运行的任务提供公平性。调度程序还会尝试为这些实体中的各个任务提供公平性。
内核调度程序可让您使用控制组将可运行的任务分组。有关更多信息,请参见第 9 章 “内核控制组”。
13.3.3 内核配置选项 #
您可以通过内核配置选项来设置任务调度程序行为的基本方面。设置这些选项属于内核编译过程的一部分。由于内核编译过程是一个复杂任务,超出了本文档的讨论范畴,因此请参考相关的信息来源。
如果您在并非 SUSE Linux Enterprise Server 随附的内核上运行该系统(例如,在自我编译的内核上运行),您会失去全部支持权利。
13.3.4 术语 #
有关任务调度策略的文档经常会使用一些技术术语,您需要熟悉这些术语才能正确理解文档中的信息。下面是其中的一些术语:
- 延迟
调度要运行的进程与实际执行该进程之间的延迟。
- 粒度
可通过以下公式表达粒度与延迟之间的关系:
gran = ( lat / rtasks ) - ( lat / rtasks / rtasks )
其中 gran 表示粒度,lat 表示延迟,rtasks 是正在运行的任务数。
13.3.4.1 调度策略 #
Linux 内核支持以下调度策略:
- SCHED_FIFO
适用于特殊时间关键型应用程序的调度策略。它使用“先进先出”调度算法。
- SCHED_BATCH
适用于 CPU 密集型任务的调度策略。
- SCHED_IDLE
适用于极低优先级任务的调度策略。
- SCHED_OTHER
大部分进程使用的默认 Linux 分时调度策略。
- SCHED_RR
与
SCHED_FIFO
类似,但使用循环复用调度算法。
13.3.5 使用 chrt
更改进程的实时属性 #
chrt
命令设置或检索运行中进程的实时调度属性,或者使用指定的属性运行某个命令。您可以获取或检索进程的调度策略和优先级。
在以下示例中,使用了 PID 为 16244 的进程。
要检索现有任务的实时属性,请执行以下命令:
root #
chrt -p 16244
pid 16244's current scheduling policy: SCHED_OTHER
pid 16244's current scheduling priority: 0
在对进程设置新的调度策略之前,需要找出每个调度算法的最小和最大有效优先级:
root #
chrt -m
SCHED_SCHED_OTHER min/max priority : 0/0
SCHED_SCHED_FIFO min/max priority : 1/99
SCHED_SCHED_RR min/max priority : 1/99
SCHED_SCHED_BATCH min/max priority : 0/0
SCHED_SCHED_IDLE min/max priority : 0/0
在以上示例中,SCHED_OTHER、SCHED_BATCH、SCHED_IDLE 策略仅允许优先级 0,而 SCHED_FIFO 和 SCHED_RR 的优先级可为 1 到 99。
要设置 SCHED_BATCH 调度策略,请执行以下命令:
root #
chrt -b -p 0 16244
pid 16244's current scheduling policy: SCHED_BATCH
pid 16244's current scheduling priority: 0
有关 chrt
的详细信息,请参见其手册页 (man 1 chrt
)。
13.3.6 使用 sysctl
进行运行时微调 #
用于在运行时检查和更改内核参数的 sysctl
接口引入了重要的变量,您可以通过这些变量更改任务调度程序的默认行为。sysctl
的语法非常简单。必须以 root
身份在命令行上输入所有以下命令。
要从内核变量读取值,请输入
root #
sysctl
VARIABLE
要赋值,请输入
root #
sysctl
VARIABLE=VALUE
要获取所有与调度程序相关的 sysctl
变量的列表,请输入
root #
sysctl
-A
|grep
"sched" |grep
-v
"domain"
root #
sysctl -A | grep "sched" | grep -v "domain"
kernel.sched_cfs_bandwidth_slice_us = 5000
kernel.sched_child_runs_first = 0
kernel.sched_compat_yield = 0
kernel.sched_latency_ns = 24000000
kernel.sched_migration_cost_ns = 500000
kernel.sched_min_granularity_ns = 8000000
kernel.sched_nr_migrate = 32
kernel.sched_rr_timeslice_ms = 25
kernel.sched_rt_period_us = 1000000
kernel.sched_rt_runtime_us = 950000
kernel.sched_schedstats = 0
kernel.sched_shares_window_ns = 10000000
kernel.sched_time_avg_ms = 1000
kernel.sched_tunable_scaling = 1
kernel.sched_wakeup_granularity_ns = 10000000
请注意,以 “_ns” 和 “_us” 结尾的变量分别接受以纳秒和微秒为单位的值。
下面提供了最重要任务调度程序 sysctl
微调变量的列表(位于 /proc/sys/kernel/
中)和简短说明:
sched_cfs_bandwidth_slice_us
使用 CFS 带宽控制时,此参数会控制从任务的控制组带宽池传送到运行队列的运行时间长短(带宽)。指定较小值可以在任务之间精细共享全局带宽,指定较大值可减少传送开销。请参见https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt。
sched_child_runs_first
新派生的子项在父项继续执行之前运行。将此参数设置为
1
有利于其中的子项在派生后完成执行的应用程序。例如,如果关闭 sched_child_runs_first,make
-j<NO_CPUS>
的表现更好。默认值为0
。sched_compat_yield
通过将放弃的任务移到可运行队列的末尾(红黑树中的最右边),启用旧 O(1) 调度程序的主动 CPU 放弃行为。出现严重的资源争用(例如锁定)时,通过给予其他进程运行机会,依赖
sched_yield(2)
syscall 行为的应用程序可能会获得性能提高。另一方面,假设这种调用是在环境切换时发生的,滥用调用可能会给工作负载造成损害。请仅当您发现性能下降时,才使用此变量。默认值为0
。sched_migration_cost_ns
自上次执行后经过多长时间,会在迁移决策中将某个任务视为“缓存热点”。“热点”任务不太可能被迁移到另一 CPU,因此增大此变量可以减少任务迁移。默认值为
500000
(纳秒)。如果存在可运行进程时 CPU 空闲时间超过预期,请尝试减小此值。如果任务过于频繁地在 CPU 或节点之间弹跳,请尝试增大此值。
sched_latency_ns
CPU 密集型任务的目标抢占延迟。增大此变量会增加 CPU 密集型任务的时间片。任务的时间片是其调度时段的加权公平份额:
时间片 = 调度时段 * (任务的权重/任务在运行队列中的总权重)
任务的权重取决于任务的 nice 级别和调度策略。SCHED_OTHER 任务的最小任务权重为 15,对应于 nice 19。最大任务权重为 88761,对应于 nice -20。
时间片随着负载增加而变小。当可运行任务的数目超过
sched_latency_ns
/sched_min_granularity_ns
时,切片将变为 number_of_running_tasks *sched_min_granularity_ns
。在此之前,切片等于sched_latency_ns
。此值还指定在权利计算中将某个休眠任务视为正在运行的最长时间量。增大此变量会增加抢占某个唤醒任务之前该任务可能消耗的时间,因此会增加 CPU 密集型任务的调度程序延迟。默认值为
6000000
(纳秒)。sched_min_granularity_ns
CPU 密集型任务的最小抢占粒度。有关细节,请参见
sched_latency_ns
。默认值为4000000
(纳秒)。sched_wakeup_granularity_ns
唤醒抢占粒度。增大此变量会减小唤醒抢占,从而减轻计算密集型任务的干扰。减小此变量可改善延迟关键型任务的唤醒延迟和吞吐量,尤其是当短工作周期负载组件必须与 CPU 密集型组件竞争时。默认值为
2500000
(纳秒)。警告:设置适当的唤醒粒度值设置值超过
sched_latency_ns
的一半将导致不会发生唤醒抢占。短工作周期任务将无法有效与 CPU 资源霸占者竞争。sched_rr_timeslice_ms
在 SCHED_RR 任务被抢占并置于任务列表末尾之前可运行的量子。
sched_rt_period_us
测量实时任务带宽强制的时间段。默认值为
1000000
(微秒)。sched_rt_runtime_us
在 sched_rt_period_us 时间段分配给实时任务的量子。设置为 -1 会禁用 RT 带宽强制。默认情况下,RT 任务每秒可能消耗 95%CPU,因而剩下 5%CPU/秒(或 0.05 秒)供 SCHED_OTHER 任务使用。默认值为
950000
(微秒)。sched_nr_migrate
控制为实现负载平衡可跨处理器迁移的任务数。由于平衡机制在禁用中断 (softirq) 的情况下迭代运行队列,它可能造成实时任务的 IRQ 延迟。因此,增大此值可能会提升大型 SCHED_OTHER 线程的性能,代价是会增加实时任务的 IRQ 延迟。默认值为
32
。sched_time_avg_ms
此参数用于设置计算实时任务运行时间的平均值所依据的时间段。该平均值可帮助 CFS 做出负载平衡决策,并指示 CPU 处理高优先级实时任务的繁忙程度。
此参数的最佳设置在很大程度上与工作负载相关,并取决于实时任务的运行频率和运行时长等因素。
13.3.7 调试界面和调度程序统计 #
CFS 随附了新的增强型调试界面,并提供运行时统计信息。相关文件已添加到 /proc
文件系统,使用 cat
或 less
命令即可检查这些文件。下面提供了相关 /proc
文件的列表及其简短说明:
/proc/sched_debug
包含影响任务调度程序行为的所有可调变量(请参见第 13.3.6 节 “使用
sysctl
进行运行时微调”)的当前值、CFS 统计,以及有关所有可用处理器上的运行队列的信息(CFS、RT 和最后期限)。此外还会显示每个处理器上运行的任务摘要、任务名称和 PID,以及调度程序特定的统计。最先显示的是tree-key
列,其中指示了任务的虚拟运行时及其在内核中的名称,在红黑树中可按此键对所有可运行任务进行排序。switches
列表示切换总次数(无论强制还是自愿),prio
显然指的是进程优先级。wait-time
值表示任务等待调度的时间。最后,sum-exec
和sum-sleep
分别统计了任务在处理器上运行或者处于休眠状态的总时间(以纳秒为单位)。root #
cat /proc/sched_debug Sched Debug Version: v0.11, 4.4.21-64-default #1 ktime : 23533900.395978 sched_clk : 23543587.726648 cpu_clk : 23533900.396165 jiffies : 4300775771 sched_clock_stable : 0 sysctl_sched .sysctl_sched_latency : 6.000000 .sysctl_sched_min_granularity : 2.000000 .sysctl_sched_wakeup_granularity : 2.500000 .sysctl_sched_child_runs_first : 0 .sysctl_sched_features : 154871 .sysctl_sched_tunable_scaling : 1 (logaritmic) cpu#0, 2666.762 MHz .nr_running : 1 .load : 1024 .nr_switches : 1918946 [...] cfs_rq[0]:/ .exec_clock : 170176.383770 .MIN_vruntime : 0.000001 .min_vruntime : 347375.854324 .max_vruntime : 0.000001 [...] rt_rq[0]:/ .rt_nr_running : 0 .rt_throttled : 0 .rt_time : 0.000000 .rt_runtime : 950.000000 dl_rq[0]: .dl_nr_running : 0 task PID tree-key switches prio wait-time [...] ------------------------------------------------------------------------ R cc1 63477 98876.717832 197 120 0.000000 .../proc/schedstat
显示与当前运行队列相关的统计。另外还显示所有已连接处理器的 SMP 系统域特定统计。由于输出格式不够友好,请阅读
/usr/src/linux/Documentation/scheduler/sched-stats.txt
的内容来了解详细信息。/proc/PID/sched
显示有关进程的调度信息及其 ID (PID)。
root #
cat /proc/$(pidof gdm)/sched gdm (744, #threads: 3) ------------------------------------------------------------------- se.exec_start : 8888.758381 se.vruntime : 6062.853815 se.sum_exec_runtime : 7.836043 se.statistics.wait_start : 0.000000 se.statistics.sleep_start : 8888.758381 se.statistics.block_start : 0.000000 se.statistics.sleep_max : 1965.987638 [...] se.avg.decay_count : 8477 policy : 0 prio : 120 clock-delta : 128 mm->numa_scan_seq : 0 numa_migrations, 0 numa_faults_memory, 0, 0, 1, 0, -1 numa_faults_memory, 1, 0, 0, 0, -1
13.4 更多信息 #
要获得有关 Linux 内核任务调度的简要介绍,需浏览多个信息来源。下面是其中一些资源:
有关任务调度程序系统调用的介绍,请参见相关手册页(例如
man 2 sched_setaffinity
)。http://www.inf.fu-berlin.de/lehre/SS01/OS/Lectures/Lecture08.pdf 中提供了有关 Linux 调度程序策略和算法的有用讲座。
由 Robert Love 发布的 Linux Kernel Development(Linux 内核开发)(ISBN-10:0-672-32512-8)很好地概述了 Linux 进程调度。请参见 https://www.informit.com/articles/article.aspx?p=101760。
由 Daniel P. Bovet 和 Marco Cesati 发布的《Understanding the Linux Kernel》(了解 Linux 内核)(ISBN 978-0-596-00565-8) 中非常全面地概述了 Linux 内核的内部结构。
/usr/src/linux/Documentation/scheduler
下的文件中包含有关任务调度程序的技术信息。