Linux 内核 CPU 热插拔深度实战:状态机、调度器交互与异构计算核绑定工程

作者:ybb | 日期:2026-10-06
标签:Linux Kernel, CPU Hotplug, CPUIdle, CPUFreq, Scheduling, Heterogeneous Computing
关键词:热插拔状态机、调度域拓扑、RCU/热插拔同步、CPU 遮蔽、SMP 启动路径


一、引言:为什么 CPU 热插拔比你想象的更复杂

在现代数据中心,CPU 热插拔(CPU Hotplug)已不再是"笔记本拔电池"时代的稀罕功能。它被广泛用于:

  • 虚拟化场景:vCPU 热添加/移除(QEMU/KVM live resize)
  • 异构计算:大小核架构下,小核集群在空闲时断电节能
  • 高可用维护:固件升级或硬件故障时,在线摘除物理 CPU
  • 机密计算:TDX/SEV-SNP 场景下,将 CPU 从通用调度域移出并交给安全监视器

多数开发者对 CPU 热插拔的认知停留在 echo 0 > /sys/devices/system/cpu/cpuN/online。实际上,一次成功的 CPU 下线(CPU-down)路径要跨越十几个内核子系统:调度器、RCU、定时器迁移、中断亲和性重排、cpuset 更新、内存 NUMA 平衡……任何一个环节卡死,都可能引发系统抖动甚至死锁。

本文深入剖析 Linux 内核 CPU 热插拔的完整状态机模型,揭示调度器与热插拔的深度交互机制,并给出生产环境的核绑定工程实践。


二、CPU 热插拔状态机架构

2.1 核心状态定义

内核使用 cpuhp_cpu_state 枚举管理每个 CPU 的热插拔生命周期:

// include/linux/cpuhotplug.h
enum cpuhp_cpu_state {
    CPUHP_OFFLINE = 0,
    CPUHP_PREPARE,          // CPU 即将下线,各子系统开始准备
    CPUHP_AP_ONLINE,        // 目标 CPU 已在运行(启动阶段)
    CPUHP_AP_ONLINE_DYN,    // 动态分配的上线态
    CPUHP_TEARDOWN,         // CPU 正在下线清理
    // ... 更多静态状态由编译时生成
};

状态转移通过 cpuhp_invoke_callback() 调用每个注册的回调函数,支持两种方向:

  • Startup 路径:PREPARE → AP_ONLINE → ... → CPU online
  • Teardown 路径:AP_ONLINE → TEARDOWN → ... → CPU offline

2.2 Step 模型与双向链表

热插拔不是单步操作,而是被分解为多个 Step。每个子系统在关心的 Step 注册回调:

// 注册示例:调度器在 CPU 下线前迁移任务
int cpuhp_setup_state(int cpu, const char *name,
                      int (*startup)(unsigned int cpu),
                      int (*teardown)(unsigned int cpu));

内核内部维护一个 cpuhp_step 双向链表(hp_hpfs->steps[]),每个 Step 包含:

struct cpuhp_step {
    const char          *name;
    int                 (*startup)(unsigned int cpu);
    int                 (*teardown)(unsigned int cpu);
    bool                cant_stop;      // 是否不可中止(关键状态)
    struct hlist_node   list;
};

2.3 静态状态 vs 动态状态

早期内核使用编译时静态状态(CPUHP_*),灵活性差。4.10+ 后引入 cpuhp_setup_state_nocalls() 和 AP_ONLINE_DYN,允许运行时按需分配状态,成为现代驱动的标准做法。


三、CPU 下线完整路径拆解

以 echo 0 > /sys/devices/system/cpu/cpu3/online 为例,追踪内核执行路径:

