Linux内核进程调度器深度实战:CFS完全公平调度与实时调度类内核源码剖析

进程调度是操作系统的灵魂。本文深入 Linux 内核 6.x 调度器子系统,从 CFS 红黑树 vruntime 演进、调度延迟保证、NUMA 感知负载均衡,到实时调度类的优先级继承链,全方位剖析内核调度机制的设计哲学与工程实现。

一、调度器架构总览

Linux 调度器采用分层调度类(Scheduling Classes)架构,每种调度类实现一组操作接口,按优先级链表串联:

stop_sched_class(最高优先级)
    → dl_sched_class(Deadline 调度)
    → rt_sched_class(实时调度:FIFO/RR)
    → fair_sched_class(CFS 完全公平调度)
    → idle_sched_class(空闲调度,最低优先级)

这种设计的精妙之处在于:新增调度策略无需修改现有代码,只需实现 sched_class 结构体并注册到全局链表。Linux 6.1 引入的 Earliest Deadline First (EDF) Deadline 调度器就是通过这种机制无缝集成的。

每个 CPU 运行队列 rq(cfs_rq + rt_rq)独立管理,通过 runqueues[cpu] 全局数组访问。多核间的负载均衡由 sched_domain 拓扑层级驱动:

// 调度域拓扑:物理封装 → NUMA 节点 → 全系统
struct sched_domain {
    struct sched_domain *parent;  // 上层域
    struct sched_domain *child;   // 下层域
    unsigned long min_interval;   // 最小均衡间隔
    unsigned long max_interval;   // 最大均衡间隔
    unsigned int  balance_interval;// 当前平衡间隔
    unsigned int  busy_factor;    // 忙碌系数
};

调度触发点主要有三个:

  1. 主动让出:schedule() 直接调用(cond_resched、mutex_unlock 等)
  2. 周期时钟:scheduler_tick() 每 1/HZ 秒触发(通常 1ms 或 4ms)
  3. 唤醒抢占:try_to_wake_up() 唤醒高优先级任务时检查抢占条件

二、CFS 完全公平调度器核心算法

2.1 vruntime:虚拟运行时间的数学建模

CFS 的核心思想是按权重比例分配 CPU 时间,而非传统的时间片轮转。每个进程维护一个 vruntime(virtual runtime),表示该进程在"\"公平标准\""下的累计运行时间:

// 权重与运行时间转换公式
// 实际运行时间 delta_exec 转换为虚拟运行时间:
vruntime += delta_exec * (NICE_0_LOAD / curr->load.weight)

// 等价于:高权重进程 vruntime 增长慢,低权重进程 vruntime 增长快
// 保证在任意时刻,所有可运行进程的 vruntime 趋于相等

其中 NICE_0_LOAD = 1024(nice=0 的基准权重),每级 nice 差约为 10% CPU 权重差异。这种对数刻度的设计源自 sched_prio_to_weight[] 数组:

const int sched_prio_to_weight[40] = {
 /* -20 */ 88761, 71755, 56483, 46273, 36291,
 /* -15 */ 29154, 23254, 18705, 14949, 11916,
 /* -10 */ 9548,  7620,  6100,  4904,  3906,
 /* -5  */ 3121,  2501,  1991,  1586,  1277,
 /*  0  */ 1024,  820,   655,   526,   423,
 /*  5  */ 335,   272,   215,   172,   137,
 /* 10  */ 110,   87,    70,    56,    45,
 /* 15  */ 36,    29,    23,    18,    15,
};

2.2 红黑树:O(log n) 的下一个调度决策

CFS 使用红黑树(rb_tree)按 vruntime 组织所有可运行进程。树的最左侧节点(rb_leftmost)始终是 vruntime 最小的进程,即下一个该调度的任务,查询复杂度 O(1)。

当进程状态变为可运行(enqueue_entity()),CFS 执行 __enqueue_entity() 插入红黑树:

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    // 标准红黑树插入逻辑
    
    // 如果 se->vruntime 比当前最小值还小,更新 leftmost 指针
    if (leftmost) {
        cfs_rq->rb_leftmost = &se->run_node;
    }
    rb_link_node(&se->run_node, parent, link);
    rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}

关键细节:enqueue_entity() 中并非总是插入最右侧,而是按 vruntime 排序。刚唤醒的进程通过 place_entity() 的 vruntime 补偿机制,避免长时间睡眠的进程被过度补偿而导致"\"饥饿震荡\""。

2.3 最小 vruntime 补偿与睡眠进程唤醒

