引言:为什么进程调度器是系统性能的核心

Linux 进程调度器是整个操作系统最核心的组件之一——它决定了哪个进程在何时获得 CPU 时间片,直接影响系统的吞吐量、响应延迟和资源利用率。无论是高并发 Web 服务的 P99 延迟、容器化环境的 CPU 资源隔离、还是实时系统的确定性响应,最终都要落实到调度器的行为上。

本文将从调度器的完整内核实现路径出发,系统覆盖 CFS(完全公平调度器)的红黑树与 vruntime 机制、实时调度策略(SCHED_FIFO/SCHED_RR/SCHED_DEADLINE)、cgroup v2 的 CPU 带宽控制、CPU 亲和性与 NUMA 感知、内核抢占配置,以及生产级低延迟与高密度部署的调优实践,帮助读者建立从内核源码到生产调优的全栈工程能力。

第一章:Linux 调度器架构全景

1.1 调度器演进历程

Linux 调度器经历了多个重大版本迭代:

  • O(n) 调度器(Linux 2.4 之前):遍历所有可运行进程选择最优,时间复杂度 O(n),无法支撑大规模并发
  • O(1) 调度器(Linux 2.6):引入 active/expired 双数组 + 优先级位图,schedule() 执行时间与进程数无关,但交互性评分算法过于复杂
  • CFS(Completely Fair Scheduler,Linux 2.6.23+):基于红黑树的虚拟运行时间(vruntime)排序,实现真正的"完全公平",成为通用调度策略的默认选择
  • EAS(Energy Aware Scheduling,Linux 5.0+):面向 ARM big.LITTLE 架构的能量感知调度,在能耗与性能间平衡
  • PREEMPT_RT(实时抢占补丁):将中断处理线程化、自旋锁转为可抢占互斥锁,使 Linux 达到硬实时级别延迟

1.2 调度器核心数据结构

调度器的核心数据结构关系如下:

struct task_struct {
    int prio;                          // 静态优先级 (0-139)
    int normal_prio;                   // 基于 static_prio 计算
    unsigned int rt_priority;          // 实时优先级 (1-99)
    const struct sched_class *sched_class;  // 调度类
    struct sched_entity se;            // CFS 调度实体
    struct sched_rt_entity rt;         // RT 调度实体
    struct sched_dl_entity dl;         // DEADLINE 调度实体
    const char *comm;                  // 进程名
    struct list_head tasks;            // 全局进程链表
    struct mm_struct *mm;              // 内存描述符
    ...
};

struct sched_entity {
    struct load_weight load;           // 权重
    struct rb_node run_node;           // 红黑树节点
    unsigned int on_rq;                // 是否在运行队列
    u64 vruntime;                      // 虚拟运行时间
    u64 exec_start;                    // 开始执行时间
    u64 sum_exec_runtime;              // 累计执行时间
    u64 prev_sum_exec_runtime;         // 上次累计值
    struct cfs_rq *cfs_rq;             // 所属 CFS 运行队列
    ...
};

struct rq {
    raw_spinlock_t lock;               // 运行队列锁
    unsigned int nr_running;           // 可运行进程数
    struct cfs_rq cfs;                 // CFS 运行队列
    struct rt_rq rt;                   // 实时运行队列
    struct dl_rq dl;                   // DEADLINE 运行队列
    struct task_struct *curr;          // 当前运行进程
    struct task_struct *idle;          // idle 进程
    u64 clock;                         // 运行队列时钟
    int cpu;                           // CPU ID
    ...
};

struct cfs_rq {
    struct load_weight load;           // 总权重
    unsigned long runnable_weight;
    unsigned int nr_running;           // CFS 红黑树中进程数
    u64 min_vruntime;                  // 最小 vruntime
    struct rb_root_cached tasks_timeline;  // 红黑树根
    struct sched_entity *curr;         // 当前调度实体
    ...
};

1.3 调度类链(Scheduling Class Chain)

Linux 采用调度类链表实现多策略优先级排列:

