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,如果是,则触发抢占。
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 规则进行二次分配。
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。跨节点内存访问延迟较高。
2.2.2 调度域与负载均衡
每个调度域都有一个 负载均衡器(load_balance),定期检查域内各 CPU 运行队列的负载是否均衡。如果发现某个 CPU 过载而另一个空闲,均衡器会尝试将进程从过载 CPU 迁移到空闲 CPU。迁移策略会考虑调度域的层级:优先在同一 Domain 0 内迁移(代价最小),必要时才跨 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 上。这种“组间均衡”策略避免了频繁的跨核心迁移,提高了缓存亲和性。
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