一个细节决定了公平性的精确度:当一个进程睡眠后唤醒,如果它的 vruntime 远小于系统当前最小值,它将获得不成比例的 CPU 时间。CFS 通过以下策略解决:

// place_entity() 中的睡眠补偿逻辑
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
    u64 vruntime = cfs_rq->min_vruntime;
    
    if (initial)
        vruntime += sched_vslice(cfs_rq, se);
    else
        vruntime -= sysctl_sched_latency;  // 减去一个调度延迟作为补偿
        
    // 限制补偿下限:不低于 min_vruntime - threshold
    se->vruntime = max_vruntime(se->vruntime, vruntime);
}

sysctl_sched_latency 默认 6ms,可通过 /proc/sys/kernel/sched_latency_ns 调整。这是公平性与响应延迟的权衡参数:

  • 较小值(2ms):短进程响应更快,但上下文切换开销增大,吞吐量下降
  • 较大值(24ms):吞吐量提升,但交互式进程可能感知卡顿

2.4 调度延迟保证(sched_latency 自适应)

CFS 保证每个可运行进程在一个调度周期内至少运行一次:

// 调度周期计算
static u64 sched_period(unsigned long nr_running)
{
    if (nr_running <= sched_nr_latency)  // 默认 8
        return sysctl_sched_latency;      // 6ms
    return nr_running * sysctl_sched_min_granularity;  // 0.75ms × N
}

// 单个进程的最小时间片
#define sysctl_sched_min_granularity  750000ULL  // 0.75ms

当可运行进程数 N <= sched_nr_latency (8) 时,所有进程都能在 6ms 内获得 CPU。当 N > 8 时,周期拉伸为 N × 0.75ms,保证最小执行时间。这种自适应缩放是 CFS 在高负载下维持公平的关键设计。

三、调度组与层级调度(CGroup 集成)

现代 Linux 调度器支持组调度(CONFIG_FAIR_GROUP_SCHED),允许将 CPU 资源按 CGroup 层级分配。每个 cfs_rq 可包含一个 sched_entity 作为"\"虚拟参与者在父级 cfs_rq 中竞争\""。

// 多级调度实体
//       cpu0 cfs_rq
//         ├── 用户A 组 (cpu.shares=2048)
//         │     ├── 进程P1 (nice=0, weight=1024)
//         │     └── 进程P2 (nice=0, weight=1024)
//         └── 用户B 组 (cpu.shares=1024)
//               └──进程P3 (nice=0, weight=1024)

// 结果:P1/P2 的调度权重被归一化为用户A组内的 50% 竞争
// P3 与 "用户A组的虚拟实体" 在父级 1:2 竞争

这种虚拟化带来一个关键问题:如何阻止\"CGroup 内大量进程\"抢占其他组? CFS 通过 throttle_cfs_rq() 实现带宽控制——当组的 cfs_bandwidth 配额耗尽,整个组的所有进程都被从红黑树移除(出队),直到下一周期配额补充。

四、实时调度类深度剖析

4.1 SCHED_FIFO vs SCHED_RR

Linux 的实时调度类(rt_sched_class)提供两种策略,优先级范围 0-99(对应内核 MAX_RT_PRIO=100,实际范围为 0-99):

策略行为时间片抢占条件
SCHED_FIFO先进先出,运行直到阻塞或主动让出无(无限)仅更高优先级实时进程
SCHED_RR时间片轮转,同优先级间公平分配默认 100ms(sched_rr_timeslice)更高优先级实时进程或更高优先级 CFS 进程时间片耗尽

实时进程的优先级始终高于普通 CFS 进程。这意味着任何 sched_setscheduler(..., SCHED_FIFO, param->sched_priority=1) 的进程都会抢占运行中的 CFS 进程。

4.2 rt_rq 的数据组织:100 级优先级队列

不同于 CFS 的红黑树,rt_rq 使用位图 + 链表数组组织 100 级优先级:

struct rt_prio_array {
    DECLARE_BITMAP(bitmap, MAX_RT_PRIO);  // 128 bits 位图
    struct list_head queue[MAX_RT_PRIO];   // 100 个链表头
};

struct rt_rq {
    struct rt_prio_array active;
    unsigned long rt_nr_running;
    struct rq *rq;
};

取下一个任务的操作是 find_first_bit() 扫描位图(硬件指令优化,通常 1-2 条指令),然后从对应链表摘下第一个节点。查 O(1),入队/出队 O(log P)(P=优先级),比红黑树更轻量,符合实时系统的确定性要求。

4.3 优先级继承(Priority Inheritance)解决优先级反转

经典场景:低优先级进程 L 持有互斥锁 → 高优先级进程 H 等待该锁 → 中优先级进程 M 就绪并抢占 L(因为 M > L)→ H 被 M 间接阻塞。

