Linux CFS 调度器深度解析:从算法原理到性能调优实战

Linux CFS 调度器深度解析:从算法原理到性能调优实战

第一章:调度器概述与演进

1.1 调度器的核心目标与衡量指标

操作系统调度器的本质是在有限的 CPU 资源上,以合理的方式分配给多个竞争进程。一个优秀的调度器需要同时兼顾多个目标,而这些目标之间往往存在矛盾:

指标含义优化目标
吞吐量 (Throughput)单位时间内完成的工作量最大化
响应时间 (Latency/Response Time)从触发到首次获得 CPU 的时间最小化
公平性 (Fairness)每个任务获得 CPU 份额的比例与权重成正比精确保障
CPU 利用率CPU 非空闲时间占比最大化
能效比 (Power Efficiency)每瓦特功耗完成的计算量最大化
核心矛盾:吞吐量要求大粒度时间片(减少切换开销),而响应时间要求小粒度时间片(快速切换)。CFS 的"完全公平"设计正是试图在这组矛盾中找到优雅的平衡点。

1.2 从 O(n) 到 O(1) 再到 CFS:调度器的演进之路

Linux 调度器经历了三个阶段的重要演进:

第一代:O(n) 调度器(Linux 2.4 时代)

// 早期 O(n) 调度器核心逻辑
for (每个可运行进程 p) {
    p->counter = (p->counter >> 1) + p->nice;  // 重新计算时间片
    if (p->counter > best->counter)
        best = p;  // 遍历选择 counter 最大的进程
}

问题:随着进程数量增长,调度决策时间线性增长,严重限制了可扩展性。

第二三代:O(1) 调度器(Linux 2.6.0 - 2.6.22)

引入了两个优先级数组(active/expired)和 140 级优先级队列,实现了常数级调度决策。但其基于固定时间片和静态优先级表的"启发式"规则在多核场景下表现不佳,交互进程的探测逻辑也日趋复杂。

第三代:CFS(Completely Fair Scheduler, Linux 2.6.23+)

Ingo Molnár 提出的 CFS 颠覆了传统调度器的设计思路——不再分配时间片,而是追踪每个进程的虚拟运行时间,选择 vruntime 最小的进程运行。这一设计简洁而深刻,至今仍是 Linux 默认的公平调度器。

1.3 CFS 的核心哲学:"完全公平"与虚拟运行时间

CFS 的核心思想可以概括为一句话:让所有可运行进程的虚拟运行时间(vruntime)以相同的速率增长。

所谓"完全公平",是指在没有进程阻塞的理想多核系统中,每个进程应该获得与其权重成正比的实际 CPU 时间。CFS 不承诺完美公平——那是 NP-hard 问题——而是通过 vruntime 这个抽象概念实现"近似完全公平":

  • 每个进程维护一个 vruntime(纳秒级)
  • 进程每运行 1 毫秒实际时间,高权重进程的 vruntime 增长慢,低权重进程增长快
  • 调度时总是选择 vruntime 最小的进程上获得 CPU
  • 红黑树保证选择操作 O(log n) 复杂度

1.4 调度类与优先级层次结构

Linux 调度器采用调度类(sched_class)的插件化架构,不同类别的调度器以优先级层次共存:

/*  kernel/sched/sched.h  */
extern const struct sched_class stop_sched_class;     /* 最高优先级 - 停止任务 */
extern const struct sched_class dl_sched_class;      /* 最早截止时间优先 */
extern const struct sched_class rt_sched_class;       /* 实时调度 (FIFO/RR) */
extern const struct sched_class fair_sched_class;     /* CFS - 普通进程 */
extern const struct sched_class idle_sched_class;     /* 空闲任务 */
调度类策略优先级说明
stop_sched_classN/A最高CPU 热插拔/迁移时的抢占式中断级任务
dl_sched_classSCHED_DEADLINE次高最早截止时间优先算法,硬实时保障
rt_sched_classSCHED_FIFO / SCHED_RR高POSIX 实时调度,不受 CFS 时间片约束
fair_sched_classSCHED_NORMAL / SCHED_BATCH / SCHED_IDLE中CFS 完全公平调度器
idle_sched_classN/A最低CPU 无任务时的 idle 线程

调度决策从最高优先级类开始依次查找,只有高优先级类没有可运行进程时,才会进入下一级。这意味着 所有实时进程在 CFS 获得 CPU 之前就被优先调度。

第二章:CFS 核心数据结构

2.1 task_struct 中的调度相关字段

每个进程的 task_struct 中嵌入了 sched_entity 结构体,这是 CFS 调度器的核心调度单元:

/* include/linux/sched.h */
struct task_struct {
    /* 调度相关 */
    int                        prio;            /* 动态优先级 [0-139] */
    int                        static_prio;     /* 静态优先级 (nice+120) */
    int                        normal_prio;     /* 基于 static_prio 和调度策略 */
    unsigned int               rt_priority;     /* 实时优先级 [0-99] */

    const struct sched_class    *sched_class;    /* 调度类指针 */

    struct sched_entity          se;              /* CFS 调度实体 */
    struct sched_rt_entity       rt;              /* 实时调度实体 */
    struct sched_dl_entity       dl;              /* 截止时间调度实体 */

#ifdef CONFIG_SMP
    int                        recent_used_cpu;  /* 最近使用的 CPU */
    int                        wake_cpu;         /* 唤醒时选择的 CPU */
#endif
};

2.2 sched_entity 深度解析

sched_entity 是每个 CFS 可运行实体的核心数据结构:

/* include/linux/sched.h */
struct sched_entity {
    struct load_weight  load;        /* 权重(用于份额计算) */
    unsigned long       runnable_weight; /* CFS bandwidth 用 */
    struct rb_node      run_node;     /* 红黑树节点 */

    u64                  exec_start;       /* 本次调度开始时间 */
    u64                  vruntime;         /* 虚拟运行时间 (ns) */
    u64                  prev_sum_exec_runtime; /* 上次累计运行时间 */
    u64                  nr_migrations;    /* 跨 CPU 迁移次数 */

    struct sched_statistics statistics; /* 统计信息 */

#ifdef CONFIG_FAIR_GROUP_SCHED
    int                  depth;        /* 层级深度 */
    struct sched_entity  *parent;     /* 父调度实体 */
    /* cfs_rq 的引用 */
    struct cfs_rq         *cfs_rq;     /* 所属运行队列 */
    struct cfs_rq         *my_q;       /* 子 cfs_rq(组调度时) */
#endif
};

2.3 cfs_rq:每 CPU 的公平调度运行队列

/* kernel/sched/sched.h */
struct cfs_rq {
    struct load_weight load;         /* 队列总权重 */
    unsigned int nr_running;         /* 可运行任务数 */
    unsigned int h_nr_running;       /* 包含子组的总数 */