用户空间
  │
  ▼
 device_store_online()  [drivers/base/cpu.c]
  │
  ▼
 cpu_down() → _cpu_down()
  │
  ▼
 cpuhp_invoke_callback(CPUHP_TEARDOWN)
  │
  ├─► stop_machine_cpu_stop()        ← 核心:停止目标 CPU
  │     │
  │     ├─► 目标 CPU 执行 migration_cpu_stop()
  │     │     迁移所有运行中的线程到 other CPUs
  │     │
  │     ├─► 调用 teardown 回调链:
  │     │     ├─► sched_cpu_deactivate()    调度域移除
  │     │     ├─► cpuidle_sys teardown      CPUIdle 退出
  │     │     ├─► hrtimermigration()       高精度定时器迁移
  │     │     ├─► rcu_report_dead()          RCU 回调迁移
  │     │     └─► ...
  │     │
  │     └─► target CPU 进入 idle()
  │
  ▼
 CPUHP_OFFLINE 状态确认

3.1 stop_machine() 的角色

stop_machine() 是热插拔的基石:它让所有其他 CPU 停下来(进入忙等待循环),保证目标 CPU 下线过程中不会有并发访问。

// 内核实现关键路径
int stop_cpu_sync(struct stop_machine_data *data)
{
    // 1. 发送 IPI 到目标 CPU,让它进入 migration_cpu_stop()
    smp_call_function_single(cpu, migration_cpu_stop, &arg, 1);
    
    // 2. 等待目标 CPU 确认停止
    while (!task_cpu(arg->task) == stop_cpu)
        cpu_relax();
}

3.2 调度器层回调详解

调度器在热插拔中注册了关键回调:

// kernel/cpu.c 中注册
static int __init cpuhp_cb(struct notifier_block *self,
                           unsigned long action, void *hcpu)
{
    unsigned int cpu = (unsigned long)hcpu;
    switch (action) {
    case CPUHP_SCHED_DEAD:
        sched_exit_cpu(cpu);           // 清理 per-cpu 运行队列
        break;
    case CPUHP_AP_SCHED_DEAD:
        rq = cpu_rq(cpu);
        raw_spin_lock_irqsave(&rq->lock, flags);
        // 迁移剩余任务
        rq->balance_callback = &no_balance;
        raw_spin_unlock_irqrestore(&rq->lock, flags);
        break;
    }
}

四、RCU 与热插拔的生死纠缠

4.1 RCU 离线回调机制

RCU 必须处理"正在使用的 CPU 突然下线"的极端场景。内核通过 rcu_report_dead() 机制:

// kernel/rcu.h 注释(简化版)
/*
 * 当 CPU 成功下线后,调用 rcu_report_dead() 通知:
 * 1. 将 CPU 从 RCU 掩码中清除
 * 2. 处理该 CPU 积压的 RCU 回调(通过 rcu_core)
 */

4.2 RCU 热插拔的死锁风险

如果某个 RCU 临界区内尝试下线 CPU(罕见但危险),会触发:

WARNING: possible circular locking dependency detected
  CPU0 (热插拔进程)          CPU3 (目标 CPU)
  -----------------------    -----------------
  rcu_read_lock()             ...
  ↓                           prepare_to_wait()
  stop_machine()              rcu_read_lock()
  → wait for CPU3             → wait for CPU0

解决方案:热插拔路径禁止在 RCU 读端临界区内发起。内核通过 get_online_cpus() / put_online_cpus() 保护热插拔操作:

// 热插拔操作的保护
get_online_cpus();      // 增加热插拔引用计数,阻止新热插拔
// ... 执行热插拔 ...
put_online_cpus();      // 释放

五、异构计算:CPU 遮蔽与核绑定实战

5.1 isolcpus 与 nohz_full

生产环境中,常需将特定 CPU 从通用调度域中隔离,专用于实时任务或 DPDK 等高性能应用:

# GRUB 参数:隔离 CPU 2-5,启用全动态无tick
GRUB_CMDLINE_LINUX="isolcpus=2-5 nohz_full=2-5 rcu_nocbs=2-5"

5.2 cset shield(cgroup v2 cpuset)

现代替代方案:使用 cpuset.cpus 动态控制:

