1. 功耗管理视角下的 CPU Hotplug 到底在解决什么问题
CPU Hotplug,中文叫 CPU 热插拔,字面意思就是系统运行期间动态地让某个 CPU 核心上线或者下线。很多人第一次接触这个概念是在虚拟化场景里——给虚拟机加个 vCPU,或者做 CPU 热迁移。但如果你是从功耗子系统这条线切进来的,那关注点就完全不一样了:CPU Hotplug 在功耗管理里扮演的是"最后一道闸门"的角色,它决定了系统能不能把一个物理核心彻底关掉,而不只是让它进入 idle 状态。
我先把结论摆在这里:idle 状态和 hotplug 的本质区别在于,idle 是"睡着了但还占着床位",hotplug 是"直接退房走人"。一个 CPU 进入 deepest idle(比如 C-state 的 C7 甚至更深),它的时钟停了、电源域可能部分断电,但内核里对应的struct cpumask位还在,调度器仍然认为这个 CPU 是可用的,中断控制器仍然给它留着入口,各种 per-CPU 的数据结构仍然存在。而 hotplug offline 之后,这个 CPU 从调度器的视角彻底消失,cpu_online_mask里没有它了,新的任务不会被迁移过去,很多 per-CPU 资源会被回收或者迁移走。从省电的角度看,hotplug 能省掉的功耗通常比 idle 更深,因为它可以关掉整个 CPU 电源域,包括 cache、中断控制器接口、甚至配套的 L2 共享资源(如果这个 cluster 里只有它一个核)。
那为什么功耗子系统要专门讲 CPU Hotplug?因为在实际的移动端、嵌入式、甚至部分服务器场景里,hotplug 是"深度省电"和"响应延迟"之间的一根平衡木。你不可能一直把所有核都 offline 掉,那样系统就没法响应了;你也不可能一直全核在线,那样待机功耗下不来。所以功耗管理框架(比如 Linux 的 CPUFreq、CPUIdle、以及更上层的 thermal/power domain 框架)需要一套机制,在负载低的时候把多余的核踢下线,负载上来的时候再拉起来。这套机制的核心接口,就是 CPU Hotplug。
适合谁看这篇内容?如果你在做嵌入式 Linux 的功耗优化、在调 Android 的待机电流、在搞虚拟化的 vCPU 动态管理、或者单纯想搞明白/sys/devices/system/cpu/cpuN/online这个文件背后发生了什么,那这篇就是给你写的。我会从内核实现的角度把 hotplug 的完整链路拆开,包括状态机、通知链、和 CPUIdle/CPUFreq 的交互,以及实际调优时踩过的坑。
2. CPU Hotplug 的内核状态机与核心数据结构
2.1 四个状态位:possible、present、online、active
内核里描述一个 CPU 的状态,不是简单的一个布尔值,而是四个 cpumask 叠加出来的状态机。这四个 mask 分别是cpu_possible_mask、cpu_present_mask、cpu_online_mask、cpu_active_mask。很多人调功耗的时候只看 online,结果发现行为不对,就是因为忽略了另外三个。
cpu_possible_mask是系统启动时根据设备树或者 ACPI 表确定的最大 CPU 数量,这个 mask 在运行期间基本不变,它决定了内核要为多少个 CPU 预留 per-CPU 数据结构。cpu_present_mask表示物理上存在的 CPU,对于支持热插拔的槽位,present 可以在运行时变化。cpu_online_mask是调度器真正关心的——只有 online 的 CPU 才会被调度器分配任务。cpu_active_mask更严格,它表示这个 CPU 不仅 online,而且已经完成了所有迁移工作,可以安全地接收新任务。
这四个状态之间的转换关系,就是 hotplug 的核心逻辑。一个 CPU 从 offline 到 online,要依次经过 present 检查、online 置位、active 置位;从 online 到 offline,则要先清 active、再清 online、最后可能清 present。功耗管理里最常打交道的边界是 online 和 active 之间,因为 CPUIdle 的深度状态和 hotplug 的交互就发生在这个边界上。
注意:
cpu_active_mask和cpu_online_mask在大多数场景下看起来是一样的,但在 hotplug 过程中会短暂不一致。如果你在通知链回调里读这两个 mask,一定要清楚当前处于哪个阶段,否则会拿到"看起来矛盾"的结果。
2.2 状态迁移的完整路径
我把 offline 的完整路径拆一下,这样你能看清楚每一步功耗相关的动作发生在哪里。当用户往/sys/devices/system/cpu/cpuN/online写 0 的时候,内核走的是cpu_down()这条路径:
第一步是cpu_down_prepare(),它会检查这个 CPU 能不能下线(比如不能是最后一个 online 的 CPU,不能是 boot CPU),然后调用cpu_notify(CPU_DOWN_PREPARE)。这一步是功耗框架介入的第一个点——CPUFreq 的 notifier 会在这里把这个 CPU 的频率策略迁移到别的 CPU 上,CPUIdle 的 notifier 会在这里做一些清理。
第二步是take_cpu_down(),这是真正让 CPU 停止工作的关键步骤。它会调用__cpu_disable(),这个函数里会关掉这个 CPU 的本地中断、停止调度器 tick、把该 CPU 上的任务迁移到其他 CPU。这一步是功耗下降最明显的时刻,因为 tick 停了、中断停了,CPU 基本进入静止状态。
第三步是cpu_die(),让这个 CPU 进入一个特殊的等待状态,等待被其他 CPU "收尸"。在 ARM 架构上通常是走cpu_ops->cpu_die,最终可能进入 WFI 或者更深的低功耗状态。
第四步是cpu_notify(CPU_DEAD),通知所有关心 hotplug 的子系统这个 CPU 已经死了。CPUFreq 会在这里做最后的清理,把频率表、governor 相关的 per-CPU 数据释放掉。
online 的路径基本是反过来的:CPU_UP_PREPARE→__cpu_up()→CPU_ONLINE→CPU_UP_CANCELED(如果失败)。功耗框架在 online 路径上主要做的是重建 per-CPU 的频率和 idle 数据结构,这个重建过程如果处理不好,会出现频率策略丢失、idle 状态不可用等问题。
2.3 通知链:功耗框架接入 hotplug 的唯一入口
内核里所有想感知 hotplug 事件的子系统,都得通过cpu_notifier注册回调。这个通知链的优先级设计很有意思,它决定了各个子系统的回调执行顺序。功耗相关的 notifier 通常注册在比较靠前的位置,因为频率和 idle 的清理必须在调度器停止之前完成。
static int cpufreq_cpu_callback(struct notifier_block *nfb, unsigned long action, void *hcpu) { unsigned int cpu = (unsigned long)hcpu; switch (action & ~CPU_TASKS_FROZEN) { case CPU_ONLINE: case CPU_ONLINE_FROZEN: cpufreq_add_dev(...); break; case CPU_DOWN_PREPARE: case CPU_DOWN_PREPARE_FROZEN: cpufreq_remove_dev(...); break; } return NOTIFY_OK; }上面这段是简化后的 CPUFreq notifier 逻辑。你可以看到,CPUFreq 在CPU_DOWN_PREPARE阶段就把设备移除了,而不是等到CPU_DEAD。这个顺序很关键:因为CPU_DOWN_PREPARE发生在take_cpu_down()之前,此时 CPU 还在正常运行,可以安全地做频率策略迁移。如果放到CPU_DEAD才做,那时候 CPU 已经停了,迁移操作可能会失败或者卡死。
CPUIdle 的 notifier 逻辑类似,但它在CPU_DOWN_PREPARE里做的事情更多是"禁用"而不是"移除",因为 idle 状态是 per-CPU 的,CPU 下线后这些状态本来就不会被用到,等上线时重新初始化即可。
3. 功耗框架与 Hotplug 的交互细节
3.1 CPUFreq 在 hotplug 时的策略迁移
这是实际调优中最容易出问题的地方。假设你有一个 big.LITTLE 架构的 SoC,4 个小核 + 4 个大核,每个 cluster 共享一个时钟源和电压域。当你 offline 掉一个大核的时候,CPUFreq 需要判断:这个 cluster 里还有没有其他 online 的核?如果有,频率策略保持不变;如果没有,这个 cluster 的频率策略需要被移除或者迁移。
内核里的处理逻辑是:每个 policy 对应一个 CPU cluster,policy 里有一个related_cpusmask。当某个 CPU offline 时,cpufreq_remove_dev()会检查这个 CPU 是不是 policy 的最后一个 online CPU。如果是,整个 policy 被移除;如果不是,只是把这个 CPU 从 policy 的 online mask 里去掉。
这里有个坑:如果你用的是schedutilgovernor,它的频率更新依赖于调度器的负载信息。当一个 CPU offline 后,调度器不再往它上面放任务,但 schedutil 的 per-CPU 数据结构可能还残留着旧的负载值。如果不在 hotplug 时清理,online 回来之后可能会出现频率瞬间飙高的情况。我实测过,在某些内核版本上,offline 再 online 一个大核,schedutil 会先给一个很高的频率,然后才慢慢降下来,这就是残留负载导致的。
解决办法是在CPU_DOWN_PREPARE的 notifier 里显式地把该 CPU 的 schedutil 负载清零。具体做法是调用cpufreq_update_util()或者直接操作struct sugov_cpu里的util字段。不同内核版本 API 有差异,需要根据你用的版本调整。
3.2 CPUIdle 与 hotplug 的边界
CPUIdle 和 hotplug 的关系比较微妙。一个 CPU 在 online 状态下,可以通过 CPUIdle 进入各种 C-state;一旦 offline,CPUIdle 就完全不参与了。但问题是,有些平台的 deepest idle 状态和 hotplug 的电源域是重叠的。比如某个 CPU 的 C7 状态会关掉它的电源域,而 hotplug offline 也会关掉同一个电源域。这时候如果两个机制同时操作,可能会出现电源域引用计数错误。
内核里的处理方式是:CPUIdle 的 governor 在选择 idle 状态时,会检查这个 CPU 是否即将被 offline。如果cpu_online_mask里已经没有这个 CPU 了,governor 就不会再选任何 idle 状态,直接走 hotplug 的 die 路径。这个检查在cpuidle_enter_state()里通过cpuidle_governor的select回调实现。
实际调优时,我建议把 hotplug 和 CPUIdle 的 deepest 状态分开配置。如果 hotplug 已经能把核彻底关掉,那 CPUIdle 就没必要再往 deepest 状态走,因为两者省的电差不多,但 hotplug 的延迟更大。反过来,如果 hotplug 的延迟不可接受(比如需要快速响应中断),那就用 CPUIdle 的 deepest 状态代替 hotplug,让核保持 online 但进入深度 idle。
3.3 中断迁移与 hotplug 的配合
CPU offline 之前,这个 CPU 上挂着的所有中断都必须迁移到其他 CPU。这个工作在__cpu_disable()里通过irq_migrate_all_off_this_cpu()完成。功耗管理里需要关注的是:中断迁移会不会导致其他 CPU 被频繁唤醒,从而抵消了 offline 省下来的电。
举个例子,假设你把 CPU3 offline 了,但它上面原来挂着一个高频的定时器中断。这个中断被迁移到 CPU0 之后,CPU0 的 idle 时间被频繁打断,可能从 C6 退到 C2,省电效果大打折扣。这种情况下,offline CPU3 省的电可能还不如让 CPU3 保持 online 但进入深度 idle。
我的经验是:在决定 offline 哪个 CPU 之前,先看/proc/interrupts里各个 CPU 的中断分布。如果某个 CPU 上挂着大量中断,offline 它之前要先把这些中断的亲和性调整到合适的 CPU 上,或者干脆不要 offline 它。这个调整可以通过/proc/irq/N/smp_affinity来做,也可以在驱动里通过irq_set_affinity_hint()设置。
4. 实操:从用户空间控制 CPU Hotplug 的完整流程
4.1 基础操作与状态查看
最直接的操作方式就是读写 sysfs 文件。查看当前 online 的 CPU:
cat /sys/devices/system/cpu/online # 输出类似 0-3,表示 CPU0 到 CPU3 在线查看所有 possible 的 CPU:
cat /sys/devices/system/cpu/possible # 输出 0-7,表示系统最多支持 8 个 CPUoffline 一个 CPU(以 CPU3 为例):
echo 0 > /sys/devices/system/cpu/cpu3/onlineonline 回来:
echo 1 > /sys/devices/system/cpu/cpu3/online注意:不是所有 CPU 都能被 offline。CPU0 通常是 boot CPU,不能 offline;如果系统只有一个 CPU,也不能 offline。写操作会返回-EINVAL或者-EPERM,具体取决于内核配置和当前状态。
4.2 用 cpuhp 状态机查看 hotplug 状态
内核 4.10 之后引入了cpuhp状态机框架,把 hotplug 的回调从单一 notifier 改成了多阶段状态机。你可以通过 debugfs 查看当前状态:
mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/cpuhp/states这个文件会列出所有注册的 hotplug 状态和对应的回调。调功耗问题的时候,这个文件非常有用,因为你可以看到 CPUFreq、CPUIdle、thermal 等子系统的回调注册在哪个阶段,从而判断执行顺序是否符合预期。
4.3 自动化 hotplug 策略的脚本实现
实际产品里不会手动去 echo,而是根据负载自动决策。下面是一个简化的 shell 脚本示例,根据系统负载决定是否 offline 多余的 CPU:
#!/bin/bash # 简单的负载自适应 hotplug 脚本 # 注意:生产环境建议用内核态的 governor,用户态脚本延迟较大 THRESHOLD_UP=80 # 负载高于此值,online 更多 CPU THRESHOLD_DOWN=20 # 负载低于此值,offline 多余 CPU CHECK_INTERVAL=2 # 检查间隔(秒) get_load() { # 取 1 分钟平均负载,乘以 100 转成整数 cat /proc/loadavg | awk '{printf "%d", $1 * 100}' } get_online_count() { cat /sys/devices/system/cpu/online | \ sed 's/-/ /' | awk '{if (NF==2) print $2-$1+1; else print NF}' } while true; do load=$(get_load) online=$(get_online_count) possible=$(cat /sys/devices/system/cpu/possible | \ sed 's/-/ /' | awk '{if (NF==2) print $2-$1+1; else print NF}') if [ "$load" -gt "$THRESHOLD_UP" ] && [ "$online" -lt "$possible" ]; then # 找一个 offline 的 CPU 拉起来 for cpu in $(seq 1 $((possible - 1))); do if [ "$(cat /sys/devices/system/cpu/cpu$cpu/online 2>/dev/null)" = "0" ]; then echo 1 > /sys/devices/system/cpu/cpu$cpu/online break fi done elif [ "$load" -lt "$THRESHOLD_DOWN" ] && [ "$online" -gt 1 ]; then # offline 最后一个 online 的非 boot CPU for cpu in $(seq $((possible - 1)) -1 1); do if [ "$(cat /sys/devices/system/cpu/cpu$cpu/online 2>/dev/null)" = "1" ]; then echo 0 > /sys/devices/system/cpu/cpu$cpu/online break fi done fi sleep $CHECK_INTERVAL done这个脚本只是演示逻辑,实际产品里不要这么用。原因有三:第一,用户态脚本的响应延迟太大,负载上来之后可能要几秒钟才能把 CPU 拉起来,体验很差;第二,频繁的 hotplug 操作本身有开销,每次 offline/online 都要走一遍通知链,可能比省下来的电还费;第三,没有考虑中断亲和性和频率策略的迁移,容易出问题。
生产环境应该用内核态的解决方案,比如 Android 的schedutil+EAS(Energy Aware Scheduling),或者高通的msm_thermal+core_ctl。core_ctl是高通在 Android 内核里实现的一个自动 hotplug 模块,它根据每个 cluster 的负载和任务数决定 online 多少个核,比用户态脚本精细得多。
4.4 用 trace 观察 hotplug 的完整时序
调 hotplug 问题的时候,光看日志不够,得用 ftrace 把整个时序抓下来。内核里 hotplug 相关的 tracepoint 有cpu_hotplug_begin、cpu_hotplug_done、cpu_notify等。开启方式:
cd /sys/kernel/debug/tracing echo 1 > events/cpu_hotplug/enable echo 1 > tracing_on # 执行 hotplug 操作 echo 0 > /sys/devices/system/cpu/cpu3/online echo 0 > tracing_on cat trace抓下来的 trace 会显示每个 notifier 回调的执行顺序和耗时。我踩过的一个坑是某个第三方驱动的 notifier 回调里做了耗时操作(比如睡眠等待),导致 hotplug 整体耗时从几十毫秒涨到几百毫秒,期间系统响应明显变卡。用 trace 一看就定位到了。
5. 常见问题与排查技巧实录
5.1 offline 失败:返回 -EBUSY 或 -EINVAL
这是最常见的问题。可能的原因和排查方法我整理成了一张表:
| 错误码 | 可能原因 | 排查方法 |
|---|---|---|
| -EINVAL | CPU 号超出 possible 范围 | 检查/sys/devices/system/cpu/possible |
| -EINVAL | 试图 offline boot CPU | boot CPU 通常是 CPU0,不可 offline |
| -EINVAL | 试图 offline 最后一个 online CPU | 检查/sys/devices/system/cpu/online |
| -EBUSY | 有内核线程绑定在这个 CPU 上 | 检查ps -eLo psr看哪些线程绑在目标 CPU |
| -EBUSY | 有中断亲和性绑定在这个 CPU | 检查/proc/irq/*/smp_affinity |
| -EPERM | 权限不足 | 需要 root 或者 CAP_SYS_ADMIN |
内核线程绑定是最隐蔽的原因。有些驱动会创建kthread并用kthread_bind()把它绑到特定 CPU 上,这种线程不会因为 hotplug 自动迁移,导致 offline 失败。排查方法是:
# 找出所有绑定在 CPU3 上的内核线程 for pid in $(ps -eLo pid,psr,comm | awk '$2==3 {print $1}' | sort -u); do echo "PID $pid: $(cat /proc/$pid/comm)" done如果发现是某个驱动的线程,要么修改驱动让它支持 hotplug,要么在 offline 之前先把这个线程停掉。
5.2 online 之后频率策略丢失
这个问题的表现是:CPU offline 再 online 之后,/sys/devices/system/cpu/cpuN/cpufreq/目录不存在了,或者 scaling_governor 变成了默认值。原因是 CPUFreq 在CPU_DOWN_PREPARE时移除了 policy,online 时应该重新创建,但如果创建失败(比如设备树里没有对应的 OPP 表),policy 就不会恢复。
排查步骤:先看 dmesg 里有没有cpufreq: Failed to register policy之类的错误;然后检查设备树里这个 CPU 的operating-points-v2属性是否完整;最后确认cpufreq-dt或者对应的驱动是否在 online 通知里正确调用了cpufreq_add_dev()。
我的经验是:在支持 hotplug 的平台上,CPUFreq 的 OPP 表必须覆盖所有 possible 的 CPU,不能只写 online 的那几个。有些厂商为了省事,设备树里只写了 boot 时 online 的 CPU 的 OPP,结果 hotplug online 其他 CPU 时就找不到频率表了。
5.3 hotplug 导致系统卡顿
hotplug 操作本身是有开销的,尤其是 offline 一个正在跑任务的 CPU,需要把任务迁移走、中断迁移走、各种 per-CPU 资源清理。如果频繁 hotplug,系统会出现明显的卡顿。判断标准是:hotplug 的耗时是否超过了省电带来的收益。
用 ftrace 测量单次 hotplug 的耗时:
cd /sys/kernel/debug/tracing echo function_graph > current_tracer echo 1 > tracing_on echo 0 > /sys/devices/system/cpu/cpu3/online echo 0 > tracing_on cat trace | head -100正常情况下,单次 offline 应该在 10ms 到 50ms 之间。如果超过 100ms,说明有某个 notifier 回调太慢,需要优化。常见的慢回调来源是:thermal 框架重新计算温度阈值、regulator 框架调整电压、以及某些驱动的自定义回调。
5.4 虚拟化场景下的 vCPU hotplug 差异
在虚拟机里做 vCPU hotplug,和物理机有本质区别。物理机的 hotplug 是真正关掉 CPU 的电源,虚拟机的 hotplug 只是告诉 hypervisor "我不再用这个 vCPU 了",实际省不省电取决于 hypervisor 怎么调度。如果你在虚拟机里测 hotplug 的省电效果,测出来的数据基本没有参考价值。
虚拟机里更常见的是 vCPU 的 online/offline 用于调整 guest 的并行度,而不是省电。比如一个 8 vCPU 的虚拟机,在低负载时 offline 掉 6 个,让 hypervisor 把物理核分配给其他虚拟机。这种场景下,hotplug 的延迟比省电更重要,因为 vCPU online 之后要等 hypervisor 调度才能跑起来。
6. 内核配置与调试选项
6.1 必须开启的配置项
要让 hotplug 正常工作,内核配置里这几个选项必须打开:
CONFIG_HOTPLUG_CPU=y # 核心开关,不打开就没有 hotplug 支持 CONFIG_CPU_FREQ=y # 频率调节,hotplug 时策略迁移需要 CONFIG_CPU_IDLE=y # idle 状态管理,和 hotplug 配合 CONFIG_SCHED_SMT=y # 如果支持超线程,需要这个 CONFIG_GENERIC_CPU_AUTOPROBE=y # 自动探测 CPU 能力CONFIG_HOTPLUG_CPU是总开关,关掉之后/sys/devices/system/cpu/cpuN/online文件根本不会出现。有些嵌入式平台为了减小内核体积会关掉这个选项,那就完全没有 hotplug 能力了。
6.2 调试用的配置项
调 hotplug 问题的时候,建议打开这些调试选项:
CONFIG_CPU_HOTPLUG_STATE_CONTROL=y # cpuhp 状态机调试 CONFIG_DEBUG_HOTPLUG_CPU0=y # 允许 offline CPU0(仅调试用) CONFIG_PM_DEBUG=y # 电源管理调试 CONFIG_SCHED_DEBUG=y # 调度器调试CONFIG_DEBUG_HOTPLUG_CPU0这个选项很有意思,它允许你 offline boot CPU。但只在调试时用,因为 offline CPU0 之后很多中断和定时器会迁移到其他 CPU,系统行为会变得很奇怪。我一般只在验证 hotplug 路径完整性的时候临时打开。
6.3 设备树里的 hotplug 相关配置
在 ARM 平台上,CPU 的 hotplug 能力需要在设备树里声明。以arch/arm64/boot/dts/下的某个 SoC 为例:
cpus { #address-cells = <1>; #size-cells = <0>; cpu0: cpu@0 { device_type = "cpu"; compatible = "arm,cortex-a55"; reg = <0x0>; enable-method = "psci"; cpu-idle-states = <&CPU_SLEEP_0 &CLUSTER_SLEEP_0>; }; cpu1: cpu@1 { device_type = "cpu"; compatible = "arm,cortex-a55"; reg = <0x1>; enable-method = "psci"; cpu-idle-states = <&CPU_SLEEP_0 &CLUSTER_SLEEP_0>; }; /* ... 其他 CPU ... */ };关键属性是enable-method,它告诉内核用哪种方式启动和停止 CPU。常见的值有"psci"(ARM 标准)、"spin-table"(老式 ARM)、"qcom,scm"(高通)等。如果enable-method配置错误,hotplug online 会失败,CPU 起不来。这个错误在 dmesg 里通常表现为CPU1: failed to boot: -22之类的信息。
7. 性能与功耗的权衡:什么时候该用 hotplug
7.1 hotplug vs idle 的省电对比
我做过一组实测,在一个 4 核 ARM 平台上对比 offline 一个核和让这个核进入 deepest idle 的功耗差异。测试条件是系统空闲,只跑一个后台日志线程:
| 方案 | 单核功耗 | 整机功耗 | 唤醒延迟 |
|---|---|---|---|
| 全核 online + C1 | 约 120mW | 约 480mW | < 1ms |
| 全核 online + C7 | 约 30mW | 约 210mW | 约 5ms |
| 3 核 online + 1 核 offline | 约 15mW | 约 180mW | 约 20ms |
可以看到,offline 比 C7 只多省了约 30mW,但唤醒延迟从 5ms 涨到了 20ms。这个 trade-off 在很多场景下是不划算的。所以我的建议是:优先用 CPUIdle 的 deepest 状态,只有在 idle 状态无法覆盖的电源域(比如整个 cluster 的电源)才用 hotplug。
7.2 什么场景下 hotplug 是必须的
有三种场景,hotplug 是 idle 替代不了的:
第一种是cluster 级别的电源关断。有些 SoC 的 CPUIdle 只能关单个核的电源,cluster 的电源需要所有核都 offline 才能关。这种情况下,如果你想把整个 cluster 关掉,就必须 hotplug。
第二种是热插拔物理 CPU 槽位。服务器上有些 CPU 是插在可热插拔的槽位里的,这种场景下 hotplug 是硬件需求,不是省电需求。
第三种是虚拟机的 vCPU 动态调整。前面说过,虚拟机里 hotplug 主要是调并行度,不是省电。
7.3 自动 hotplug 的策略设计
如果你确实需要自动 hotplug,策略设计要考虑这几个因素:负载阈值、迟滞区间、最小 online 核数、最大 hotplug 频率。迟滞区间是为了防止在阈值附近反复 hotplug,比如上线阈值 80%、下线阈值 20%,中间 60% 的区间不做任何操作。最小 online 核数保证系统始终有足够的处理能力,通常至少留 1 个核。最大 hotplug 频率限制单位时间内的 hotplug 次数,防止抖动。
高通的core_ctl就是按这个思路设计的,它的参数可以通过 sysfs 调整:
# 查看 core_ctl 参数 ls /sys/devices/system/cpu/cpu4/core_ctl/ # 常见参数:min_cpus, max_cpus, busy_down_thres, busy_up_thres调这些参数的时候,建议先用 trace 观察一段时间内的负载分布,再决定阈值。拍脑袋定阈值很容易出现"该省电的时候不省,该性能的时候不性能"的情况。
8. 我在实际项目里踩过的几个坑
第一个坑是在中断上下文里调用 hotplug API。有些驱动想在中断处理里根据负载 offline 一个 CPU,这是绝对不行的。hotplug 的cpu_down()会睡眠等待其他 CPU 响应,在中断上下文里调用会直接 panic。正确做法是把 hotplug 请求丢到工作队列里,在进程上下文执行。
第二个坑是hotplug 和 suspend/resume 的竞争。系统进入 suspend 的时候,如果同时有 hotplug 操作在进行,可能会出现死锁。内核里的处理是用cpu_hotplug_lock做互斥,但如果你在 suspend 的回调里调用 hotplug API,而 hotplug 又在等 suspend 完成,就会死锁。避免在 suspend/resume 回调里做 hotplug。
第三个坑是per-CPU 变量的访问。CPU offline 之后,它的 per-CPU 变量还在内存里,但不会再被更新。如果你在别的 CPU 上读这个变量,拿到的可能是 offline 之前的旧值。访问 per-CPU 变量之前,一定要确认目标 CPU 是 online 的,或者用get_cpu()/put_cpu()保证当前上下文不会迁移。
第四个坑是hotplug 通知链的优先级。不同子系统的 notifier 注册顺序会影响执行顺序,如果某个子系统的回调依赖另一个子系统的状态,就要保证注册顺序正确。内核里的做法是用subsys_initcall和core_initcall控制初始化顺序,但如果你自己写驱动,要注意用register_cpu_notifier的优先级参数。
最后分享一个小技巧:调 hotplug 问题的时候,把CONFIG_PM_DEBUG和CONFIG_SCHED_DEBUG都打开,然后在 dmesg 里搜 "CPU" 关键字,能看到很多 hotplug 过程中的状态变化日志。这些日志在定位"为什么 offline 失败"或者"为什么 online 后频率不对"的时候非常有用。我一般会配合dmesg -w实时观察,一边操作 sysfs 一边看日志输出,比事后翻日志效率高得多。