    u64 exec_clock;               /* CPU 执行时间(单调递增) */
    u64 min_vruntime;             /* 队列最小 vruntime(公共基准) */
    struct rb_root_cached tasks_timeline; /* 红黑树根(按 vruntime 排序) */

    struct sched_entity *curr;    /* 当前运行实体 */
    struct sched_entity *next;    /* 下一个要抢占的 */
    struct sched_entity *last;    /* 上次运行的(缓存热 */

#ifdef CONFIG_SMP
    /*
     * CPU 负载跟踪(PELT / WALT)
     * 用于负载均衡决策
     */
    unsigned long runnable_load_avg;  /* 平均可运行负载 */
    unsigned long blocked_load_avg;   /* 阻塞状态负载贡献 */
    u64 last_blocked_load_stamp;
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
    struct rq *rq;                    /* 绑定的物理 runqueue */
    struct task_group *tg;            /* 所属的 task_group */
#endif
};

2.4 红黑树:为什么不用哈希表或数组?

CFS 选择红黑树作为 vruntime 排序容器,是有深思熟虑的设计选择:

容器类型插入删除查找最小值范围查询
未排序链表O(1)O(n)O(n)O(n)
最小堆O(log n)O(n)O(1)O(n log n)
红黑树O(log n)O(log n)O(log n) (cached O(1))O(log n + k)
排序数组O(n)O(n)O(1)O(log n + k)

红黑树的选择原因:

  1. 查找最小值 O(1) 均摊:rb_leftmost 指针(rb_root_cached)缓存最左节点
  2. 稳定的 O(log n) 插入/删除:唤醒进程、阻塞进程都是 O(log n)
  3. 天然支持范围查询:pick_next、vruntime 追赶等操作需要"从最左边开始向右搜索"
rb_root_cached 是红黑树的一个增强结构,它在红黑树根节点上额外保存了一个 rb_node *rb_leftmost 指针,使获取最小值操作变为 O(1)。这是 CFS 高性能的关键微优化之一。

2.5 权重计算与 load_weight

CFS 的公平性通过权重(weight)实现。每个 sched_entity 的权重决定了它相对于其他任务获得的 CPU 时间比例:

/* 权重计算的核心公式 */
/*
 * 实际获得CPU时间 = (进程权重 / cfs_rq总权重) * 调度周期
 * 
 * 两个进程A(权重1024)和B(权重3072)竞争:
 * A获得: 1024/(1024+3072) = 25%
 * B获得: 3072/(1024+3072) = 75%
 */

/* update_load_avg() 中 PELT 负载跟踪 */
static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    u64 now = cfs_rq_clock_pelt(cfs_rq);

    if (se->avg.last_update_time) {
        /* 更新 PELT (Per-Entity Load Tracking) */
        ___update_load_avg(now, &se->avg);
        cfs_se_util_change(&se->avg);
    }
    /* ... */
}

2.6 pick_next_task_fair:选择下一个进程

/* kernel/sched/fair.c */
static struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    struct task_struct *p;

    /* 步骤1: 如果不是全换出,尝试简单处理 prev */
    if (prev)
        put_prev_task(rq, prev);

    /* 步骤2: 从红黑树最左节点选出 vruntime 最小的实体 */
    do {
        se = pick_next_entity(cfs_rq, NULL);
        set_next_entity(cfs_rq, se);    /* 标记为当前运行 */
        cfs_rq = group_cfs_rq(se);       /* 如果是组调度,递归 */
    } while (cfs_rq);

    p = task_of(se);

    /* 步骤3: 信息统计与追踪 */
    if (hrtick_enabled(rq))
        hrtick_start_fair(rq, p);

    return p;
}

关键在于 pick_next_entity() 的实现——它总是选择红黑树最左侧(最小 vruntime)的节点:

static inline struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    struct sched_entity *left = __pick_first_entity(cfs_rq); /* O(1) */
    struct sched_entity *se;

    /*
     * 如果 curr 的 vruntime 不比 left 大太多,
     * 让 curr 继续运行(减少缓存失效)
     */
    if (curr && (!left || entity_before(curr, left)))
        se = curr;
    else
        se = left;

    return se;
}

第三章:虚拟运行时间机制

3.1 vruntime 的计算公式

虚拟运行时间(vruntime)是 CFS 公平性的度量衡。其核心计算公式为:

/* kernel/sched/fair.c - update_curr() */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(cfs_rq->rq);   /* 当前时间戳 */
    u64 delta_exec;

    delta_exec = now - curr->exec_start;     /* 实际运行时间 */
    if ((s64)delta_exec <= 0)
        return;

    curr->exec_start = now;                  /* 更新下次起始时间 */

    /* ===== 核心公式 ===== */
    curr->vruntime += calc_delta_fair(delta_exec); /* 更新 vruntime */
    /* ================== */

    update_min_vruntime(cfs_rq);             /* 更新队列最小值 */
}

/* calc_delta_fair: 将实际时间转换为虚拟时间 */
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
    /* 基准权重是 NICE_0_LOAD = 1024 */
    if (se->load.weight != NICE_0_LOAD)
        delta = __calc_delta(delta, NICE_0_LOAD, &se->load); /* delta * 1024 / weight */

    return delta;
}

/* 宏展开: __calc_delta(delta, NICE_0_LOAD, weight)
 * 等价于: delta * 1024 / weight
 * 
 * nice 0 (weight=1024): vruntime 增长速率 = 实际时间 (1:1)
 * nice -5 (weight=1280): vruntime 增长慢 → 更频繁获得 CPU
 * nice +5 (weight=102): vruntime 增长快 → 获得 CPU 更少
 */
关键公式:Δvruntime = Δreal_time × (NICE_0_LOAD / weight)
权重越大的进程,vruntime 增长越慢,因此被选中运行的频率越高,获得的实际 CPU 时间也越多。

3.2 nice 值到权重的转换表

Linux 内核使用预计算的 prio_to_weight 表将 [-20, +19] 的 nice 值映射到权重:

/* kernel/sched/core.c */
/*
 * Nice levels are multiplicative, with a gentle 10% change in
 * CPU usage for every nice level changed.
 * 每增加 1 级 nice 值,CPU 时间减少约 10%
 * (因为 nice+1 的权重是 nice 的 88.79%, 1/0.8879 ≈ 1.126 → 12.6%)
 *
 * 实际上 1024/1270 ≈ 0.806 → 1/0.806 ≈ 1.24, 但经验值约为 10%
 * 因为调度周期内还涉及 slice 和 wakeup_granularity 等配置
 */
static const int 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,
};
nice权重相对 CPU 份额典型应用场景
-208876186.7%紧急系统任务
-1095489.3%高优先级服务
0 (默认)10241.00x (基准)普通进程
+101100.107x低优先级批处理
+19150.015x几乎仅在空闲时运行

