2. Linux 进程调度器概述:CFS 调度器原理、调度域与调度组

在多核系统中,进程调度器是内核最核心的组件之一。它决定了哪个进程在哪个 CPU 上运行、运行多久、何时被抢占。理解调度器的工作原理,是掌握 CPU 绑定与中断亲和性优化的前提。本章深入剖析完全公平调度器(CFS)的核心机制,并详细解释调度域与调度组这两个多核调度中的关键抽象概念。

2.1 完全公平调度器(CFS)原理

CFS(Completely Fair Scheduler)自 Linux 2.6.23 引入以来,一直是默认的调度器。其设计哲学是:为每个进程提供对 CPU 时间的“公平”分配,而非传统的时间片轮转。CFS 使用一个基于红黑树(Red-Black Tree)的模型来管理可运行进程,并引入“虚拟运行时间”(vruntime)作为核心度量。

2.1.1 核心数据结构:红黑树与 vruntime

CFS 为每个 CPU 的运行队列(cfs_rq)维护一棵红黑树。树中每个节点代表一个可运行的调度实体(sched_entity,通常是进程或线程)。节点的键值是进程的 vruntime。

  • vruntime:进程实际运行时间的加权值。公式为:vruntime += 实际运行时间 * (NICE_0_LOAD / 进程权重)。权重由进程的 nice 值决定,nice 值越低(优先级越高),权重越大,vruntime 增长越慢。
  • 红黑树特性:最左侧节点(vruntime 最小)是当前最需要 CPU 的进程,即下一个被选中的进程。
  • 调度点:当进程被创建、唤醒、或时钟 tick 发生时,CFS 检查当前进程的 vruntime 是否已经超过红黑树最左侧节点的 vruntime,如果是,则触发抢占。
关键洞察: CFS 的“公平”并非绝对平均,而是按权重分配。一个 nice=0 的进程比 nice=10 的进程获得更多的 CPU 时间。vruntime 的引入使得调度决策可以在 O(log N) 的时间复杂度内完成,其中 N 是运行队列中的进程数。
CFS 调度器核心结构:红黑树与 vruntime vruntime=100 vruntime=80 vruntime=120 vruntime=60 vruntime=90 vruntime=140 调度决策流程: 1. 每次调度时,CFS 选择红黑树最左侧节点(vruntime 最小)的进程运行。 2. 进程运行时,其 vruntime 按权重比例增长;被抢占或睡眠时,vruntime 保持不变。

2.1.2 调度周期与最小粒度

CFS 引入了两个关键参数来控制调度行为:

参数 默认值 说明
sched_latency_ns 6,000,000 (6ms) 目标调度延迟。CFS 试图在一个调度周期内让所有可运行进程至少运行一次。
sched_min_granularity_ns 750,000 (0.75ms) 进程被抢占前的最小运行时间。防止过于频繁的上下文切换。

调度周期(__sched_period)的计算方式为:max(sched_latency_ns, nr_running * sched_min_granularity_ns)。当运行进程数较少时,调度周期固定为 6ms;当进程数增多时,周期自动延长,以保证每个进程至少运行 0.75ms。

2.1.3 组调度(Group Scheduling)

CFS 支持将进程组织成调度组(task group),实现更细粒度的资源控制。例如,使用 cgroups 的 cpu 子系统时,每个控制组就是一个调度组。组内的所有进程共享分配给该组的 CPU 时间,组内再按 CFS 规则进行二次分配。

性能影响: 组调度增加了调度层级,但提供了更强的隔离性。在虚拟化或容器环境中,组调度是保证服务质量(QoS)的关键机制。

2.2 调度域(Scheduling Domains)

调度域是内核为多核、多 NUMA 节点系统设计的拓扑抽象。它将 CPU 按照物理拓扑(如超线程、核心、L3 缓存、NUMA 节点)组织成层级结构,帮助调度器做出更优的负载均衡决策。

2.2.1 调度域的层级结构