Linux 通过 RT-Mutex 的优先级继承协议解决:

// 优先级继承流程
// 1. H 尝试 lock(rt_mutex) 被 L 持有
// 2. H 调用 rt_mutex_waiter 将自身 priority "提升"给 L
// 3. L->pi_waiter = H 继承 H 的优先级
// 4. L 以 H 的高优先级运行,不被 M 抢占
// 5. L unlock 后恢复原优先级,H 获得锁运行

struct rt_mutex_waiter {
    struct plist_node list_entry;     // 按优先级排序的等待者列表
    struct plist_node pi_list_entry;  // 持有者优先级继承链表
    struct rt_mutex *lock;            // 关联的互斥锁
    struct task_struct *task;         // 等待者进程
    struct task_struct *deadlock_task; // 死锁检测
};

plist(优先级链表)保证继承者始终获得当前等待者中最高的优先级。多个嵌套锁时,优先级会沿锁链级联传播。

4.4 Deadline 调度器(SCHED_DEADLINE)

Linux 3.14+ 引入的 Deadline 调度器基于 EDF(Earliest Deadline First)+ CBS(Constant Bandwidth Server),是强实时保证的最佳实现:

// 三个参数定义实时任务的时间约束
struct sched_attr {
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime  = 10 * 1000 * 1000,   // 10ms 每周期最大运行时间
    .sched_deadline = 20 * 1000 * 1000,   // 20ms 截止期限
    .sched_period   = 20 * 1000 * 1000,   // 20ms 周期
};

CBS 调度保证:每个 DL 任务在周期内获得的 CPU 时间不超过 runtime/deadline × period,防止"\"全局 EDF\""中单个高 runtime 任务饿死其他任务。SCHED_DEADLINE 优先级 高于 SCHED_FIFO/RR,保证了硬实时任务的首要地位。

五、多核负载均衡与 NUMA 感知

5.1 负载均衡触发条件与层级

内核在三个层级间迁移任务:

  1. IDLE 均衡(最激进):当 CPU IDLE 时,从最忙的组偷任务,无间隔限制
  2. NEWLY IDLE 均衡:进程从阻塞变为可运行时唤醒均衡
  3. 周期性均衡(tick-based):tick 或固定间隔检查各域内负载差异

负载计算使用指数移动平均(PELT, Per-Entity Load Tracking):

// 负载贡献的 PELT 衰减模型
// y^n = y^(n-1) * y + load × (1 - y)
// y = 0.978572 (半衰期 32ms)
// 周期:每 1ms 调用 decay_load() 乘以衰减因子

// 多核层级负载聚合
// cfs_rq->h_load: 追踪组内活跃/可运行的历史负载
// cfs_rq->runnable_load: 组内所有可运行进程的负载
// 核心公式: load_avg = load × (runnable_avg / running_avg)

PELT 的半衰期是 32ms,意味着约 32ms 前的负载贡献衰减到 50%。这种快速衰减使调度器对负载突变(突发创建进程、批处理任务结束)有秒级响应。

5.2 NUMA 均衡:migrate_degraded_cache vs migrate_improved_util

NUMA 架构下,本地节点内存访问延迟可远低于跨节点。Linux 通过两种策略优化任务分布:

  • NUMA Balancing(平衡):扫描进程的页表,统计各 NUMA 节点的页面访问频率,迁移任务到其主要内存节点。
  • NUMA Migration(迁移):在负载均衡中引入节点亲和性权重,优先在节点内均衡。

关键阈值由 /proc/sys/kernel/numa_balancing 和 scan_period_min/max(默认 100ms - 60s)控制。扫描粒度 scan_size 默认 256MB(或每次 4 个粒度)。

5.3 SMT/HT 感知调度

在现代 CPU 上(如 Intel 12/13/14 代混合架构 P/E 核),调度面临全新挑战:

// Linux 6.x 的 ITMT / CPPC 支持
// P-core(性能核)与 E-core(能效核)区分:

1.ASF(Intel Speed Select):
  - 通过 ACPI CPPC 获取每个 CPU 的 performance hint
  - 调度器知道 P-core 比 E-core 性能高 ~30-50%
  
2.异构调度核心(HMP):
  - capacity 数组记录每个 CPU 的相对计算能力
  - P-capacity = 1024, E-capacity = 512(示例)
  - CFS 计算 vruntime 时除以 capacity,P-core 进程"老"得更慢

3.唤醒放置策略(WAKEUP):
  - 唤醒优先选择空闲 P-core
  - 若 P-core 满负载,小任务可填 E-core
  - 通过 find_idlest_group_cpu() 实现