相邻 nice 值的权重比例约为 1.25(即 1024/820 ≈ 1.249),这意味着每调高 1 级 nice 值,进程大约少获得 20% 的 CPU 份额(与老的 O(1) 调度器每级约 10% 不同)。

3.3 时钟中断中的 vruntime 更新路径

/* 时钟中断触发路径 */
tick_handle_periodic()
  → tick_periodic()
    → update_process_times()      /* 内核定时器框架 */
      → scheduler_tick()         /* CFS 时钟处理入口 */
        → task_tick_fair()       /* 调用调度类的 tick 方法 */
          → entity_tick()         /* 检查是否需要抢占 */
            → update_curr()       /* 更新当前进程 vruntime */
            → resched_curr()      /* 必要时设置 need_resched 标志 */

在 entity_tick() 中,CFS 检查当前进程是否已经用完了自己的"期望运行时间"(ideal_runtime):

static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
    /* 1. 更新 vruntime */
    update_curr(cfs_rq);

    /* 2. 理想运行时间检查 */
    if (cfs_rq->nr_running > 1) {
        struct sched_entity *se = __pick_first_entity(cfs_rq);

        /* 判断当前进程是否比次优者领先太多 */
        if (entity_before(se, curr) ||
            curr->vruntime - se->vruntime > sched_slice(cfs_rq, curr)) {
            resched_curr(rq_of(cfs_rq));
        }
    }
}

3.4 新建进程的 vruntime 继承策略

新进程创建时会继承父进程的 vruntime,但这需要特殊处理,否则新进程会因继承较大的 vruntime 而长时间得不到 CPU:

/* kernel/sched/fair.c - task_fork_fair() */
static void task_fork_fair(struct task_struct *p)
{
    struct cfs_rq *cfs_rq = task_cfs_rq(current);
    struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
    struct rq *rq = this_rq();
    struct rq_flags rf;

    update_rq_clock(rq);
    update_curr(cfs_rq);  /* 确保父进程 vruntime 是最新的 */

    /* 策略1: 子进程的 vruntime 不小于队列 min_vruntime */
    if (sched_feat(PLACE_CHILD_SAME_CPU))
        se->vruntime = curr->vruntime;  /* 继承父进程 vruntime */
    else
        se->vruntime = max_vruntime(se->vruntime, cfs_rq->min_vruntime);

    /*
     * 策略2: RUN_TO_POSITION 特性
     * 新建进程默认可以抢占同级的旧进程
     * (如果 sched_features 开启了 PLACE_DEADLINE_INITIAL)
     */
    if (sched_feat(PLACE_DEADLINE_INITIAL))
        se->vruntime -= sched_latency(se);
}

3.5 时钟漂移与 NUMA 影响

在 NUMA 系统中,不同节点的时钟计数器(TSC)可能不完全同步。CFS 使用 sched_clock() 获取每个 CPU 的本地时间:

/* kernel/sched/clock.c */
u64 __always_inline sched_clock(void)
{
    if (static_branch_unlikely(&sched_clock_stable))
        return sched_clock_cpu(smp_processor_id());
    
    return sched_clock_noincrement(); /* 回退方式 */
}

/*
 * rq_clock_task(): CFS 使用的任务时钟
 * 比当前 CPU 的 rq->clock 更精确,扣除了 IRQ 时间
 */
static inline u64 rq_clock_task(struct rq *rq)
{
    return rq->clock_task;
}

第四章:多核调度与负载均衡

4.1 每 CPU runqueue 设计

Linux 采用 per-CPU runqueue 设计以获得最佳的可扩展性,减少跨 CPU 锁竞争:

/* kernel/sched/sched.h */
struct rq {
    /* 运行队列锁(自旋锁) */
    raw_spinlock_t __aligned(sizeof(long)) lock;

    /* 三个调度类的运行队列 */
    struct cfs_rq cfs;       /* CFS 公平队列 */
    struct rt_rq   rt;        /* 实时队列 */
    struct dl_rq   dl;        /* 截止时间队列 */

    /* 当前运行任务 */
    struct task_struct *curr, *idle, *stop;

    unsigned int nr_running;       /* 可运行总任务数 */
    unsigned long cpu_load[5];     /* 历史负载均线 */

    /* 负载跟踪 */
#endif

#ifdef CONFIG_SMP
    struct sched_domain __rcu *sd;  /* 调度域起始指针 */
    unsigned long  rd_load;        /* 运行域负载 */

    unsigned int  cpu_capacity;     /* CPU 计算能力 */
    unsigned int  cpu_capacity_orig; /* 原始计算能力 */
#endif
};

/* 每 CPU 声明 */
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
per-CPU 设计的好处:
  • 每个 CPU 独立操作自己的 runqueue,无需跨 CPU 加锁
  • 缓存亲和性更好:任务在自己的 CPU 上被调度,L1/L2 cache 热
  • 减少总线锁争用,SMP 可扩展性显著提升

4.2 负载均衡算法

Linux 的负载均衡在多个层次运行,形成一种"自底向上"的均衡策略:

/* 负载均衡触发路径 */
scheduler_tick()
  → trigger_load_balance()   /* 时钟中断触发 */
    → raise_softirq(SCHED_SOFTIRQ)
均衡类型触发时机CPU 关系迁移代价
Idle BalanceCPU 进入 idle 前目标到空闲 CPU最低
Periodic Balancetick / softirq调度域内 Busy CPU 间中
New Idle Balance刚变为 idle 的 CPU整个调度域搜索中
NUMA Balancing缺页异常时NUMA 节点间最高
/* 核心均衡函数调用链 */
run_rebalance_domains()
  → rebalance_domains(this_rq, idle)
    → sd->balance_cpu()  /* 遍历调度域自顶向下 */
      → load_balance(sd, this_rq, idle, continue_balancing)
        → find_busiest_group(sd)   /* 找到最繁忙的调度组 */
        → move_tasks()             /* 从繁忙组移动任务到本组 */
          → detach_tasks()         /* 挑选合适任务 */
          → attach_tasks()         /* 加入目标队列 */

4.3 调度域(sched_domain)与调度组(sched_group)

调度域是 Linux 用于描述 CPU 拓扑的层级化抽象,每一层对应一种物理拓扑级别:

/* kernel/sched/sched.h */
struct sched_domain {
    /* 子域(更底层拓扑) */
    struct sched_domain *parent;     /* 必须被均衡后才均衡本层 */
    struct sched_domain *child;      /* 基础域(SMT 或 MC) */

    /* 调度域内的调度组(环形链表) */
    struct sched_group *groups;      /* 第一个调度组 */