一个典型的调度域层级如下(以双路 NUMA 系统为例):

  • Domain 0 (SMT 域):包含共享同一个物理核心的超线程 CPU。粒度最小,共享 L1/L2 缓存。
  • Domain 1 (MC 域):包含同一个物理封装(Package)内的所有核心。共享 L3 缓存。
  • Domain 2 (NUMA 域):包含同一个 NUMA 节点内的所有 CPU。内存访问延迟相同。
  • Domain 3 (NUMA 跨节点域):包含系统中所有 CPU。跨节点内存访问延迟较高。
调度域层级结构示例(双路 NUMA 系统) Domain 3: NUMA 跨节点域 (All CPUs) Domain 2: NUMA 节点 0 Domain 1: MC 域 (Package 0) Core 0 Domain 0: SMT Core 1 Domain 0: SMT Domain 2: NUMA 节点 1 Domain 1: MC 域 (Package 1) Core 2 Domain 0: SMT Core 3 Domain 0: SMT 负载均衡从最底层 Domain 0 开始,逐层向上进行。跨域迁移代价随层级升高而增大。

2.2.2 调度域与负载均衡

每个调度域都有一个 负载均衡器(load_balance),定期检查域内各 CPU 运行队列的负载是否均衡。如果发现某个 CPU 过载而另一个空闲,均衡器会尝试将进程从过载 CPU 迁移到空闲 CPU。迁移策略会考虑调度域的层级:优先在同一 Domain 0 内迁移(代价最小),必要时才跨 NUMA 节点迁移。

优化提示: 对于延迟敏感型应用,应尽量将进程绑定在同一个 NUMA 节点内,避免跨节点内存访问。可以通过 /sys/devices/system/cpu/cpu*/topology/ 下的文件查看 CPU 拓扑信息。

2.3 调度组(Scheduling Groups)

调度组是调度域内的基本组成单元。每个调度域由多个调度组构成,组内包含一个或多个 CPU。调度组的划分与 CPU 拓扑紧密相关。

2.3.1 调度组的划分规则

调度域层级 调度组划分 示例
Domain 0 (SMT) 每个物理核心为一个组 Core 0 的 2 个超线程 CPU 组成一个组
Domain 1 (MC) 每个物理封装为一个组 Package 0 的所有核心组成一个组
Domain 2 (NUMA) 每个 NUMA 节点为一个组 NUMA 节点 0 的所有 CPU 组成一个组

2.3.2 调度组与负载均衡的关系

负载均衡器在调度域内工作时,比较的是各个调度组的总负载。如果某个组的负载远高于其他组,均衡器会从该组中挑选一个 CPU,将进程迁移到负载较低的组中的某个 CPU 上。这种“组间均衡”策略避免了频繁的跨核心迁移,提高了缓存亲和性。

调度组与负载均衡流程 Domain 1: MC 域 (Package 0) 调度组 0 (Core 0) CPU 0: 负载 3 CPU 1: 负载 2 总负载: 5 调度组 1 (Core 1) CPU 2: 负载 1 CPU 3: 负载 0 总负载: 1 调度组 2 (Core 2) CPU 4: 负载 2 CPU 5: 负载 1 总负载: 3 迁移 负载均衡器比较组间总负载,将进程从高负载组(组0)迁移到低负载组(组1),实现域内均衡。

2.4 调度域与调度组的实际查看方法

在运行中的 Linux 系统上,可以通过以下方式查看调度域和调度组的拓扑信息:

# 查看 CPU 拓扑
$ ls /sys/devices/system/cpu/cpu0/topology/
core_id  core_siblings  core_siblings_list  die_id  package_cpus  package_cpus_list  physical_package_id  thread_siblings  thread_siblings_list

# 查看调度域信息(需要内核编译选项 CONFIG_SCHED_DEBUG)
$ cat /proc/sys/kernel/sched_domain/cpu0/domain*/name
SMT
MC
NUMA
NUMA

# 查看调度组详细信息
$ cat /proc/sys/kernel/sched_domain/cpu0/domain0/groups
实践建议: 在进行 CPU 绑定优化前,务必先通过上述命令了解系统的实际拓扑结构。不同厂商(Intel、AMD、ARM)的 CPU 拓扑可能差异很大,调度域的层级和命名也会有所不同。

2.5 本章小结