// 内核: kernel/sched/core.c
const struct sched_class stop_sched_class;       // 优先级最高(停机任务)
const struct sched_class dl_sched_class;         // SCHED_DEADLINE(最早截止时间优先)
const struct sched_class rt_sched_class;         // SCHED_FIFO / SCHED_RR(实时调度)
const struct sched_class fair_sched_class;       // SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE
const struct sched_class idle_sched_class;       // SCHED_IDLE(空闲任务)

当需要选择下一个进程时,pick_next_task() 函数按优先级从高到低遍历调度类:先检查 DEADLINE 队列,再检查 RT 队列,然后是 CFS 红黑树,最后是 idle 任务。这意味着 DEADLINE 任务的优先级永远高于 RT,RT 任务永远高于普通进程

第二章:CFS 完全公平调度器深入实现

2.1 核心概念:vruntime(虚拟运行时间)

CFS 的核心思想是让每个进程的虚拟运行时间(vruntime)增长速率与其权重成反比。权重越大的进程,vruntime 增长越慢,因此被调度的机会越多。

vruntime 的计算公式:

// 内核: kernel/sched/fair.c: update_curr()
delta_exec = now - curr->exec_start;              // 实际执行时间
delta_exec_weighted = calc_delta_fair(delta_exec, curr);  // 根据权重换算
curr->vruntime += delta_exec_weighted;            // 累加虚拟时间

calc_delta_fair() 的实现:

static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se) {
    // NICE_0_LOAD = 1024,对应 nice=0 的权重
    if (unlikely(se->load.weight != NICE_0_LOAD))
        delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
    return delta;
}

nice 值每降低 1(优先级提高),权重增加约 1.25 倍。具体映射关系由 sched_prio_to_weight[] 数组定义:

nice权重相对 CPU 份额(nice=0 为基准)
-2088761~10.0x
-101561~2.9x
-5312~1.7x
010241.0x
5330~0.57x
10110~0.32x
1536~0.17x
1915~0.09x

2.2 红黑树与进程选择

CFS 使用红黑树(Red-Black Tree)按 vruntime 组织所有可运行进程:

  • 最左节点 = vruntime 最小 = 最该被调度
  • 选择操作 O(1):通过 rb_leftmost 缓存直接获取
  • 插入操作 O(log n):标准红黑树插入
// 内核: kernel/sched/fair.c: __enqueue_entity()
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) {
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    bool leftmost = true;

    // 按 vruntime 查找插入位置
    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct sched_entity, run_node);
        if (entity_before(se, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = false;
        }
    }

    rb_link_node(&se->run_node, parent, link);
    rb_insert_color_cached(&se->run_node, &cfs_rq->tasks_timeline, leftmost);
    // 如果是新的最左节点,更新 leftmost 缓存
}

// 选择下一个运行进程 O(1)
static struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq) {
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
    if (!left) return NULL;
    return rb_entry(left, struct sched_entity, run_node);
}

2.3 调度周期与目标延迟

CFS 使用两个关键参数控制时间分配:

  • sched_latency:目标调度延迟,默认 6ms(所有可运行进程至少运行一次的时间窗口)
  • sched_min_granularity:最小时间片,默认 0.75ms,防止过多进程时的切换开销
  • sched_wakeup_granularity:唤醒抢占粒度,默认 1ms,控制新唤醒进程的抢占时机

每个进程分配的时间片计算公式:

time_slice = sched_latency / nr_running  // 如果结果 >= min_granularity
time_slice = min_granularity              // 否则取最小粒度

当可运行进程数超过 sched_latency / sched_min_granularity = 8 个时,每个进程的时间片不再缩小,但调度周期会拉长(min_granularity * nr_running)。

2.4 组调度(Group Scheduling)

组调度使得调度决策在用户组间也保持公平:

// 两级调度流程:用户组间 CFS → 组内 CFS → 进程选择
用户 A (shares=1024) 
  ├── 进程 A1 (shares=1024)  → vruntime 在组内红黑树排序
  └── 进程 A2 (shares=512)