    /* 均衡参数 */
    unsigned int min_interval;       /* 最小均衡间隔 */
    unsigned int max_interval;       /* 最大均衡间隔 */
    unsigned int busy_factor;        /* 繁忙因子 */
    unsigned int imbalance_pct;       /* 不平衡触发阈值 */

    /* 不均衡标志位 */
    unsigned long flags;             /* SD_LOAD_BALANCE/SD_BALANCE_WAKE 等 */

    /* 统计信息 */
    unsigned int lb_count[NUM_LB_TYPES]; /* 各类型均衡执行次数 */
};

struct sched_group {
    struct sched_group *next;        /* 环形链表 */
    const struct sched_group_energy *sge; /* 能耗数据 */
    unsigned int group_weight;       /* 组内 CPU 数量 */

    /* 组的 CPU 掩码和负载统计 */
    struct cpumask cpumask;
    unsigned long cpumask_bit[BITS_TO_LONGS(nr_cpu_ids)];

    unsigned long sg_load;          /* 组的负载 */
    unsigned long sg_capacity;      /* 组的容量 */
};

典型的 x86 服务器调度域层次:

DIE (NUMA node)
  └── MC (Multi-Core)
        └── SMT (Hyper-Threading)

典型的 4-socket 服务器:
  Level 0: SMT (每个物理 Core 的两个 HT)   → 负载最频繁
  Level 1: MC (同 DIE 的所有 Core)          → 中等频率
  Level 2: DIE/NUMA (不同 node 间)          → 最不频繁

4.4 NUMA 感知调度和进程迁移

Linux 的 NUMA 调度包含两种机制:

/*
 * 1. Auto NUMA Balancing (内核 3.13+)
 * 由内核自动采样页面访问,识别 NUMA 远程页面并迁移
 * 
 * 触发路径: do_anonymous_page() / do_swap_page()
 */
task_numa_fault()
  → numa_migrate_memory()      /* 迁移内存页面 */
  → task_numa_assess()         /* 评估是否需要迁移整个进程 */
  → migrate_task_to()          /* 迁移进程到页面所在节点 */

/*
 * 2. NUMA-aware initial placement
 * 在 fork/wake 时选择进程的初始 CPU
 */
select_task_rq_fair()          /* 选择最佳唤醒 CPU */
  → select_idle_sibling()      /* 优先选择共享 cache 的空闲 CPU */
  → find_idlest_group()        /* 空闲组选择 */
  → sched_cpu_startup_entry()  /* 考虑 NUMA 距离 */
注意:Auto NUMA Balancing 不适合内存密集型工作负载中频繁随机访问模式。对于 HPC 或大内存应用,考虑使用 numactl --interleave=all 或 mbind() 手动控制。

4.5 load_balance() 与 move_tasks() 核心代码

/* kernel/sched/fair.c */
static inline int load_balance(int this_cpu, struct rq *this_rq,
               struct sched_domain *sd, enum cpu_idle_type idle,
               int *continue_balancing)
{
    /* 步骤1: 判断是否需要均衡 */
    struct lb_env env = { ... };
    struct sd_lb_stats *sds = ...;

    update_sd_lb_stats(&env, sds);

    /* 步骤2: 找到最繁忙的调度组 */
    struct sched_group *busiest = find_busiest_group(&env, sds);
    if (!busiest)
        return 0;  /* 已经均衡或不需要均衡 */

    /* 步骤3: 选择具体的迁移任务 */
    env.src_cpu = busiest_cpu;
    return move_tasks(&env, &moved);
}

第五章:组调度(cgroup CPU 子系统)

5.1 Cgroup v1 cpu 子系统参数详解

参数含义默认值典型配置
cpu.shares该组在竞争 CPU 时获得的相对份额1024Web: 2048, Batch: 512
cpu.cfs_period_us带宽核算周期(微秒)100000100ms
cpu.cfs_quota_us周期内可用 CPU 时间(微秒)-1 (unlimited)80000 (8 核限 4 核)
cpu.stat运行统计(nr_periods / nr_throttled / throttled_time)-诊断用
# 创建 cgroup
mkdir -p /sys/fs/cgroup/cpu/web_tier

# 设置相对份额(2 倍于默认)
echo 2048 > /sys/fs/cgroup/cpu/web_tier/cpu.shares

# 限制最高使用 0.5 核(50ms/100ms)
echo 100000 > /sys/fs/cgroup/cpu/web_tier/cpu.cfs_period_us
echo 50000 > /sys/fs/cgroup/cpu/web_tier/cpu.cfs_quota_us

# 将进程加入 cgroup
echo $PID > /sys/fs/cgroup/cpu/web_tier/cgroup.procs

# 查看统计(是否被 throttle)
cat /sys/fs/cgroup/cpu/web_tier/cpu.stat
输出示例:
nr_periods 12345       # 累计周期数
nr_throttled 123       # 被限流周期数
throttled_time 1.2e9   # 被限流总时间(纳秒)

5.2 层级化带宽控制实现原理

/* kernel/sched/fair.c - CFS bandwidth 控制 */

/* 
 * 每个 cfs_bandwidth 对应一个层级
 * 过 quota 时,throttle_cfs_rq() 将实体从红黑树中移除
 */
struct cfs_bandwidth {
    raw_spinlock_t lock;
    ktime_t period;
    u64 quota;              /* 周期配额 */
    u64 runtime;            /* 当前周期剩余 */
    struct hrtimer period_timer; /* 周期定时器 */
    struct list_head throttled_cfs_rq; /* 被限流的队列 */
};

/* 限流处理 */
static void throttle_cfs_rq(struct cfs_rq *cfs_rq)
{
    struct sched_entity *se;
    long task_delta, dequeue = 1;

    /* 从红黑树中移除所有子实体 */
    for_each_sched_entity(se) {
        struct cfs_rq *qcfs_rq = cfs_rq_of(se);
        /* 从红黑树中 dequeue,减少 nr_running */
        dequeue_entity(qcfs_rq, se, DEQUEUE_SLEEP);
        qcfs_rq->h_nr_running -= task_delta;
    }
    /* 子 cfs_rq 从父灰黑树中移除 */
}

/* 周期定时器回调 - 补充 quota */
static enum hrtimer_restart sched_cfs_period_timer(struct hrtimer *timer)
{
    /* 补充 runtime = quota */
    start_cfs_bandwidth(cfs_b);
    distribute_cfs_runtime(cfs_b); /* 给被限流的 rqs 恢复 */
}

5.3 容器/K8s 中的应用

K8s Pod 的 CPU requests/limits 在底层映射到 CFS cgroup 参数:

# K8s Pod 资源声明
resources:
  requests:
    cpu: "500m"    # 0.5 核 → cpu.shares = 512 */
  limits:
    cpu: "2"       # 2 核 → quota=200000, period=100000 */
