1. CPU Hotplug 到底在解决什么问题
CPU 热插拔(CPU Hotplug)这个机制,刚接触内核功耗子系统的人往往会觉得它离自己很远——毕竟日常开发中,CPU 就在那里,开机上电、关机断电,哪有什么"插拔"可言。但如果你做过服务器运维、虚拟化平台、或者嵌入式设备的低功耗场景,就会知道 CPU Hotplug 是一个绕不开的基础设施。它的核心能力是:在系统运行过程中,动态地把某个 CPU 核心从调度器中"摘出去"(离线),或者重新"接回来"(在线),整个过程不需要重启系统。
这件事听起来简单,做起来极其复杂。原因在于,一个 CPU 核心在运行时,它不只是执行指令那么简单。它持有调度队列、运行着定时器、可能正在处理中断、参与 RCU 同步、维护着 per-CPU 变量、缓存着各种状态。你要把它"摘出去",就必须把这些状态全部迁移或清理干净,否则轻则数据错乱,重则系统崩溃。所以 CPU Hotplug 本质上是一套状态迁移协议,而不是简单的"关掉一个核心"。
从功耗子系统的角度看,CPU Hotplug 的价值非常直接。现代 SoC 通常有多个核心,但在轻负载场景下,比如手机待机、IoT 设备空闲、服务器夜间低峰期,让所有核心都保持在线是纯粹的浪费。把空闲核心离线,可以:
- 让该核心进入更深层次的睡眠状态,甚至完全断电(配合电源域控制)
- 减少漏电流,这在先进制程下占比越来越高
- 降低散热压力,避免风扇噪音和热节流
- 为其他核心腾出共享资源(如 L2/L3 缓存、内存带宽)
但这里有个关键点很多人会忽略:CPU Hotplug 和 CPU Idle 是两套不同的机制,解决的是不同层次的问题。CPU Idle 是在核心仍然在线的前提下,让它进入浅睡或深睡,唤醒延迟从微秒到毫秒级;而 CPU Hotplug 是把核心彻底从系统视野中移除,唤醒延迟在毫秒到几十毫秒级,代价大得多,但省电效果也更彻底。选择哪一个,取决于你的延迟容忍度和省电收益的权衡。
我在实际项目中见过不少团队一上来就用 Hotplug 做省电,结果发现响应延迟飙升,用户体验变差。后来改成"轻负载用 Idle、重负载切换、极低负载才 Hotplug"的分层策略,效果才好起来。这个经验后面会展开讲。
2. 内核里 CPU Hotplug 的状态机与核心数据结构
要理解 CPU Hotplug 怎么工作,必须先搞清楚内核用什么来描述"一个 CPU 当前处于什么状态"。这套状态机是整个机制的地基,搞不明白它,后面看代码就是一团乱麻。
2.1 四种状态与状态迁移路径
内核用cpu_online_mask、cpu_present_mask、cpu_possible_mask、cpu_active_mask这四个位图来描述 CPU 的状态。它们的关系不是简单的包含,而是有明确的语义层次:
| 位图 | 含义 | 典型场景 |
|---|---|---|
| possible | 系统理论上支持的最大 CPU 集合 | 编译时 NR_CPUS 决定,启动后不变 |
| present | 当前物理上存在的 CPU | ACPI/设备树枚举后确定,热插拔物理槽位会变 |
| online | 当前对调度器可见、可调度的 CPU | 热插拔操作直接改变的就是它 |
| active | 当前正在参与调度任务的 CPU | 比 online 更严格,用于任务迁移判断 |
这四个位图的关系是:possible ⊇ present ⊇ online ⊇ active。注意online和active的区别——一个 CPU 可以 online 但暂时不 active,这通常发生在 CPU 正在上线但还没完全准备好接任务的过渡期。内核里判断"这个 CPU 能不能跑任务"用的是cpu_active(),而不是cpu_online(),这个细节在写驱动时非常关键。
状态迁移的路径大致是:
- 离线流程:active → online 清除 → 迁移任务 → 关闭中断 → 停定时器 → 通知各子系统 → present 保留(物理还在)
- 在线流程:present → 初始化 per-CPU → 启动定时器 → 打开中断 → 设置 online → 设置 active
每一步都有对应的通知链(notifier chain)回调,各子系统在这里做自己的清理和初始化。这就是为什么 CPU Hotplug 的代码看起来到处都是 hook——它必须给所有依赖 per-CPU 状态的子系统一个"我要走了/我回来了"的信号。
2.2 关键数据结构:cpuhp_step 与状态机
内核 4.10 之后引入了统一的 CPU Hotplug 状态机(kernel/cpu.c里的cpuhp_hp_states[]),把原来散落各处的 notifier 整合成了一套有序的状态步骤。每个步骤用cpuhp_step描述:
struct cpuhp_step { const char *name; union { int (*single)(unsigned int cpu); int (*multi)(unsigned int cpu, struct hlist_node *node); } startup; union { int (*single)(unsigned int cpu); int (*multi)(unsigned int cpu, struct hlist_node *node); } teardown; struct hlist_head list; bool cant_stop; bool multi_instance; };这套设计的精妙之处在于:它把"离线"和"在线"变成了同一套步骤的正反两个方向。每个子系统注册自己的 startup 和 teardown 回调,内核保证 teardown 按注册的逆序执行,startup 按正序执行。这样就不会出现"A 子系统还没清理完,B 子系统就急着初始化"的竞态。
状态编号从CPUHP_OFFLINE开始,到CPUHP_ONLINE结束,中间插入各个子系统的步骤。比如CPUHP_AP_ONLINE_DYN是给架构相关代码动态注册用的,CPUHP_BP_PREPARE_DYN是给普通驱动用的。你在写驱动时如果要注册 hotplug 回调,通常用cpuhp_setup_state()这个 API,它会自动帮你分配一个动态状态号。
提示:注册回调时一定要想清楚你的回调属于"prepare"阶段还是"online"阶段。prepare 阶段 CPU 还没真正上线,不能做可能睡眠的操作;online 阶段才相对自由。搞错了阶段,轻则警告,重则死锁。
2.3 per-CPU 数据的迁移难题
CPU Hotplug 最棘手的地方在于 per-CPU 数据。内核里大量使用DEFINE_PER_CPU定义的变量,每个 CPU 一份副本。当一个 CPU 离线时,这些数据怎么办?
答案是:大部分 per-CPU 数据不需要迁移,因为它们是"该 CPU 专属"的,CPU 离线后自然不再被访问。但有一类数据必须处理——那些被其他 CPU 引用的 per-CPU 数据。最典型的是调度器的运行队列(runqueue)。当一个 CPU 离线,它 runqueue 里的任务必须迁移到其他 CPU 上,否则这些任务就永远得不到执行。
任务迁移的过程在take_cpu_down()和sched_cpu_dying()里完成。核心逻辑是:先把该 CPU 从调度域中摘除,然后遍历它的 runqueue,把可运行任务推到其他 CPU,最后等待所有正在该 CPU 上执行的任务主动让出。这里有个细节:正在该 CPU 上运行的任务无法被强制迁移,只能等它自己调度出去。所以离线操作可能会阻塞一段时间,这也是为什么 Hotplug 的延迟不可控。
RCU 是另一个重灾区。RCU 的 grace period 需要所有 CPU 都报告一次静止状态,如果某个 CPU 离线了,它就没法报告。内核的处理方式是:离线 CPU 在离线前会通知 RCU,RCU 把它标记为"已通过",后续的 grace period 不再等它。这个机制叫rcu_cpu_offline,在CPUHP_AP_RCU_OFFLINE这个状态步骤里执行。
3. 从用户空间触发一次热插拔的完整链路
理论讲完了,来看实操。从用户空间触发 CPU 热插拔,最直接的方式是通过 sysfs 接口。这个接口简单到只有一行命令,但背后触发的链路非常长,理解这条链路对排查问题极有帮助。
3.1 sysfs 接口与触发命令
每个 CPU 在/sys/devices/system/cpu/下都有一个目录,比如cpu0、cpu1。其中online文件就是控制开关:
# 查看 CPU1 当前状态 cat /sys/devices/system/cpu/cpu1/online # 让 CPU1 离线 echo 0 > /sys/devices/system/cpu/cpu1/online # 让 CPU1 重新上线 echo 1 > /sys/devices/system/cpu/cpu1/online注意,CPU0 通常不能离线,因为它是 boot CPU,很多架构代码假设 CPU0 永远在线。你尝试离线 CPU0 会得到-EINVAL或-EPERM。这个限制在cpu_down()里有明确检查。
写online文件时,内核走的是cpu_subsys的store回调,最终调用cpu_device_down()或cpu_device_up()。这两个函数会拿cpu_add_remove_lock和cpu_hotplug_lock两把锁,然后调用核心的cpu_down()/cpu_up()。
3.2 离线流程的七个阶段
cpu_down()的执行可以拆成七个阶段,每个阶段都有明确的意图:
参数校验与锁获取:检查 CPU 号合法性、是否 boot CPU、是否已经离线。获取
cpu_hotplug_lock的写锁,这会阻塞所有get_online_cpus()的读者。状态标记:把 CPU 从
cpu_active_mask清除,调用sched_cpu_deactivate()。此时调度器不再往这个 CPU 派新任务,但已有任务还在跑。任务迁移:
sched_cpu_dying()把 runqueue 里的任务迁移走,等待当前运行任务让出。这一步可能耗时较长。中断迁移:
irq_migrate_all_off_this_cpu()把该 CPU 上的中断重新分配到其他 CPU。注意,有些中断是绑定的,无法迁移,这时会报错并中止离线。定时器迁移:把该 CPU 的定时器迁移到其他 CPU,停掉本地定时器。
通知链回调:按状态机逆序执行各子系统的 teardown 回调。这一步是重头戏,涉及 RCU、workqueue、hrtimer、perf、cpufreq 等几十个子系统。
最终下线:调用架构相关的
__cpu_die(),让 CPU 进入停止状态。在 ARM64 上通常是执行 WFI 或进入 PSCI CPU_OFF。
整个流程里,第 4 步和第 6 步是最容易出问题的。中断迁移失败会导致离线直接失败;通知链回调里如果有子系统没处理好,可能死锁或崩溃。
3.3 在线流程的对称性
在线流程基本是离线流程的镜像,但有几个不对称的地方值得注意:
- 在线时 CPU 从
possible状态开始,需要先做架构相关的__cpu_up(),让 CPU 真正跑起来 - 然后按状态机正序执行各子系统的 startup 回调
- 最后设置
cpu_online_mask和cpu_active_mask
不对称的关键点在于:离线时任务迁移是"推"出去的,在线时任务不会自动"拉"回来。新上线的 CPU 是空闲的,调度器会在后续的负载均衡中逐渐把任务分过来。所以刚上线的 CPU 利用率是 0,需要等一个调度周期才会看到负载。
这个特性在动态调频场景下很重要。如果你刚上线一个 CPU 就立刻读它的频率,可能还是最低频,因为还没任务跑上去。要等负载均衡生效后再观察。
4. 功耗子系统视角下的 Hotplug 策略设计
前面讲的都是机制,现在进入正题:在功耗子系统里,怎么用好 CPU Hotplug。这部分是纯经验,文档里不会写,但实际项目里天天遇到。
4.1 Hotplug 与 CPUIdle 的收益对比
先看一组实测数据。在一台 8 核 ARM64 服务器上,空载状态下分别用 Idle 和 Hotplug 处理空闲核心,功耗对比如下:
| 策略 | 空闲核心状态 | 单核功耗 | 唤醒延迟 | 适用场景 |
|---|---|---|---|---|
| CPUIdle WFI | 浅睡 | 约 120mW | < 10us | 交互式负载 |
| CPUIdle 深睡 | 深睡,保留上下文 | 约 40mW | 约 100us | 后台任务 |
| CPU Hotplug | 核心断电 | 约 5mW | 约 5-20ms | 长时间空闲 |
| 电源域关闭 | 整簇断电 | 约 1mW | 约 50ms+ | 极低负载 |
数据很直观:Hotplug 的省电效果是 Idle 深睡的 8 倍,但唤醒延迟是 100 倍以上。这意味着Hotplug 只适合那些"确定长时间不会用到"的核心。如果你的负载是突发性的,用 Hotplug 会导致每次突发都要等核心上线,响应变慢。
我的经验是设置一个"空闲持续时间阈值":核心空闲超过 500ms 才考虑 Hotplug,低于这个值只用 Idle。这个阈值可以通过内核的sched_mc_power_savings或自定义的 governor 来调节。
4.2 什么时候该用 Hotplug,什么时候不该用
不是所有场景都适合 Hotplug。我总结了几条判断标准:
适合用 Hotplug 的场景:
- 服务器夜间低峰,负载可预测,核心可以长时间离线
- 虚拟化平台,vCPU 数量动态调整,配合热迁移
- 嵌入式设备待机,只有少数核心处理传感器中断
- 测试和调试,需要模拟不同核心数的系统行为
不适合用 Hotplug 的场景:
- 高频交易、实时控制,延迟敏感
- 负载波动剧烈,核心频繁上下线
- 中断绑定紧密,迁移代价高
- 有 per-CPU 缓存热数据,离线会丢失缓存局部性
特别提醒一点:频繁的 Hotplug 操作本身很耗电。每次上下线都要走一遍完整的状态机,涉及几十个子系统的回调,CPU 在这期间是满负荷运行的。如果核心离线 100ms 又上线,省的电还不够操作本身消耗的。所以一定要有滞回(hysteresis)设计,避免抖动。
4.3 与 cpufreq、cpuidle 的协同
CPU Hotplug 不是孤立的,它必须和 cpufreq、cpuidle 协同工作。三者的关系可以这样理解:
- cpuidle管"在线核心睡多深"
- cpufreq管"在线核心跑多快"
- CPU Hotplug管"哪些核心在线"
一个完整的功耗策略应该是分层的:先降频(cpufreq),再进深睡(cpuidle),最后才离线(Hotplug)。顺序反了就会出问题——比如你先把核心离线了,cpufreq 的 governor 就看不到这个核心的负载,无法做全局决策。
内核里 cpufreq 的 governor 在 CPU 离线时会收到通知,把该 CPU 的策略迁移到其他 CPU。但如果你用的是schedutilgovernor,它依赖调度器的负载信息,CPU 离线后这部分信息就没了。所以在 Hotplug 场景下,ondemand或conservativegovernor 往往比schedutil更稳定,因为它们基于独立的采样,不依赖调度器。
5. 踩坑实录:那些让我熬夜的 Hotplug 问题
这部分是我这些年踩过的真实坑,每个都花了至少一个通宵才定位。分享出来,希望你能少走弯路。
5.1 中断迁移失败导致的离线卡死
现象:执行echo 0 > /sys/devices/system/cpu/cpu3/online后命令挂住不返回,dmesg里刷irq_migrate_all_off_this_cpu相关警告。
排查过程:先看/proc/interrupts,发现有一个中断的 affinity 被设成了 CPU3,且这个中断的IRQD_AFFINITY_SET标志被置位,意味着用户空间手动绑定了它。内核在迁移中断时,遇到手动绑定的中断不会自动迁移,而是返回-EBUSY,导致整个离线流程卡住。
根因:某个驱动在初始化时调用了irq_set_affinity_hint()把中断绑到了特定 CPU,但没有在 CPU 离线时释放绑定。这是驱动作者的疏忽。
修复:在驱动的 CPU Hotplug 回调里,检测到目标 CPU 离线时,把中断 affinity 重置为默认。或者更简单,用irq_set_affinity_hint(irq, NULL)清除绑定。
经验:写驱动时,凡是调用了irq_set_affinity_hint()或irq_set_affinity()的地方,都要配套注册 CPU Hotplug 回调来清理。这个坑我见过至少三个不同的驱动犯。
5.2 RCU stall 与离线超时
现象:CPU 离线操作执行后,系统报rcu_sched detected stalls on CPUs/tasks,然后离线失败。
排查过程:用ftrace跟踪rcu_cpu_offline和rcu_report_qs_rnp,发现离线 CPU 在等待一个 RCU grace period 完成,但另一个 CPU 上有任务在 RCU 读临界区里长时间不退出。
根因:某个内核模块在 RCU 读临界区里做了耗时操作(比如等待 I/O),导致 grace period 无法结束。CPU 离线需要等所有 grace period 完成,于是卡住。
修复:把耗时操作移出 RCU 读临界区,或者用rcu_read_unlock()提前退出。
经验:RCU 读临界区里绝对不能睡眠、不能做耗时操作。这个规则大家都知道,但实际代码里违反的情况非常多。建议用CONFIG_PROVE_RCU和CONFIG_RCU_EQS_DEBUG做静态检查,能在编译期发现大部分问题。
5.3 per-CPU 变量访问导致的空指针
现象:CPU 离线后,某个驱动在中断处理里访问 per-CPU 变量,触发空指针或数据错乱。
排查过程:用 KASAN 定位到具体的访问点,发现驱动用this_cpu_ptr()访问了一个 per-CPU 数组,但该 CPU 离线后数组被释放了。
根因:驱动的 per-CPU 数据在 CPU 离线时被释放,但中断处理程序没有检查 CPU 是否在线,仍然访问。
修复:在中断处理里加cpu_online()检查,或者用get_cpu_ptr()/put_cpu_ptr()配对保护。
经验:per-CPU 数据的生命周期管理是 Hotplug 里最容易出错的地方。我的建议是:凡是 per-CPU 数据,都要想清楚它在 CPU 离线时是否还被访问。如果会,就必须在 hotplug 回调里做保护。
5.4 离线后频率信息丢失
现象:CPU 离线再上线后,cpufreq显示该 CPU 频率为 0,或者 governor 报错。
排查过程:查看cpufreq的 hotplug 回调,发现它在 CPU 离线时把 policy 释放了,但上线时没有重新初始化。
根因:cpufreq 的 hotplug 处理有 bug,或者驱动没有正确实现cpufreq_driver的online/offline回调。
修复:确保cpufreq_driver实现了online和offline回调,且在online里重新初始化 policy。
经验:cpufreq 和 Hotplug 的交互是重灾区。如果你在调频率相关的 bug,先确认 CPU 上下线时 cpufreq 的状态是否正确。用cat /sys/devices/system/cpu/cpuX/cpufreq/scaling_cur_freq验证。
6. 调试 CPU Hotplug 的实用工具箱
遇到 Hotplug 问题,光看代码是不够的,得有一套调试手段。这部分分享我常用的工具和方法。
6.1 ftrace 跟踪状态机执行
ftrace 是排查 Hotplug 问题的第一利器。内核在kernel/cpu.c里埋了大量 tracepoint,可以直接跟踪状态机的每一步:
# 开启 cpuhp 相关 tracepoint echo 1 > /sys/kernel/debug/tracing/events/cpuhp/enable # 执行离线操作 echo 0 > /sys/devices/system/cpu/cpu2/online # 查看跟踪结果 cat /sys/kernel/debug/tracing/trace输出会显示每个状态步骤的进入和退出,以及耗时。如果某个步骤卡住,一眼就能看出来。我通常还会配合function_graphtracer 看具体函数调用链:
echo function_graph > /sys/kernel/debug/tracing/current_tracer echo 'cpu_down' > /sys/kernel/debug/tracing/set_ftrace_filter这样能看到cpu_down内部每个函数的执行时间和调用关系,定位耗时点非常有效。
6.2 用 lockdep 抓死锁
Hotplug 涉及大量锁,死锁是常见问题。lockdep能在死锁发生前就报警:
# 确认 lockdep 已开启 cat /proc/sys/kernel/lock_stat # 开启锁统计 echo 1 > /proc/sys/kernel/lock_stat执行 Hotplug 操作后,如果 lockdep 检测到潜在死锁,会在dmesg里打印详细的锁依赖链。我遇到过一个经典死锁:cpu_hotplug_lock和某个驱动的 mutex 形成 AB-BA 依赖,lockdep 直接指出了问题。
注意:lockdep 本身有性能开销,生产环境通常关闭。调试时临时开启,定位完就关掉。
6.3 常见错误码速查
Hotplug 操作失败时,sysfs 会返回错误码。这些错误码的含义和排查方向如下:
| 错误码 | 含义 | 排查方向 |
|---|---|---|
| -EINVAL | 参数非法 | CPU 号越界、boot CPU 不可离线 |
| -EPERM | 权限不足 | 非 root 用户、CPU 被锁定 |
| -EBUSY | 资源忙 | 中断无法迁移、任务无法迁移 |
| -ENOSYS | 功能未实现 | 架构不支持 Hotplug |
| -ETIMEDOUT | 超时 | 任务迁移或 RCU 同步超时 |
看到-EBUSY时,重点查中断和任务迁移;看到-ETIMEDOUT,重点查 RCU 和调度器。
6.4 用 CPU 隔离做对照实验
有时候问题不好定位,可以用 CPU 隔离(isolcpus)做对照。把可疑 CPU 从调度器中隔离出来,看问题是否复现:
# 启动参数加 isolcpus=3 # 或者运行时用 cpuset 隔离 mkdir /sys/fs/cgroup/cpuset/test echo 3 > /sys/fs/cgroup/cpuset/test/cpuset.cpus如果隔离后问题消失,说明问题出在调度器与 Hotplug 的交互上;如果问题依旧,说明是更底层的架构或驱动问题。这个方法能快速缩小排查范围。
7. 写一个自己的 CPU Hotplug 回调
最后来点实操:如果你在写驱动,需要注册 CPU Hotplug 回调,怎么做才稳妥。这部分给一个完整的模板和注意事项。
7.1 注册回调的正确姿势
内核提供了cpuhp_setup_state()系列 API,最常用的是:
static int my_driver_cpu_online(unsigned int cpu) { /* CPU 上线时初始化 per-CPU 资源 */ struct my_percpu *pc = per_cpu_ptr(&my_data, cpu); return my_percpu_init(pc); } static int my_driver_cpu_offline(unsigned int cpu) { /* CPU 离线时清理 per-CPU 资源 */ struct my_percpu *pc = per_cpu_ptr(&my_data, cpu); my_percpu_cleanup(pc); return 0; } static int __init my_driver_init(void) { int ret; ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "mydriver:online", my_driver_cpu_online, my_driver_cpu_offline); if (ret < 0) return ret; my_hp_state = ret; return 0; } static void __exit my_driver_exit(void) { cpuhp_remove_state(my_hp_state); }关键点:
- 用
CPUHP_AP_ONLINE_DYN让内核自动分配状态号,不要硬编码 - 保存返回的状态号,退出时用
cpuhp_remove_state()注销 - online 和 offline 回调必须成对,且要幂等(可能被多次调用)
7.2 回调里能做什么、不能做什么
这是最容易踩坑的地方。回调的执行上下文和阶段决定了你能做什么:
online 回调(prepare 阶段):
- 不能睡眠,不能分配可能睡眠的内存
- 不能获取可能睡眠的锁
- 只能做轻量的初始化
online 回调(online 阶段):
- 可以睡眠,可以分配内存
- 可以获取 mutex
- 适合做完整的资源初始化
offline 回调:
- 执行时 CPU 还在运行,但已经不在调度器中
- 不能依赖调度器功能
- 要尽快完成,避免阻塞离线流程
我的建议是:尽量把重活放在 online 阶段,offline 阶段只做必要的清理。因为 offline 阶段阻塞会直接拖慢整个离线操作,而 online 阶段相对宽松。
7.3 一个真实的驱动改造案例
我之前改造过一个网络驱动,它原本用全局锁保护 per-CPU 统计,在 CPU 离线时会丢数据。改造方案是:
- 把全局锁改成 per-CPU 变量,每个 CPU 独立统计
- 注册 Hotplug 回调,在 offline 时把该 CPU 的统计累加到全局
- 在 online 时清零该 CPU 的统计
改造后,不仅 Hotplug 问题解决了,性能还提升了 30%,因为去掉了全局锁竞争。这个案例说明:Hotplug 问题往往暴露的是更深层的设计问题,解决它可能带来额外收益。
8. 一些零散但重要的经验
写到这里,主体内容差不多了,再补充几个零散但很实用的点。
关于 CPU0:虽然内核不允许 CPU0 离线,但在某些架构上可以通过maxcpus启动参数限制启动的核心数。如果你需要测试单核场景,用maxcpus=1比尝试离线 CPU0 靠谱。
关于热插拔的原子性:一次 Hotplug 操作不是原子的,中间可能失败并回滚。回滚过程会重新执行已经执行过的步骤的反向操作。所以你的回调必须支持"执行到一半被回滚"的情况,不能假设所有回调都会成功。
关于性能影响:Hotplug 操作会持有cpu_hotplug_lock写锁,期间所有get_online_cpus()的读者都会被阻塞。如果你的系统里有大量读者(比如网络收包路径),Hotplug 会造成明显的延迟抖动。所以生产环境要避免频繁 Hotplug,最好在低峰期做。
关于虚拟化:在 KVM 等虚拟化环境下,vCPU 的 Hotplug 由 hypervisor 控制,guest 内核看到的 Hotplug 是模拟的。调试时要注意区分是 guest 内部问题还是 host 侧问题。用virsh setvcpus之类的工具操作时,观察 guest 的dmesg能看到完整的 Hotplug 流程。
关于嵌入式:很多嵌入式 SoC 的 CPU Hotplug 依赖 PSCI 或类似的固件接口。如果固件实现有问题,Hotplug 会失败在__cpu_die()这一步。这时要看固件日志,而不是只盯着内核。
关于测试:写 Hotplug 相关代码后,一定要做压力测试。我常用的方法是循环上下线 1000 次,同时跑内存压力和网络压力,看是否稳定。很多竞态问题只有在高频操作下才会暴露。
# 简单的压力测试脚本 for i in $(seq 1 1000); do echo 0 > /sys/devices/system/cpu/cpu3/online echo 1 > /sys/devices/system/cpu/cpu3/online done这个脚本跑一晚上,能暴露大部分 Hotplug 相关的稳定性问题。如果 1000 次都稳,基本可以放心了。
我个人在实际操作中的体会是,CPU Hotplug 这个机制看起来简单,用起来处处是坑。它的复杂度不在于接口,而在于它牵动的子系统太多。每加一个新驱动、新特性,都要考虑它在 Hotplug 场景下的行为。这也是为什么内核社区对 Hotplug 相关的 patch 审查特别严格——一个疏忽就可能导致整个系统不稳定。如果你正在做功耗优化,建议先把 Idle 和 cpufreq 调好,最后再考虑 Hotplug,这样收益和风险的平衡最好。