用户 B (shares=1024)
  └── 进程 B1 (shares=1024)

当用户 A 有两个活跃进程而用户 B 只有一个时,用户 A 虽然有组级别的公平份额 1024,但每个进程实际获得 512/1024 的份额。这就是"组内公平、组间也公平"的双重平衡机制。

2.5 负载均衡(Load Balancing)

多核环境下,每个 CPU 有独立的运行队列。负载均衡在以下时机触发:

  1. tick 时钟中断时:检查是否需要进行周期性负载均衡(load_balance())
  2. 空闲 CPU 时:空闲 CPU 会尝试从繁忙 CPU 窃取任务(idle_balance())
  3. exec/fork 时:选择最空闲的 CPU 放置新进程
  4. 唤醒新进程时:select_task_rq_fair() 选择最佳 CPU

负载均衡的核心度量是 "负载"(load),不是简单的可运行进程数,而是包含历史衰减权重的加权平均值:

// 内核负载的指数移动平均
load_avg = load_avg * decay + instant_load * (1 - decay)
// 每 32ms 衰减一次,半衰期约 32ms

第三章:实时调度策略

3.1 SCHED_FIFO:先进先出实时策略

SCHED_FIFO 是严格优先级的非抢占式策略——高优先级进程始终运行直到主动让出 CPU:

特点:

  • 优先级范围 1-99(99 最高)
  • 同优先级进程按 FIFO 顺序运行
  • 除非主动 sched_yield()、阻塞或被更高优先级抢占,否则永远运行
  • 可能导致低优先级任务 "饿死"(starvation)
// 设置 SCHED_FIFO 的策略示例
#include 

struct sched_param param;
param.sched_priority = 50;
sched_setscheduler(pid, SCHED_FIFO, ¶m);

// 限制实时任务最大 CPU 使用时间(防止系统挂起)
// /proc/sys/kernel/sched_rt_period_us = 1000000  (1 秒周期)
// /proc/sys/kernel/sched_rt_runtime_us = 950000   (实时任务最多占 95%)

3.2 SCHED_RR:轮转实时策略