K8s 资源底层映射含义
cpu.requestscpu.shares相对份额(权重),影响竞争时的分配比例
cpu.limitscpu.cfs_quota_us + cfs_period_us硬性周期配额,超限即被 throttle
无 requestsshares=2 (下限 2)至少获得极少量 CPU 份额
无 limitsquota=-1 (unlimited)可以使用空闲 CPU(按 shares 比例)

K8s CPU Throttling 的常见问题:

容器内运行单线程 CPU 密集型进程,设 limits=1 核(period=100ms, quota=100ms):

  • 该进程在一个 100ms 周期内只能运行 100ms
  • 但 CFS 调度粒度更细,进程可能在周期前端就用尽 quota
  • 随后被 throttle 在整个周期剩余时间内无法运行

5.4 Cgroup v2 cpu.weight 调优

# Cgroup v2 的 cpu 控制器参数 */

# 查看当前设置(权重范围 1-10000,默认 100)
cat /sys/fs/cgroup/web_tier/cpu.weight

# 设置高优先级组(100 倍于默认)
echo 10000 > /sys/fs/cgroup/web_tier/cpu.weight

# 设置最大带宽限制
echo "max 100000" > /sys/fs/cgroup/web_tier/cpu.max
# 含义: 在 100000us 周期内最多使用 100000us (不限制) */

echo "50000 100000" > /sys/fs/cgroup/web_tier/cpu.max
# 含义: 在 100000us 周期内最多使用 50000us (限 0.5 核) */

cat /sys/fs/cgroup/web_tier/cpu.max.burst
burst 值允许短期突发超过限制,但要在一个大周期内偿还 */

5.5 Cgroup 与 vruntime 的关系:throttle 机制

当一个 cgroup 的 cfs_rq 被 throttle 时:

  1. 该 cgroup 下的所有 sched_entity 从红黑树中移除
  2. 进程保持在 TASK_RUNNING 状态但实际上不执行
  3. vruntime 不再增长(关键!被限流的任务不会"落后"于其他任务)
  4. 当被 unthrottle 时重新入队,vruntime 维持在之前的值
/* unthrottle 时恢复策略 */
static void unthrottle_cfs_rq_async(struct cfs_rq *cfs_rq)
{
    /* 重新入队,vruntime 不变 → 被限流进程不会获得不公平的优势 */
    for_each_sched_entity(se) {
        enqueue_entity(cfs_rq, se, ENQUEUE_WAKEUP);
        /* vruntime 保持不变 → 进程回到队列中正确位置 */
    }
}

第六章:性能调优实战

6.1 核心可调参数详解

参数路径默认值含义
sched_min_granularity_ns/proc/sys/kernel/sched_min_granularity_ns1000000 (1ms)CFS 最小调度粒度(一个进程最少运行时间)
sched_wakeup_granularity_ns/proc/sys/kernel/sched_wakeup_granularity_ns15000000 (15ms)唤醒抢占粒度(被唤醒进程需要领先多少才抢占)
sched_migration_cost/proc/sys/kernel/sched_migration_cost_ns500000 (0.5ms)判断进程是否 cache-hot 的时间阈值
sched_nr_migrate/proc/sys/kernel/sched_nr_migrate32每次均衡最多迁移的任务数
sched_cfs_bandwidth_slice_us/proc/sys/kernel/5000 (5ms)CFS bandwidth 过期后默认切片
sched_tunable_scaling/proc/sys/kernel/logarithmic参数缩放模式
# 查看当前值
sysctl -a | grep sched_

# 调整最小调度粒度(减小以降低延迟,增大以提高吞吐)
sysctl -w kernel.sched_min_granularity_ns=2000000    # 2ms */

# 调整唤醒抢占粒度(增大减少不必要抢占)
sysctl -w kernel.sched_wakeup_granularity_ns=20000000 # 20ms */

# 降低迁移成本阈值,让负载均衡更积极
sysctl -w kernel.sched_migration_cost_ns=200000       # 0.2ms */
权衡说明:
  • min_granularity 越小 → 响应延迟越低,但切换开销越大
  • wakeup_granularity 越大 → 越不容易被抢占,吞吐越好,响应越差
  • migration_cost 越大 → 越不倾向迁移(有利于 cache 复用)

6.2 CPU 亲和性与 NUMA 策略

/* 设置 CPU 亲和性 */
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);   /* CPU 0 */
CPU_SET(2, &cpuset);   /* CPU 2 */
sched_setaffinity(pid, sizeof(cpuset), &cpuset);
# 使用 taskset 工具 */
taskset -cp 0,2 $PID           # 将 PID 绑定到 CPU 0 和 2 */
taskset -c 0 ./my_program       # 启动时绑定到 CPU 0 */

# 使用 numactl 控制 NUMA 策略 */
numactl --cpunodebind=0 --membind=0 ./app  # 绑定到 node0 */
numactl --interleave=all ./db_app            # 交错分布 */
numactl --preferred=1 ./ml_training          # 优先使用 node1 */

# 查看 NUMA 拓扑 */
numactl --hardware
lscpu | grep NUMA

6.3 实时线程与 CFS 共存

/* POSIX 实时调度设置 */
struct sched_param param;
param.sched_priority = 50;   /* RT 优先级 1-99 */

// SCHED_FIFO: 先进先出,运行到阻塞或主动让出 */
sched_setscheduler(pid, SCHED_FIFO, &param);

// SCHED_RR: 时间片轮转(受 sched_rr_get_interval 约束)*/
sched_setscheduler(pid, SCHED_RR, &param);
# chrt: 实时调度工具 */
chrt -p $PID                    # 查看调度策略 */
chrt -f -p 50 $PID              # 设为 SCHED_FIFO 优先级 50 */
chrt -r -p 80 -b /path/to/bin   # 以 SCHED_RR 启动 */

# schedtool: 更强大的调度设置 */
schedtool -F -p 50 -e ./app     # FIFO + 执行 */
schedtool -D -e ./batch_script  # SCHED_IDLE(最小影响)*/

# 查看 rt 时间片信息 */
chrt -m  # 查看支持的最小/大间隔 */
cat /proc/sys/kernel/sched_rt_runtime_us  # RT 任务总 CPU 时间限制 */
cat /proc/sys/kernel/sched_rt_period_us     # RT 周期 (默认 1s) */
重要:为避免实时任务独占 CPU 导致系统死锁,内核默认将 RT 任务限制在每 1 秒周期内最多运行 950ms(sched_rt_runtime_us=950000)。在高实时性要求的嵌入式系统中可能需要调整为 -1(unlimited),但要确保有 watchdog 机制兜底。

6.4 sched_features 调优开关

/proc/sys/kernel/sched_features 中的特性开关:

# 查看当前特性 */
cat /proc/sys/kernel/sched_features