六、调度器性能调优参数速查表

参数路径默认值调优建议
sched_latency_ns/proc/sys/kernel/6ms(2-3 核),18ms(多核)交互式桌面:降为 4ms;批处理服务器:升为 24ms
sched_min_granularity_ns/proc/sys/kernel/0.75ms吞吐量优先:升为 4ms;延迟敏感:降低至 0.5ms
sched_wakeup_granularity_ns/proc/sys/kernel/1ms降低:提升唤醒响应(吞吐下降);升高:减少抢占
sched_migration_cost_ns/proc/sys/kernel/500μs增加:减少缓存热迁移;降低:更激进均衡
sched_cfs_bandwidth_slice_us/proc/sys/kernel/5000μsCGroup CPU 带宽控制的计费粒度
sched_rt_period_us/proc/sys/kernel/1s实时进程的 CPU 时间账户刷新周期
sched_rt_runtime_us/proc/sys/kernel/0.95s限制实时进程每 1s 内最多使用 950ms CPU
sched_deadline_period_max_us/proc/sys/kernel/4sSCHED_DEADLINE 最大周期上限
numa_balancing/proc/sys/kernel/0(关闭)NUMA 服务器开启:echo 1 > numa_balancing
sched_energy_aware/sys/devices/system/cpu/0(关闭)混合架构 CPU 开启:echo 1 > sched_energy_aware

七、内核调试与性能观测实战

7.1 ftrace 跟踪调度事件

# 跟踪进程切换事件
$ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
$ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_migrate_task/enable
$ cat /sys/kernel/debug/tracing/trace_pipe

# 输出示例:
#   <idle>-0     [001] d.h3   100.234: sched_switch: prev_comm=swapper/1 prev_pid=0 
#                             next_comm=myapp next_pid=1234 prev_state=R==> next_prio=120

7.2 perf sched 可视化调度延迟

# 记录调度事件 30 秒并生成详细报告
$ perf sched record -a sleep 30
$ perf sched latency --sort max

# 输出示例显示最大调度延迟:
#  Task                  |   Average (ms) |   Maximum (ms) |
# -----------------------|---------------|----------------|
#  streaming_encode (T1234) |           0.8 |          45.2 ← 延迟异常
#  database_query  (T5678)  |           0.3 |           3.1

7.3 BPFTrace 探针分析 vruntime 异常

// 跟踪 CFS 红黑树插入时的 vruntime 值
$ sudo bpftrace -e '
tracepoint:sched:sched_wakeup {
    $task = (struct task_struct *)args->pid;
    printf("wake: pid=%d comm=%s vruntime=%llu
", 
           $task->pid, $task->comm, $task->se.vruntime);
}'

7.4 /proc/<pid>/sched 深度解读

$ cat /proc/1234/sched
my_app (1234, #threads: 1)
----------------------------------------------------
se.exec_start       :    1234567890  # 最近开始执行时间(ns)
se.vruntime          :    234567890  # 当前 vruntime
se.sum_exec_runtime  :     12345678  # 总运行时间
se.nr_migrations     :           42  # 跨 CPU 迁移次数
nr_switches          :         1234  # 总上下文切换
nr_voluntary_switches :         1000  # 主动让出(等待 IO)
nr_involuntary_switches:         234 # 时间片耗尽被抢占!需关注
policy               :            0  # SCHED_NORMAL
prio                 :          120  # nice=0
clock-delta          :           67  # 时钟源差异

nr_involuntary_switches / nr_switches 比率如果超过 15-20%,说明进程频繁被抢占,可能需要:增大 sched_min_granularity_ns、绑定 CPU(taskset)、或提升实时优先级。

八、总结:调度器设计哲学

Linux 调度器的演进是一部"\"公平与效率的平衡史\"":

  • O(1) 调度器(2.6 之前):固定时间片 + 优先级数组,查 O(1),但低优先级进程饥饿
  • CFS(2.6.23+):红黑树 + vruntime,算法公平但 O(log n),随 sched_nr_latency 自适应
  • Deadline(3.14+):CBS/EDF 带宽隔离,强实时子系统
  • PELT / NUMA Balancing(3.8+):多级负载追踪与 NUMA 感知放置
  • 异构调度(5.x+):混合架构容量感知 + 能效优化

理解这些机制,才能正确回答"\"为什么我的实时进程偶尔卡一下\""、"\"为什么这个 CGroup 的 CPU 打不上去\""、"\"为什么跨 NUMA 迁移后延迟翻倍\""等系统工程问题的根源。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部