SCHED_RR 与 SCHED_FIFO 类似,但同优先级进程之间有时间片轮转:

  • 每个 RR 进程的默认时间片为 100ms(sched_rr_timeslice = RR_TIMESLICE
  • 时间片耗尽后,进程被移到同优先级队列末尾
  • 适合需要公平性的实时任务

3.3 SCHED_DEADLINE:最早截止时间优先(EDF)

SCHED_DEADLINE 是基于 EDF 算法的周期性实时任务调度策略,是 Linux 实时调度中最严格和最强大的策略:

每个任务由三个参数定义:

  • runtime(Q):每个周期内需要的最坏执行时间
  • period(T):任务的周期
  • deadline(D):截止时间(通常等于 period)
[[Q 运行 | 空闲 ] [Q 运行 | 空闲 ] [Q 运行 | 空闲 ]]
├──────── period ────────┤├──────── period ────────┤

可调度性检验(Schedulability Test):

// 必要条件:Σ(Q_i / T_i) <= 1 (对于隐式截止时间 D_i = T_i)
// 内核中的准入检查: dl_task_can_fit()
u64 util = 0;
for_each_dl_task(t) {
    util += t->runtime / t->period;
}
return util <= 1;

Deadline 任务使用独立的 dl_rq(按运行时间排序的红黑树),永远优先于 FIFO/RR/CFS 任务执行。

第四章:CPU 亲和性与 NUMA 感知

4.1 CPU 亲和性(CPU Affinity)

CPU 亲和性使进程/线程绑定到特定 CPU 核心,减少 CPU Cache 失效:

// 使用 taskset 命令绑定进程
taskset -c 0-3 ./my_app                    // 绑定到 CPU 0-3
taskset -cp 4,5                      // 设置已运行进程的亲和性

// 使用 sched_setaffinity() 系统调用
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
CPU_SET(1, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);

// 使用 cgroup cpuset.cpus(容器场景)
echo "0-3" > /sys/fs/cgroup/mygroup/cpuset.cpus
echo "0"   > /sys/fs/cgroup/mygroup/cpuset.mems  // NUMA node 0

CPU 亲和性的底层原理:

struct task_struct {
    ...
    cpumask_t cpus_allowed;  // 允许的 CPU 位图
    ...
};

// 选择 CPU 时的过滤逻辑
for_each_cpu(cpu, &p->cpus_allowed) {
    if (!cpu_active(cpu)) continue;
    if (available) return cpu;  // 选择可用的绑定 CPU
}

4.2 NUMA 架构与调度优化

NUMA(Non-Uniform Memory Access)架构下,进程访问本地节点的内存远快于远程节点。调度器通过多层级调度域(sched_domain)实现 NUMA 感知:

NUMA 层级调度域结构:
┌─────────────────────────────────────┐
│ DIE 域(Socket 级,共享 L3 Cache)    │
│  ┌─────────┐  ┌─────────┐           │
│  │ NUMA MC │  │ NUMA MC │  ← 多核域 │
│  │ CPU 0-5 │  │ CPU 6-11│           │
│  └─────────┘  └─────────┘           │
└─────────────────────────────────────┘

// 查看调度域层级
cat /proc/sys/kernel/sched_domain/cpu0/domain*/name
# OUTPUT: DIE → NUMA

// 查看 NUMA 节点拓扑
numactl --hardware
# available: 2 nodes (0, 1)
# node 0 cpus: 0 1 2 3 4 5 6 7
# node 1 cpus: 8 9 10 11 12 13 14 15
# node 0 size: 65448 MB
# node 1 size: 65536 MB
# node distances:
# node   0   1
#   0:  10  21    ← 本地 10,远程 21(2.1x 延迟)
#   1:  21  10

NUMA 感知唤醒(NUMA Aware Wakeup):

Linux 4.0+ 引入了 numa_fork 和 numa_wake 优化——唤醒进程时不仅考虑 CPU 负载,还考虑内存所在 NUMA 节点:

// 内核: kernel/sched/fair.c: select_idle_sibling()
static int select_idle_sibling(struct task_struct *p, int prev, int target) {
    // 1. 首先尝试 prev CPU(缓存亲和)
    if (available_idle_cpu(prev))
        return prev;

    // 2. 尝试同 NUMA 节点内 waker CPU
    if (cpumask_test_cpu(target, p->cpus_allowed) &&
        available_idle_cpu(target))
        return target;

    // 3. 在同 NUMA 域内寻找空闲 CPU
    sd = rcu_dereferencesched_domain(target);
    for_each_domain(target, sd) {
        // 在该调度域内寻找空闲 CPU
    }
}

第五章:cgroup v2 资源控制

5.1 cgroup v2 CPU 控制

cgroup v2 提供了精细的 CPU 资源隔离机制:

// 创建 cgroup
mkdir /sys/fs/cgroup/myapp

// 设置 CPU 带宽(基于 CFS Bandwidth Control)
echo "200000 1000000" > /sys/fs/cgroup/myapp/cpu.max
// 表示每 1s 周期内最多使用 200ms CPU → 等效 2 个核心的 20%
// 超出后进程会被节流(throttled),直到下一个周期

// 设置 CPU 权重(相对于其他 cgroup)
echo "100" > /sys/fs/cgroup/myapp/cpu.weight
// 权重区间 1-10000,默认 100
// cgroup A weight=100 vs cgroup B weight=200 → B 获得 2x CPU 份额

// 查看 cpu.stat 了解节流情况
cat /sys/fs/cgroup/myapp/cpu.stat
// nr_periods 1234    (周期数)
// nr_throttled 56    (被节流次数)
// throttled_time 123456789(纳秒)
// usage_usec 987654321

5.2 cgroup v2 cpuset 控制

cpuset 子集提供物理 CPU 和内存节点绑定:

// 限制 cgroup 只能使用 CPU 4-7 和 NUMA node 1
echo "4-7" > /sys/fs/cgroup/myapp/cpuset.cpus
echo "1"   > /sys/fs/cgroup/myapp/cpuset.mems
echo "1"   > /sys/fs/cgroup/myapp/cpuset.cpus.partition  // 独占分区模式

// 查看当前绑定情况
cat /sys/fs/cgroup/myapp/cpuset.cpus.effective

5.3 cgroup v2 与 Kubernetes 映射

Kubernetes 的 cpu request/limits 直接映射到 cgroup 参数:

// Linux 内核 CFS 带宽触发逻辑
// cpu request 1000m → cpu.weight = 3908(近似值,按比例换算)
// cpu limit 2000m → cpu.max = "200000 1000000"(200ms/1s 周期)

// 查看 Pod 实际的 cgroup 配置
cat /sys/fs/cgroup/kubepods/pod/cpu.weight
cat /sys/fs/cgroup/kubepods/pod/cpu.max

第六章:内核抢占模型与实时性

6.1 内核抢占模式

Linux 支持多种内核抢占配置,在系统启动时选择:

模式配置说明
No Forced PreemptPREEMPT_NONE服务器默认,仅在明确抢占点让出,最高吞吐
Voluntary PreemptPREEMPT_VOLUNTARY在主要抢占点自动让出,平衡吞吐与延迟
Preemptible KernelPREEMPT桌面/通用,内核几乎处处可抢占,较好响应
Full RT PreemptPREEMPT_RT实时扩展,中断线程化,自旋锁→互斥锁

抢占相关的内核参数:

// 检查当前抢占模式
uname -a   # 查看是否有 PREEMPT 字样
zgrep CONFIG_PREEMPT /proc/config.gz

// 抢占计数器(task_struct->preempt_count)
#define PREEMPT_BITS    8   // 禁用抢占深度(preempt_disable 嵌套)
#define SOFTIRQ_BITS    8   // 是否在 softirq 上下文
#define HARDIRQ_BITS    4   // 是否在 hardirq 上下文
#define NMI_BITS        1   // 是否在 NMI 上下文

6.2 上下文切换优化

上下文切换涉及的关键步骤:

// 内核切换流程: __switch_to()
1. 保存当前进程寄存器状态(callee-saved + 栈指针)
2. 切换 page table (CR3 寄存器)→ 触发 TLB flush
3. 恢复目标进程寄存器状态
4. 切换内核栈指针(rsp)
5. 返回目标进程的执行点

减少 TLB flush 的优化手段:

  • KPTI(PTI)隔离:虽然带来 TLB flush 开销,但防止 Meltdown 漏洞
  • Lazy TLB mode:进程切换时不立即 flush,推迟到首次用户态访问
  • ASID(Address Space ID):ARM/x86 PCID 支持,无需每次 flush 整个 TLB
  • 大页(Huge Page):2MB/1GB 页减少 TLB miss 率

第七章:生产级调优实战

7.1 低延迟场景调优

金融交易、音视频处理等对延迟敏感的系统:

# 1. 禁用 C-states 深度休眠
echo 0 > /sys/devices/system/cpu/cpu*/cpuidle/state{C3,C6,C7}/disable

# 2. 限制 C1E(增强 Halt 状态)
# 在 BIOS 中禁用 C-states 或添加内核参数: processor.max_cstate=1 idle=poll

# 3. CPU 频率锁定为 performance 模式
echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

# 4. 隔离 CPU 核心(启动参数)
# GRUB: isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7
# 让 CPU 4-7 不参与常规调度,进程需手动绑定

# 5. 中断亲和性绑定——网卡中断到非隔离核心
echo 3 > /proc/irq/IRQ_NUMBER/smp_affinity  # CPU 0-1
# 业务进程绑到 CPU 4-7,完全不处理中断

# 6. 禁用透明大页(某些 THP 策略会引入不确定的延迟)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

# 7. 使用 PREEMPT_RT 内核
# 实时内核将中断处理、自旋锁等全部线程化,典型延迟 < 50>

7.2 高密度容器场景调优

云原生环境下最大化 CPU 利用率:

# 1. 调整调度周期(默认 6ms 偏保守,适当缩短提升响应)
sysctl -w kernel.sched_latency_ns=4000000        # 4ms
sysctl -w kernel.sched_min_granularity_ns=500000  # 0.5ms
sysctl -w kernel.sched_wakeup_granularity_ns=800000  # 0.8ms

# 2. NUMA 感知调度
sysctl -w kernel.numa_balancing=1   # 自动 NUMA 任务迁移

# 3. 负载均衡优化
sysctl -w kernel.sched_migration_cost_ns=500000  # 500us 内不迁移
sysctl -w kernel.sched_nr_migrate=32              # 每次最多迁移 32 个任务

# 4. cgroup v2 CPU burst 允许临时超额
echo "150000 1000000" > cpu.max    # 允许突发到 150%,但总量受限

# 5. CPU 超线程感知调度(SMT 级调度域)
# 同物理核心的逻辑核共享 L1/L2 Cache
# 如果任务 Cache-sensitive,尽量跨物理核心调度避免 HT 争抢

7.3 批处理与离线任务优化

大数据分析、CI/CD 构建等可以容忍较高延迟但追求吞吐的场景:

# 1. 使用 SCHED_BATCH 策略减少唤醒抢占
schedtool -B -e ./batch_job    # 启动时设置
chrt -b 0                # 动态修改策略

# 2. SCHED_IDLE 策略——万不得已才运行
chrt -i 0    # 优先级最低,只有系统空闲时才获得 CPU

# 3. 利用 CPU bandwidth 控制离线任务的带宽上限
echo "100000 1000000" > /sys/fs/cgroup/offline/cpu.max  # 限制到 10%

# 4. 禁用自动均衡,手动绑定到 NUMA node
taskset -c 0-15 -p    # 绑定到指定 NUMA 节点

# 5. 大页支持减少 TLB miss(TLB 是批处理的重要瓶颈)
echo 1024 > /proc/sys/vm/nr_hugepages
# 或挂载 hugetlbfs
mount -t hugetlbfs none /dev/hugepages

第八章:调度器监控与可观测性

8.1 内核调度统计

/proc/sched_debug          # 每个 CPU 的运行队列详细状态
/proc/schedstat            # 调度器统计(负载均衡次数、上下文切换等)
/proc//sched          # 单进程调度统计

// 典型 sched_debug 输出
cfs_rq[0]:/
  .exec_clock          : 12345.678
  .MIN_vruntime        : 0.000
  .min_vruntime        : 9876543.210
  .nr_running          : 5
  .load                : 5120
  .runnable_load       : 5120
  .util                : 4800

// /proc//sched 关键字段
nr_switches            # 总上下文切换次数
nr_voluntary_switches  # 主动切换(等待 IO 等)
nr_involuntary_switches # 被动切换(时间片耗尽)
se.vruntime            # 当前 vruntime
se.sum_exec_runtime    # 累计执行时间
policy                 # 调度策略
prio / normal_prio     # 优先级

8.2 perf sched 分析工具

// 录制调度事件(持续 10 秒)
perf sched record -a sleep 10

// 查看调度延迟分布
perf sched latency
# 输出: 每个任务的平均/最大# 生成调度时间线(适合分析调度抖动)
perf sched map

// 查看调度器统计汇总
perf sched stat
# 输出:
#            Task                  |   Runtime ms  | Switches | Average delay ms | Max delay ms
# ---------------------------------------------------------------------------------------
#     myapp (1234)                 |     987.654   |   5234   |       0.12      |    45.67
#     nginx (5678)                 |     456.789   |   3456   |       0.09      |    12.34

8.3 eBPF/Bpftrace 调度器追踪

// 追踪所有上下文切换事件
sudo perf trace -e sched:sched_switch

// bpftrace 脚本:统计每个进程的上下文切换次数
sudo bpftrace -e 'tracepoint:sched:sched_switch {
    @comm[t->comm] = count();
}'