# 常用特性:
# GENTLE_FAIR_SLEEPERS       - 对睡眠进程更友好(减少唤醒后对新进程的不公)
# NEXT_BUDDY                 - 刚唤醒的进程优先作为 "next" 运行
# LAST_BUDDY                 - 被切换下来的进程优先作为 "next" 运行
# CACHE_HOT_BUDDY            - 优先使用 cache-hot 的进程
# WAKEUP_PREEMPTION          - 唤醒时立即抢占(低延迟模式)
# HRTICK                     - 高精度定时器 tick
# DOUBLE_TICK                - 双倍 tick(兼容)
# NONTASK_CAPACITY           - 非任务容量感知
# TTWU_QUEUE                 - 唤醒队列到目标 CPU
# SIS_PROP                   - 搜索空闲 CPU 时考虑 cache 亲和性
# SIS_UTIL                   - 基于 CPU 利用率选择
# WARN_DOUBLE_CLOCK          - 双时钟警告
# RT_RUNTIME_SHARE           - RT 运行时间共享
# LB_MIN                     - 负载均衡最小任务数
# ATTACH_AGE_LOAD            - 迁移时传递负载年龄
# WA_IDLE_AT_NOHZ            - NOHZ 空闲唤醒
# NO_LATENCY_ACCOUNTING      - 不计入延迟统计
# NO_NEXT_BUDDY              - 不优先 next
# NO_LAST_BUDDY              - 不优先 last
# NO_CACHE_HOT_BUDDY         - 不优先 cache-hot
# NO_HRTICK                  - 关闭 HRTICK
# NO_DOUBLE_TICK             - 关闭 DOUBLE_TICK
# NO_NONTASK_CAPACITY        - 忽略 NONTASK_CAPACITY
# NO_TTWU_QUEUE_GLOBAL       - 关闭 TTWU 全局队列
# NO_SIS_AVG_CPU             - 不基于平均利用率搜索
# NO_SIS_PROP                - 基于 cache 亲和性搜索
# NO_SIS_UTIL                - 基于利用率搜索
# NO_UCLAMP_MAX              - 不限制 uclamp max
# NO_RT_PUSH_IPI             - 不通过 IPI 推送 RT
# NO_RT_RUNTIME_SHARE        - 不共享 RT 运行
# NO_LB_MIN                  - 不限制最小均衡
# NO_ATTACH_AGE_LOAD         - 不传递负载年龄
# NO_WA_IDLE_AT_NOHZ         - 不 NOHZ 唤醒
# NO_LATENCY_ACCOUNTING_WARN - 不警告延迟统计
*/

# 通过 echo 关闭某些特性(加 NO_ 前缀) */
# 注意:需 echo 到位,实际使用较复杂,建议查看内核文档 */

6.5 实际案例:数据库工作负载调优

以 MySQL/PostgreSQL 为代表的高性能数据库系统,对调度器的要求是低延迟和高吞吐的平衡:

#!/bin/bash
# 数据库服务器 CFS 调优脚本 (适用于 OLTP 工作负载) */

# 1. 降低最小调度粒度以获得更好的交互响应 */
sysctl -w kernel.sched_min_granularity_ns=1000000  # 1ms */

# 2. 适度的唤醒抢占(避免频繁抢占导致锁竞争) */
sysctl -w kernel.sched_wakeup_granularity_ns=12000000  # 12ms */

# 3. 提高迁移成本阈值,减少数据跨 NUMA 节点 */
sysctl -w kernel.sched_migration_cost_ns=1000000   # 1ms */

# 4. NUMA 策略:数据库进程绑定到本地内存节点 */
NUMA_NODE=0
DB_PID=$(pidof mysqld | awk '{print $1}')
numactl --cpunodebind=$NUMA_NODE --membind=$NUMA_NODE \
    -p $DB_PID 2>/dev/null || echo "Binding at process start preferred"

# 5. 专用 CPU set for DB foreground threads */
# 假设 CPU 0-3 给 DB,其余给操作系统和其他服务 */
systemctl set-property --runtime -- user.slice CPUQuota="400%"

# 6. 禁用 RT runtime share 以确保 OS 不被完全抢占 */
sysctl -w kernel.sched_rt_runtime_us=950000          # 默认值,谨慎修改 */

# 7. 大数据库系统应调整:允许更激进的进程迁移 */
sysctl -w kernel.sched_nr_migrate=64                  # 增加批量均衡 */

# 8. irqbalance 确保中断不集中在 DB 使用的 CPU 上 */
systemctl enable irqbalance

第七章:观测与调试

7.1 /proc/{pid}/sched 深度解读

# 查看进程 1234 的调度信息 */
cat //proc/1234/sched | head -30

/*
 * 关键输出字段解读:
 * ------------------------------------------------------------------
 * mysqld (1234, #threads: 45)
 * ------------------------------------------------------------------
 * exec_start                               :    19283471523.456789  # 本次调度开始时间 (s.μs)
 * vruntime                                 :         12345.678901   # 虚拟运行时间 (ns)
 * sum_exec_runtime                         :     9876543210.123456   # 累计实际运行时间 (ns)
 * nr_migrations                            :                 234     # CPU 迁移次数
 * nr_voluntary_switches                    :               56789     # 自愿切换 (等待I/O等)
 * nr_involuntary_switches                  :               12345     # 非自愿切换 (时间片到期)
 * se.load.weight                           :                1024    # 当前权重 (nice 0 = 1024)
 * se.runnable_load_sum                     :           987654321     # 负载跟踪累加器
 * se.runnable_load_avg                     :                1024    # 平均可运行负载
 * se.avg.last_update_time                  :          1234567890    # 上次更新 (ns)
 * se.avg.runnable_sum                      :           123456789     # PELT 累加器
 * se.avg.runnable_avg                      :                 512     # 平均运行份额
 * se.avg.avg_period                        :               47360    # 平均周期 (48ms对应1024)
 * se.avg.decay_count                       :              567890    # 衰减计数
 * policy                                  :                    0    # SCHED_NORMAL (CFS)
 * prio                                    :                  120    # 静态优先级 (= nice 0)
 * clock-delta                               :                  42    # 时钟偏差
 * */

通过 nr_voluntary_switches 和 nr_involuntary_switches 的比例可以判断进程的调度行为:

  • voluntary 很高:进程主动放弃 CPU(等待 I/O、锁、条件变量),属于 I/O 密集型
  • involuntary 很高:进程被强制切换(时间片到期),属于 CPU 密集型或调度参数需调整

7.2 perf sched 工具

# 1. 录制调度事件(采样调度器决策) */
perf sched record -a sleep 10
# 生成 perf.data.sched 文件 */