# 创建专用分区
mkdir /sys/fs/cgroup/partition
echo "2-5" > /sys/fs/cgroup/partition/cpuset.cpus
echo "root" > /sys/fs/cgroup/partition/cpuset.cpus.partition  # 启用分区

# 将关键任务移入
echo $PID > /sys/fs/cgroup/partition/cgroup.procs

5.3 CPU 热插拔 + 调度策略组合

在大小核架构(如 ARM big.LITTLE / Intel Hybrid)中,小核可能随时被热插除:

// 监控 CPU 在线状态变更
static int __init monitor_hp_init(void)
{
    return cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "my_monitor:online",
                             cpu_online_callback, cpu_offline_callback);
}

static int cpu_offline_callback(unsigned int cpu)
{
    pr_warn("CPU%d 下线!当前活跃 CPU: %d\n",
            cpu, cpumask_weight(cpu_online_mask));
    // 触发负载均衡策略调整
    adjust_sched_domain(cpu);
    return 0;
}

六、生产环境工程实践

6.1 CPU 下线失败的诊断

当 CPU 下线卡死时,可查询:

# 查看热插拔状态机当前阻塞位置
cat /sys/devices/system/cpu/cpuN/hotplug/state

# 打印所有注册的热插拔回调
grep -r "cpuhp_setup_state" arch/x86 kernel/ | wc -l

# 检测 D-state 等待
echo w > /proc/sysrq-trigger
dmesg | tail -50 | grep -i "hotplug\|blocked"

6.2 BPF 监控热插拔事件

// BPF 程序:跟踪 CPU 热插拔事件
SEC("tp_btp/cpuhp_enter")
int trace_cpuhp_enter(struct trace_event_raw_cpuhp_enter *ctx)
{
    u32 cpu = bpf_get_smp_processor_id();
    u32 target = ctx->cpu;
    
    bpf_printk("CPU%d: hp_enter target=%d state=%d\n",
               cpu, target, ctx->target);
    return 0;
}

6.3 NUMA 场景下的热插拔注意事项

在 NUMA 架构下关闭一个 CPU 可能触发:

  1. 该 CPU 本地 NUMA 节点的内存页面迁移到其他节点
  2. mempolicy(内存策略)需要重新平衡
  3. ksm(内核同页合并)需跳过离线 CPU
# NUMA 感知的热插拔策略
numactl --cpunodebind=0 --membind=0 echo 0 > /sys/devices/system/cpu/cpu8/online

七、内核源码关键路径速查

功能文件路径关键函数
热插拔状态机kernel/cpu.c_cpu_down()
调度器回调kernel/sched/core.csched_cpu_deactivate()
RCU 热插拔kernel/rcu/tree.crcu_report_dead()
CPUIdledrivers/cpuidle/cpuidle.ccpuidle_install_handler()
CPUFreqdrivers/cpufreq/cpufreq.ccpufreq_offline()
定时器迁移kernel/time/timer.ctimers_dead_cpu()

八、总结

CPU 热插拔是 Linux 内核最复杂的协同机制之一,涉及调度器、RCU、定时器、内存管理、中断系统等多个子系统的精密配合。本文梳理了:

  1. 状态机模型:Step-based 双向回调,支持静态/动态状态注册
  2. CPU 下线路径:stop_machine → migration_cpu_stop → teardown 回调链
  3. RCU 分布式同步:引用计数保护、离线回调、死锁规避
  4. 异构计算工程:isolcpus/cpuset/热插拔事件监控
  5. 生产诊断:状态机阻塞定位、BPF 追踪、NUMA 注意事项

掌握这些底层机制,才能在机密计算、实时调度、DPDK 等对 CPU 拓扑敏感的场景中避免踩坑。


参考文档:

  • Linux Kernel Documentation: Documentation/core-api/cpu_hotplug.rst
  • Documentation/admin-guide/cputopology.rst
  • LWN.net: "CPU hotplug in the Linux kernel" 系列
  • Kernel Newbies: cpuhp 状态机图解
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部