// 追踪调度延迟(任务就绪到实际运行的时间)
sudo bpftrace -e '
tracepoint:sched:sched_wakeup,
tracepoint:sched:sched_wakeup_new {
    @qtime[args->pid] = nsecs;
    @state[args->pid] = args->prio;
}
tracepoint:sched:sched_switch /@qtime[args->next_pid]/ {
    $delay = nsecs - @qtime[args->next_pid];
    @delay_us = hist($delay / 1000);  // 微秒级延迟统计
    delete(@qtime[args->next_pid]);
}
'

// 追踪实时调度抢占延迟(RT latency histogram)
sudo bpftrace -e '
kprobe:__schedule {
    if (prev != 0 && prev->policy != SCHED_NORMAL) {
        $now = nsecs;
        $delta = $now - prev->last_queued_ts;
        @rt_latency_us = hist($delta / 1000);
    }
}'

8.4 ftrace 调度器深度追踪

// 设置 ftrace 追踪器
cd /sys/kernel/debug/tracing
echo nop > current_tracer
echo 1 > events/sched/enable         # 启用所有调度事件追踪
echo 1 > tracing_on

# 查看特定 CPU 的运行队列状态(sched_update_nr_running)
cat trace_pipe | grep "sched_switch"

# 主要追踪事件:
# sched_wakeup / sched_wakeup_new  — 新任务被唤醒
# sched_switch                      — 进程上下文切换
# sched_migrate_task                — 负载均衡迁移
# sched_process_exit                — 进程退出
# sched_process_wait                — 进程进入等待
# sched_wait_task                   — 任务等待 CPU
# sched_stat_runtime                — 任务运行时间统计