# 2. 查看调度时间线(哪些任务在什么时候运行) */
perf sched timeline
/*
 *  TIMESTAMP      CPU     TASK NAME                       DURATION (ms)
 *  12345.6789      0      mysqld:[1234]                   10.5         ← 进程运行时长
 *  12345.6894      0      swapper/0                        0.1          ← 空行切换
 *  12345.6895      0      nginx:[5678]                     8.2          ← 长运行
 *  ...
 */

# 3. 统计每个任务的调度延迟(从唤醒到实际运行的等待时间) */
perf sched latency --sort max
/*
 *  TASK                  |  Runtime (ms) |  Switches |  Average delay (ms) |  Maximum delay (ms)
 *  mysqld:1234           |     8567.3    |     2345  |        0.5           |       15.2  ← 最大延迟!
 *  redis-server:5678     |     6123.4    |     1890  |        0.3           |        8.7
 *  ...
 */

# 4. 查看 CPU 时间分布(哪个任务占用了哪些 CPU 的时间) */
perf sched map
/*
 *  CPU    0    1    2    3
 *  t1     +    -    -    -
 *  t2     -    A    -    -
 *  t3     -    -    B    -
 *  t4     -    -    -    +
 *  -      C    C    C    C
 *  A/B/C 表示时间线,+ 是当前运行任务,- 是被抢占
 */

# 5. 重建调度序列(检测反复睡眠被唤醒但没有进展的死锁) */
perf sched replay -v

7.3 ftrace 调度事件跟踪

# 启用调度跟踪 */
cd /sys/kernel/tracing    # 或 /sys/kernel/debug/tracing */

# 1. sched_switch:跟踪进程切换事件 */
echo 0 > tracing_on
echo sched_switch > set_event
echo 1 > tracing_on
# 等待 N 秒后... */
echo 0 > tracing_on
cat trace

/*
 * 输出示例:
 *             mysqld-1234    [001] d.h. 12345.6789: sched_switch: \
 *               mysqld:1234 [120] S ==> nginx:5678 [120] R \
 *               target cpu: 001  comm=mysqld pid=1234 prio=120 state=S ==> comm=nginx pid=5678 prio=120 state=R
 */

# 2. sched_wakeup / sched_wakeup_new:跟踪进程唤醒 */
echo sched_wakeup sched_wakeup_new > set_event
echo 1 > tracing_on

# 3. 使用 trace-cmd 更方便 */
trace-cmd record -e sched_switch -e sched_wakeup -p function_graph sleep 10
trace-cmd report | less

7.4 eBPF/BCC 脚本分析调度延迟

# 安装 BCC-tools */
apt install bpfcc-tools    # 或 yum install bcc-tools */

# 1. runqlat:测量任务在 runqueue 中等待 CPU 的延迟分布 */
/usr/share/bcc/tools/runqlat 1 3
/*
 * usecs               : count     distribution
 * 0 -> 1          : 5        |****                                    |
 * 2 -> 3          : 8        |*******                                 |
 * 4 -> 7          : 15       |***************                         |
 * 8 -> 15         : 45       |******************************************|
 * 16 -> 31         : 28       |****************************              |
 * 32 -> 63         : 12        |************                            |
 * 64 -> 127        : 3         |***                                     |
 * 128 -> 255       : 1         |*                                       |
 * 256 -> 511       : 0        |                                        |
 * 512 -> 1023      : 0        |                                        |
 * 1024 -> 2047     : 1        |*        ← 极端延迟:1ms+!
 */

# 2. runqlen:测量队列长度(并行度) */
/usr/share/bcc/tools/runqlen -C 1 3
/*
 * cpu = 0
 *               : count     distribution
 *     0         : 92        |***********************************************|
 *     1         : 25        |***********                                  |
 *     2         : 10        |****                                         |
 *     3         : 3         |*                                             |
 *     5         : 1         |                                              |
 */

# 3. offcptime:统计 CPU 在 off-CPU 状态的时间(等待 I/O 等) */
/usr/share/bcc/tools/offcptime -p $PID 5

# 4. wakeuptime:分析唤醒延迟(从 sched_wakeup 到实际调度) */
/usr/share/bcc/tools/wakeuptime 1 5

7.5 实际排查:响应时间抖动分析

场景:某 API 服务 P99 延迟周期性从 10ms 飙升到 200ms。

# 步骤1: 观测 P99 延迟是否与 runqueue 延迟相关 */
runqlat -p $(pidof api_server) 1 60 | tee runqlat.log
/* 发现: 高延迟时 runqlat 在 50-100μs 区间有峰值 */

# 步骤2: 检查 CPU 亲和性中断分布 */
perf stat -e cpu-clock -p $API_PID -a sleep 1

# 步骤3: 定位哪个进程在"偷"CPU */
perf sched record -p $API_PID sleep 5
perf sched latency --sort max | head -20
/* 发现: host-agent-backup 压缩进程间歇性占用同核 */

# 步骤4: 验证 NUMA 位置 */
cat /proc/$BACKUP_PID/status | grep Cpus_allowed_list
cat /proc/$API_PID/status | grep Cpus_allowed_list

# 修复: 将 backup 进程限制到不同 NUMA 节点 */
numactl --cpunodebind=1 --membind=1 -p $BACKUP_PID
taskset -cp 0-7 $API_PID

根本原因:日志压缩进程(nice=0)随机选择唤醒 CPU,反复抢占 API 进程所在的物理核。通过 CPU 亲和性隔离后,P99 延迟稳定在 15ms 以内。

第八章:最新发展趋势

8.1 EEVDF 调度器(Linux 6.6+)

2023 年 Linux 6.6 内核引入的 EEVDF(Earliest Eligible Virtual Deadline First)调度器是对 CFS 的突破性改进,旨在解决 CFS 在高负载和精确延迟场景下的不足:

/*
 * EEVDF 核心概念:
 * 
 * 传统 CFS: 选择 vruntime 最小的进程
 * EEVDF:   选择 deadline 最小的进程
 *
 * 每个进程有三个关键时间:
 *   - vruntime: 虚拟运行时间(已见)
 *   - vdegrade: 虚拟降解时间
 *   - deadline: vruntime + 调度延迟期望
 *
 * 相比 CFS 的优势:
 * 1. O(1) 调度决策(用当前时间戳替代红黑树搜索)
 * 2. 更平滑的延迟分布(避免 CFS 可能出现的"饥饿窗口")
 * 3. 更精确的 CPU 份额分配
 */

/* 切换到 EEVDF */
// 编译时启用 */
CONFIG_SCHED_CLASS=y
CONFIG_SCHED_CORE=y
CONFIG_SCHED_ALT=y           /* 或 */
// 启动参数 */
sched=eevdf
特性CFSEEVDF (6.6+)
调度决策复杂度O(log n)O(1) + O(log n) 维护
延迟稳定性良好(平均值),尾部不稳定优秀(严格保证截止时间)
公平逼近度渐近公平(理想延迟模型)精确公平(在每个粒度内)
红黑树依赖是(vruntime 排序)否(时间轮 + deadline 队列)
NUMA 均衡逐步完善更精确(每实体 deadline 同步)

