ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux CPU Hotplug 功耗管理:内核状态机、通知链与调优实践

Linux CPU Hotplug 功耗管理:内核状态机、通知链与调优实践 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 架构的 SoC4 个小核 4 个大核每个 cluster 共享一个时钟源和电压域。当你 offline 掉一个大核的时候CPUFreq 需要判断这个 cluster 里还有没有其他 online 的核如果有频率策略保持不变如果没有这个 cluster 的频率策略需要被移除或者迁移。内核里的处理逻辑是每个 policy 对应一个 CPU clusterpolicy 里有一个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一旦 offlineCPUIdle 就完全不参与了。但问题是有些平台的 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 的 CPUcat /sys/devices/system/cpu/online # 输出类似 0-3表示 CPU0 到 CPU3 在线查看所有 possible 的 CPUcat /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_UP80 # 负载高于此值online 更多 CPU THRESHOLD_DOWN20 # 负载低于此值offline 多余 CPU CHECK_INTERVAL2 # 检查间隔秒 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 (NF2) print $2-$11; else print NF} } while true; do load$(get_load) online$(get_online_count) possible$(cat /sys/devices/system/cpu/possible | \ sed s/-/ / | awk {if (NF2) print $2-$11; 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 的schedutilEASEnergy Aware Scheduling或者高通的msm_thermalcore_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这是最常见的问题。可能的原因和排查方法我整理成了一张表错误码可能原因排查方法-EINVALCPU 号超出 possible 范围检查/sys/devices/system/cpu/possible-EINVAL试图 offline boot CPUboot 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 $23 {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时移除了 policyonline 时应该重新创建但如果创建失败比如设备树里没有对应的 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_CPUy # 核心开关不打开就没有 hotplug 支持 CONFIG_CPU_FREQy # 频率调节hotplug 时策略迁移需要 CONFIG_CPU_IDLEy # idle 状态管理和 hotplug 配合 CONFIG_SCHED_SMTy # 如果支持超线程需要这个 CONFIG_GENERIC_CPU_AUTOPROBEy # 自动探测 CPU 能力CONFIG_HOTPLUG_CPU是总开关关掉之后/sys/devices/system/cpu/cpuN/online文件根本不会出现。有些嵌入式平台为了减小内核体积会关掉这个选项那就完全没有 hotplug 能力了。6.2 调试用的配置项调 hotplug 问题的时候建议打开这些调试选项CONFIG_CPU_HOTPLUG_STATE_CONTROLy # cpuhp 状态机调试 CONFIG_DEBUG_HOTPLUG_CPU0y # 允许 offline CPU0仅调试用 CONFIG_PM_DEBUGy # 电源管理调试 CONFIG_SCHED_DEBUGy # 调度器调试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: cpu0 { device_type cpu; compatible arm,cortex-a55; reg 0x0; enable-method psci; cpu-idle-states CPU_SLEEP_0 CLUSTER_SLEEP_0; }; cpu1: cpu1 { 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。常见的值有psciARM 标准、spin-table老式 ARM、qcom,scm高通等。如果enable-method配置错误hotplug online 会失败CPU 起不来。这个错误在 dmesg 里通常表现为CPU1: failed to boot: -22之类的信息。7. 性能与功耗的权衡什么时候该用 hotplug7.1 hotplug vs idle 的省电对比我做过一组实测在一个 4 核 ARM 平台上对比 offline 一个核和让这个核进入 deepest idle 的功耗差异。测试条件是系统空闲只跑一个后台日志线程方案单核功耗整机功耗唤醒延迟全核 online C1约 120mW约 480mW 1ms全核 online C7约 30mW约 210mW约 5ms3 核 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 一边看日志输出比事后翻日志效率高得多。
返回列表