第九章:典型生产场景案例分析

案例 1:高并发 Nginx 调度优化

场景:8 核物理机 + 16 逻辑核(HT),64 并发 Nginx worker,混合静态资源和反向代理。

// 问题:RSS 波动时部分 worker 出现长尾请求(P99 延迟飙升)
// 根因分析:Nginx worker 在 CPU 间频繁迁移,L1/L2 Cache 失效

// 解决方案:
// 1. 将 Nginx worker 绑定到物理核心
worker_cpu_affinity 0001 0010 0100 1000 0001 0010 0100 1000;

// 2. 网卡中断绑定到独立核心
echo 0f > /proc/irq//smp_affinity  # CPU 0-3 处理中断
worker_cpu_affinity f0 f0 f0 f0 f0 f0 f0 f0;           # CPU 4-7 运行 worker

// 3. 对比优化前后
// 优化前:P99 延迟 15ms,Cache-miss 23%
// 优化后:P99 延迟 3ms, Cache-miss 5%

案例 2:K8s HPA 触发延迟优化

场景:K8s 集群 Deployment 配置 CPU request=500m limit=1000m,HPA 阈值 70%。

问题:Rapid traffic spikes 时扩容触发延迟 30s+,导致上游超时。

// 根因:cgroup CPU 限流导致 probe / 启动脚本被 throttle
// container cpu.weight = 3908(request=500m),启动时 burst 能力受限