8.2 sched_ext:BPF 可扩展调度器框架

Linux 6.12 引入的 sched_ext(Scheduler Extender)框架允许用户空间通过 BPF 程序完全替换调度器策略:

/*
 * sched_ext 架构:
 * 
 * 用户空间 BPF 程序提供:
 *   - pick_next_task(): 自定义选择下一个进程的逻辑
 *   - enqueue_task(): 入队通知
 *   - dequeue_task(): 出队通知
 *   - dispatch(): 决定如何分发任务
 * 
 * 内核提供:
 *   - CPU 共享的顶级调度逻辑
 *   - 安全边界和超时机制
 *   - BPF map 与 helpers
 */

/* 编译一个自定义 scheduler */
// 1. 编写 BPF 程序 (my_sched_ext.bpf.c) */
#include <scx/common.bpf.h>

SEC("")
int select_cpu(struct task_struct *p, int prev_cpu, u64 wake_flags)
{
    /* 自定义 CPU 选择逻辑 */
    return bscctx_select_cpu_from_idle(p, prev_cpu, available_mask);
}

SEC("")
void enqueue(struct task_struct *p, u64 enq_flags)
{
    /* 自定义入队逻辑(如优先级调整) */
}

// 2. 加载并启用 */
$ sudo scx_rustland        /* 社区实现的 Rust BPF 调度器 */
$ sudo scx_simple          /* 最简单的参考实现 */

// 查看当前使用的调度器 */
$ sysctl kernel.sched_ext_name
kernel.sched_ext_name = scx_rustland

sched_ext 的首批社区调度器:

  • scx_rustland:Rust 编写的通用调度器,综合性能优秀
  • scx_nest:针对 Nest 型突发负载设计
  • scx_layered:分层调度策略,不同层不同策略
  • scx_pair:成对SMT感知调度器

8.3 异构调度(ARM big.LITTLE / Intel Thread Director)

在异构 CPU 架构下,调度器需要感知不同核心的计算能力差异:

/*
 * CPU Capacity 模型:
 * 
 * X86 Intel (大小核):
 *   P-core: capacity_orig = 1024  (最大)
 *   E-core: capacity_orig = 640   (约 63%)
 * 
 * ARM big.LITTLE (示例):
 *   Cortex-X2: capacity_orig = 1024
 *   Cortex-A710: capacity_orig = 676
 *   Cortex-A510: capacity_orig = 320
 */

struct sched_domain {
    unsigned int cpu_capacity;       /* 该 CPU 的计算能力 */
    unsigned int cpu_capacity_orig;  /* 原始满频能力 */
};

关键技术要点:

/*
 * 1. Capacity-aware placement: 任务放置考虑 CPU 能力
 * 2. Capacity-aware load balancing: 负载均衡使用 PELT-SMT cap ratio
 * 3. Utilization Clamping (UCLAMP): 用户空间可以限制任务的最小/最大 CPU 容量
 */

/* UCLAMP API (通过 sched_setattr 或 cgroup) */
// uclamp_min: 要求任务至少在此性能段运行(防止降频)
// uclamp_max: 限制任务不超过此性能段(限制功耗)
// 
// 场景示例:
// - 视频解码线程: uclamp_min=512 (至少中等性能核) 
// - 后台日志写入: uclamp_max限制到效率核

/* Intel Thread Director 支持 */
/* 6 级硬件提示 vs Linux 使用 AClearHint 跨 CPU 迁移 */

8.4 AI/ML 训练工作负载中的调度优化

AI/ML 训练工作负载(特别在大规模分布式训练中)对调度器有独特要求:

/*
 * AI/ML 调度挑战:
 * 
 * 1. 长耗时任务 + 敏感于抢占(GPU 上下文切换代价高)
 * 2. 通信密集(NCCL/RCCL 集合通信需要精确同步)
 * 3. 内存绑定(模型状态、梯度聚合占用大量内存)
 * 4. NUMA 敏感(GPU 到 CPU 的亲和性)
 */

/* 实际优化手段 */
// 1. 使用 SCHED_IDLE 避免干扰训练任务 */

sysctl -w kernel.sched_latency_ns=24000000          # 延长调度周期 */
sysctl -w kernel.sched_min_granularity_ns=3000000   # 3ms 最小粒度 */

// 2. 使用 NUMA 绑定确保训练进程靠近 GPU */
# GPU 位于 PCIe NUMA node 0 */
numactl --cpunodebind=0 --membind=0 python train.py

// 3. CPU 隔离减少系统噪声 */
# 在 boot 参数中隔离 CPU 5-31 给训练任务 */
isolcpus=5,6,7,...,31 nohz_full=5-31 rcu_nocbs=5-31

// 4. 离线推理任务使用 cpu.idle 策略 */
chrt -i 0 ./inference_worker

对于使用 torchrun 等分布式训练框架的场景,关键调度优化参数:

# 分布式训练推荐配置 */


# 1. 关闭该 NUMA 节点的自动均衡 */

echo 0 > /proc/sys/kernel/numa_balancing

# 2. 增加迁移阈值(减少训练进程被均衡的时间) */
sysctl -w kernel.sched_migration_cost_ns=5000000

# 3. 使用 cpuset 精确隔离 GPU 附近的 CPU */
mkdir /sys/fs/cgroup/cpuset/training
echo "5-15" > /sys/fs/cgroup/cpuset/training/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/training/cpuset.mems
echo $TRAIN_PID > /sys/fs/cgroup/cpuset/training/cgroup.procs

# 4. IRQ 中断必须避开训练 CPU(由 irqbalance 处理或手动设置) */
# 手动示例: 将所有中断绑定到 CPU 0-4 */
for irq in $(ls /proc/irq/ | grep -v default); do
    echo 0f > /proc/irq/$irq/smp_affinity  # 绑定到 CPU 0-3 */
done
展望:Linux 调度器正处于从 CFS 向 EEVDF 和 sched_ext 过渡的关键阶段。CFS 作为 20 年来 Linux 内核的基石调度器设计,其"虚拟运行时间"和"红黑树排序"的思想仍然深刻影响着新一代调度器的架构。对于系统工程师而言,理解 CFS 原理不仅是解决当前问题的必备知识,更是掌握 EEVDF、sched_ext 等新兴技术的必经之路。在可预见的将来,BPF 驱动的可编程调度器将成为大规模定制场景的标准方案,而 EEVDF 则代表着通用调度器向更高精度、更低尾部延迟的演进方向。
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部