// 优化方案:
// 1. 适当提高 request 权重,确保启动 burst
resources:
  requests:
    cpu: "500m"
  limits:
    cpu: ""
// 不设置 limit 则可利用 node 上空闲 CPU burst

// 2. 容器启动阶段使用 CRIO cpu burst
# 在 kubelet 配置中启用: --cpu-manager-policy=static
# 为 Guaranteed Pod 分配独占 CPU(cpuset 绑定)

// 3. 在容器内部 pin 关键启动进程
taskset -c 0 /start-app.sh   # 绑定到独占 CPU 加速启动

案例 3:实时音视频确定性延迟

场景:XMPP 媒体网关,音频 RTP 处理线程需要 < 1ms>

// 使用 SCHED_DEADLINE 策略
struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime = 800000,     // 800μs 最坏执行时间
    .sched_deadline = 1000000,   // 1ms 截止时间
    .sched_period = 1000000,     // 1ms 周期
};
sched_setattr(pid, &attr, 0);

// 系统侧隔离 CPU
// 启动参数 isolcpus=3 nohz_full=3 rcu_nocbs=3
// 中断亲和性:echo 7 > /proc/irq/*/smp_affinity(绑定 0-2)
// CPU governor: performance

// 配合 PREEMPT_RT 内核,实测结果:
// 非实时内核:P99 调度延迟 ~800μs,抖动 ±500μs
// PREEMPT_RT:P99 延迟 < 50>

第十章:内核源码关键路径速查

// 调度器入口
kernel/sched/core.c
   ├── schedule()          // 主调度函数(中断返回/系统调用返回时调用)
   ├── __schedule()        // 真正的调度逻辑
   │   ├── pick_next_task()  // 按调度类优先级选择下一个任务
   │   ├── context_switch()  // 执行上下文切换
   │   │   ├── switch_mm()   // 切换地址空间
   │   │   └── __switch_to() // 切换寄存器状态
   │   └── update_rq_clock()  // 更新运行队列时钟
   └── scheduler_tick()    // tick 中断触发的调度更新

// CFS 调度器
kernel/sched/fair.c
   ├── enqueue_task_fair()    // 入队到 CFS 红黑树
   ├── dequeue_task_fair()    // 从红黑树出队
   ├── pick_next_task_fair()  // 从红黑树最左节点选任务
   ├── update_curr()          // 更新 vruntime(每个 tick)
   ├── task_tick_fair()       // tick 回调(检查时间片 + 抢占)
   ├── check_preempt_wakeup() // 唤醒时的抢占检查
   ├── select_task_rq_fair()  // 多核负载选择
   └── can_migrate_task()     // 负载均衡迁移判断

// 实时调度
kernel/sched/rt.c
   ├── enqueue_task_rt()      // RT 入队
   ├── dequeue_task_rt()
   ├── pick_next_task_rt()    // 按优先级位图选择 RT 任务
   └── task_tick_rt()

// DEADLINE 调度
kernel/sched/dl.c
   ├── enqueue_task_dl()
   ├── pick_next_task_dl()

// 负载均衡
kernel/sched/fair.c
   ├── load_balance()         // 核心负载均衡函数
   ├── detach_tasks()         // 从忙 CPU 拆下任务
   ├── attach_tasks()         // 附加到空闲 CPU
   └── can_migrate_task()     // 可迁移性检查(Cache hot/affinity)

// 上下文切换
arch/x86/kernel/process_64.c
   └── __switch_to()          // x86 专用汇编切换路径

总结

Linux 进程调度器是一个精心设计的多层系统,理解它的内核行为对系统性能至关重要:

  • CFS 通过红黑树和 vruntime 实现 "完全公平",是日常工作的基石
  • 实时策略(FIFO/RR/DEADLINE)为确定性延迟任务提供严格保障
  • CPU 亲和性NUMA 感知 减少了 Cache 失效和跨节点访存开销
  • cgroup v2 提供了云原生环境下精细的资源隔离与带宽控制
  • PREEMPT_RT隔离 CPU 是低延迟场景的终极武器

掌握这些机制后,无论是构建百万级并发的 Web 服务、设计低延迟的交易系统,还是优化 Kubernetes 集群的 CPU 利用率,都能做到有的放矢,从内核源码级别理解并解决性能问题。调度器不是一成不变的——从 CFS 到 EAS,从 cgroup v1 到 v2,从标准内核到 PREEMPT_RT,持续演进的设计始终在吞吐、延迟和能效之间寻找最佳平衡。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论