文章/linux-idle
22 分钟阅读
Linux 下的 CPU Idle 行为详解:从 idle=poll 到 C-State
CPU 没事干的时候,它真的在休息吗?
1. 为什么需要关心 idle 行为
现代服务器 CPU 大部分时间是闲着的。哪怕整机负载很高,拆到单核上看,仍然有大量"等下一次调度"的空隙。CPU 在这段空隙里干什么,直接决定两件事:
- 功耗:深睡眠状态(如 C6)下核心电压可以降到接近零,整机功耗差异可达数倍;
- 唤醒延迟:从深睡眠回到可执行状态需要几十到上百微秒,对延迟敏感业务(高频交易、内存数据库、DPDK 转发)是实打实的尾延迟来源。
Linux 的 idle 子系统就在功耗和延迟之间做权衡,idle= 系列内核参数让你绕开这套权衡,直接指定 idle 行为。
2. idle loop:空闲时内核在跑什么
每个 CPU 一创建就带一个 0 号线程,也就是 idle 线程(swapper)。调度器找不到可运行的任务时切进来,执行 do_idle()(kernel/sched/idle.c)。简化之后是这样:
do_idle()
└── 循环:
├── 检查 need_resched(),有任务就赶紧回去调度
├── 关中断,进入"准睡眠"
├── 调用 cpuidle 框架: cpuidle_idle_call()
│ ├── governor 选择目标 idle state(预测空闲时长)
│ └── driver 执行进入该状态的硬件指令(MWAIT / HLT / WFI)
└── 被中断唤醒 → 开中断 → 回到循环顶部
两个关键抽象:
- Governor(策略层):根据历史空闲时长、定时器距离、调度器提示,预测这次会空多久,挑一个"够省但不至于醒得太慢"的状态。内核里有四个:
menu、TEO(Timer Events Oriented)、ladder、haltpoll,用哪个取决于内核配置:tickless 系统(空闲时能停调度 tick)默认menu,非 tickless 的默认ladder。TEO是menu的替代品,得自己选;haltpoll只在虚拟化场景里跟着 haltpoll 驱动一起上。
注意别混淆:这里说的是 CPUIdle 的 governor。
Linux 里有两个都叫 "governor" 的东西:
- CPUIdle governor(这篇讲的):决定空闲时进哪个 C-State,可选值通常是
menu/teo/ladder(虚拟化场景还会有haltpoll),查看方式是:$ cat /sys/devices/system/cpu/cpuidle/available_governors menu teo $ cat /sys/devices/system/cpu/cpuidle/current_governor menu如果teo和menu都编译进去了,默认还是menu。governor 是按rating挑的,menu是 20、teo是 19。想换就写current_governor,或者加cpuidle.governor=teo。- CPUFreq governor(调频子系统):决定运行时用哪个频率(P-State)。你看到的如果是
performance/powersave两个选项,那看的就是它:$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors performance powersave这是intel_pstate跑在 active 模式(HWP)下的典型表现:细粒度调频交给硬件自治,内核只留"性能优先"和"省电优先"两档。两者的分工:cpufreq governor 决定 CPU 在工作时运行在哪个频率(P-State),cpuidle governor 决定 CPU 在空闲时进入哪个休眠状态(C-State)。
idle=参数只作用于后者。
- Driver(机制层):真正执行硬件指令的那一层。x86 Intel 平台通常是
intel_idle,用驱动内置的 CPU 型号表去驱动 MWAIT,默认不理 ACPI 说什么,BIOS 里关掉的 C-State 也可能被它重新启用。AMD 和其他 x86 平台走通用的acpi_idle,解析 ACPI 表,BIOS 关掉的状态就不会出现。KVM 半虚拟化场景还有haltpoll。
AMD 没有专属驱动,走的就是
acpi_idleIntel 有
intel_idle,AMD 没有对应的东西。主线内核里不存在amd_idle这种 AMD 专用 cpuidle 驱动,drivers/idle/下从头到尾只有intel_idle.c一个文件。AMD 机器上的 idle 行为一律交给acpi_idle,状态全部从 ACPI 表枚举。这条路上踩过两个历史包袱,后来分别修掉了:
- dummy wait。2002 年 ACPI support 刚进内核时留了一段"空读 I/O 端口"的延迟操作,用来兼容某些 STPCLK# 断言不及时的老芯片组。2022 年 AMD 的 Prateek Nayak 用 IBS 采样发现,Zen 上大量时间耗在这段空转里,还被误计成 C-State 驻留。驻留数字偏高会诱导 governor 选更深的状态,形成恶性循环。双路 Zen3 上跑 tbench(128 客户端),跳过 dummy wait 后最低吞吐从 2215 MB/s 提到 33016 MB/s,平均提升约 50%,几乎追平"直接把 C2 关掉"。随后 Dave Hansen 又发了个紧急 patch,把 dummy wait 限制成 Intel 专用。
- 用 MWAIT 还是 HLT。没有 cpuidle 驱动可加载时(BIOS 里关了全局 C-State,或者内核压根没编译 cpuidle),x86 的兜底路径在 AMD 上原本一律用 HLT。AMD 的 Wyes Karny 在 6.0 前后改了这件事:Family 17h(Zen)及以后优先用 MWAIT C1,实测 Zen 3 上退出延迟降约 22%,上下文切换场景的唤醒延迟降约 45%。这也解释了为什么
idle=nomwait在 AMD 上能把默认路径退回 HLT。所以 AMD 这边不存在"驱动之争",只有
acpi_idle一条路上打的补丁。接力关系:
intel_idle初始化失败(CPU 不认识,或传了idle=覆盖默认行为)就退回acpi_idle;acpi_idle因为 ACPI 信息缺失也起不来,才轮到架构默认的default_idle(HLT 还是 MWAIT C1,看 CPU 和内核版本)。
3. C-State:idle 状态的硬件分级
Intel/AMD CPU 提供分级睡眠,越深越省电、醒得越慢:
| 状态 | 含义 | 典型退出延迟 | 典型功耗影响 |
|---|---|---|---|
| C0 | 运行中 | — | 满功耗 |
| C1 (HLT) | 时钟门控,流水线停 | < 1 µs | 略降 |
| C1E | 增强 C1,降压降频 | ~1 µs | 进一步降低 |
| C3 | L1/L2 缓存 flush,总线时钟停 | ~数十 µs | 明显下降 |
| C6 | 核心电压近零,状态存入 SRAM | ~50–100+ µs | 核心几乎零功耗 |
C2、C4、C5 去哪了?
C-State 的编号不连续:
C1–C3 是 ACPI 规范定义的。真正意义上的硬件级 C2("Stop-Clock",通过 STPCLK 引脚在总线层面停时钟)确实已经死了。省电有限,实现复杂,现代平台不再做。
但 "C2" 这个名字还活着,全新的机器上你可能就看得见。ACPI 只有 C1/C2/C3 三个类型档位,idle 状态凡是走 ACPI 表枚举出来的,sysfs 里的名字就是
C1_ACPI/C2_ACPI/C3_ACPI。实际睡多深由_CST里携带的 MWAIT hint 决定,很可能直达 C6/C10,ACPI 的 C2/C3 只是粗略分桶。最典型的触发场景是 CPU 型号太新,内核内置的 idle 状态表还没收录它。比如一台 2025 年的 Core Ultra 9 285H(Arrow Lake,model 197),在旧内核上看到的是:$ cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name POLL C1_ACPI C2_ACPI C3_ACPI
这说明 intel_idle 不认识这个型号,退回去用了 ACPI 枚举。等内核升到收录了该型号的版本,同一台机器可能变成
C1 / C6 / C10这样的"原生"名字:状态名字跟着内核对 CPU 型号的认知走,硬件本身没变。acpi_idle获取 C-State 信息的两个 ACPI 来源:_CST(C States):挂在每个 CPU 的 ACPI 对象下的方法,现代平台的主路径。它逐个描述状态的三要素:进入方式(寄存器地址 + 地址空间类型,FFH表示走 MWAIT,SystemIO表示走 I/O 端口)、退出延迟、功耗。状态数量不限,由固件动态生成。前面说"MWAIT hint 决定实际深度",那个 hint 就藏在_CST返回包的 FFH 描述符里。- FADT(Fixed ACPI Description Table):老平台的兜底路径。表里只有
P_LVL2_LAT/P_LVL3_LAT等几个固定字段,最多描述 C2、C3 两个状态,进入方式还写死为 I/O 端口(P_LVL2/P_LVL3寄存器)。表达能力远弱于_CST,这正是老机器上 ACPI 枚举的状态又少又浅的原因。内核只在找不到_CST或者_CST无效时才退回来读 FADT。 所以一张状态表的生命力排序是:驱动内置表(intel_idle)>_CST> FADT,越靠后信息越粗略。
C4/C5 是移动平台过渡时期的产物。Pentium M / Core Duo 时代(2003–2006)Intel 试过 C4(Deeper Sleep,进一步降压)这类中间档位,C5 则停留在规范草案层面,基本没有量产实现。这些中间状态很快被淘汰,因为 C6(2006 年随 Core 2 引入)做到了"状态存入专用 SRAM + 核心电压归零",省电效果碾压式领先,中间的过渡档位就没意义了。
编号是厂商各自扩展的,ACPI 只管 C1–C3。Intel 之后一路往后编:C7(LLC 可以 flush)、C8/C9/C10(更深、更多部件断电),还要区分核级 C-State(CCx)和封装级 C-State(PCx,整个 package 一起睡,要求所有核都睡了才能进)。AMD 有自己的 CC6/PC6。
还有
C1E这类带后缀的变体,它是 C1 的增强版:进入时顺便降压降频,层级上和 C1 同级。
所以一张现代服务器的状态列表里看到 POLL / C1 / C1E / C6,中间那些档位在硬件演进中从未被实现,或者早已被淘汰。
通过 sysfs 可以看到当前系统注册了哪些状态:
$ ls /sys/devices/system/cpu/cpu0/cpuidle/ state0/ state1/ state2/ state3/ $ cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name POLL C1 C1E C6 $ cat /sys/devices/system/cpu/cpu0/cpuidle/state2/latency 10 # 退出延迟,单位 µs
注意 state0/POLL 始终存在:它就是"什么都不做、原地轮询"的兜底状态,退出延迟为 0。
4. 核心:idle= 启动参数详解
x86 架构认三个合法取值(见 Documentation/admin-guide/kernel-parameters.rst),ARM64 另有三个。
4.1 idle=poll:彻底放弃节能,原地空转
idle=poll
行为:禁用 intel_idle 和 acpi_idle 驱动,等于关掉整个 cpuidle 子系统。空闲 CPU 在一个紧凑循环里执行 cpu_relax()(x86 上就是 PAUSE 指令),不断轮询 TIF_NEED_RESCHED 标志,一旦有任务就绪立刻抢过去。
- 优点:唤醒延迟最低(理论上零),性能计数器采样更"干净"(不会被 C-State 进出打断)。
- 代价:
- CPU 功耗大幅上升,发热、风扇、电费全部拉满;
- 在 Intel 平台上会挡住那些需要"package 内若干核空闲"才能达成的 P-State/Turbo 提频,单线程性能反而可能变差;
- 和超线程配合很差,空转的 sibling 会抢共享的物理核资源;
- 官方文档明确提示:在支持 MONITOR/MWAIT 的 CPU 上,
idle=poll相比默认 idle loop 没有性能优势。
- 适用场景:短时间的基准测试对比、特定的 profiling 场景、调试 idle 问题时拿来当对照组。不建议生产使用。
4.2 idle=halt:只用 HLT,拒绝深睡
idle=halt
行为:同样禁用 intel_idle/acpi_idle,但空闲时执行 HLT 指令,CPU 挂起执行直到下一个中断到达。硬件上相当于只进 C1,C2/C3 及更深的状态永远用不上。
- 优点:唤醒延迟稳定在 µs 级且可预期,没有深睡的"睡眠惯性";功耗仍显著低于 poll。
- 代价:放弃了 C6 级别的节能;中断风暴时会频繁进出。
- 适用场景:延迟敏感又不能接受 poll 功耗的场景,或者老平台上某些 C-State 有硬件 errata 时的规避手段。
4.3 idle=nomwait:禁用 MWAIT 指令
idle=nomwait
行为:三个选项里最精细的一个。它不禁用 cpuidle 框架本身,只禁止用 MONITOR/MWAIT 指令进入 idle 状态。具体效果分平台:
- Intel 平台:
intel_idle初始化失败,退回acpi_idle(前提是 ACPI 表信息完整),进入状态时改用HLT; - 各 C-State 的"名义层级"还在,但进入方式从 MWAIT 换成 HLT,硬件实际能睡多深受限。
- 适用场景:某些 CPU errata 导致 MWAIT 不可靠(历史上多个 Atom/Xeon 型号中过招),或者虚拟化环境里 MWAIT 被 hypervisor 拦截出问题时的规避。
4.4 ARM64 的 idle= 参数
ARM64 上取值不同,思路相通:
idle=wfi(默认):执行WFI指令,等中断,进低功耗状态;idle=yield:改用YIELD提示指令,不进低功耗状态,SMT 下把执行资源让给兄弟线程(可以看成 x86 poll 的耗电版);idle=nop:不执行任何 idle 指令,给 WFI 行为异常的平台做规避。
5. 相关参数与运行时控制手段
配套的工具还有这些:
启动参数
| 参数 | 作用 |
|---|---|
cpuidle.off=1 | 整体关掉 cpuidle 管理(idle loop 照跑,走架构默认机制,通常很粗糙,不推荐生产) |
cpuidle.governor=menu | 指定 governor |
intel_idle.max_cstate=N | intel_idle 丢弃比 N 更深的状态;=0 直接禁用 intel_idle |
processor.max_cstate=N | acpi_idle 同理(=0 等价于 =1) |
intel_idle.states_off=0x4 | 位掩码,默认禁用指定状态(可事后经 sysfs 打开) |
nohz=on/off | 动态时钟(dyntick idle)开关,影响空闲时的 tick 中断 |
rcu_nocbs=、irqaffinity= | 配合隔离 CPU 使用,减少 idle 核被打扰 |
运行时控制(无需重启)
# 禁用某个 CPU 的某个深度状态(延迟敏感进程运行前常用) echo 1 > /sys/devices/system/cpu/cpu3/cpuidle/state3/disable # 通过 PM QoS 从进程维度声明"我能容忍的最大唤醒延迟" # 打开 /dev/cpu_dma_latency 写入 0(µs),保持 fd 打开期间生效
后者更优雅:不用全局改内核参数,进程自己声明 QoS 需求,governor 选状态时会保证退出延迟不超过这个上限。tuned 的 latency-performance profile、real-time 内核的低延迟配置,底层都大量依赖这个接口。
6. 一次 Hygon 服务器性能回退背后的 ACPI C1 问题
6.1 现象与定位
事情是同事先发现的:一台 Hygon 服务器换上 6.6 内核后,I/O 性能掉了约 30%。回退到 5.x 内核,性能恢复。硬件没动,配置没动,变量只有内核版本,问题显然出在内核里。
在 6.6 上做 bisect,配合 A/B 测试,性能变化被定位到一个提交:
9e9b893404d4
ACPI: processor: idle:
Return an error if both P_LVL{2,3} idle states are invalid
Revert 掉重新构建内核,性能恢复。受影响机器上的 acpi_idle 没注册成功,idle driver 显示 none。
这个提交怎么就掐掉了 idle 驱动,得从 acpi_idle 的启动流程说起(细节见第 3 节):
- 先查每个 CPU 的 ACPI
_CST对象,从里面枚举可用的 C-State; - 查不到
_CST,就退到更老的 FADT 路径:读P_BLK寄存器块和 FADT 里的 C2/C3 latency 字段,能凑出多少状态算多少; - 固件连一个深状态都给不出来,只要
P_BLK还在,内核可以给 CPU 建一个"默认 HLT C1",至少保证 idle 时有个像样的休眠入口。这条路的开关是 Linux 5.8 的496121c02127(Allow probing on platforms with one ACPI C-state)。在那之前,"只提供了一个 C-State"本身就是失败条件:power.count要求至少 2,而且只有存在 C2/C3 时才置flags.power,只有 C1 的平台干脆不让acpi_idle注册。那个提交的动机其实更偏_CST侧,是"BIOS 关掉深 C-State 时_CST只返回一个状态",但它顺手把 FADT 路径的同一条限制也放宽了,本案走的正是后半段; - 以上任何一步走不通,
acpi_idle就注册失败,/sys/devices/system/cpu/cpuidle/current_driver显示none。
driver 显示 none 时 idle loop 仍在跑,只是落回架构默认路径,CPU 照样会执行休眠指令。有没有驱动的行为差异会反映到具体 workload 的性能上。
对照上面的流程,问题收敛成一个具体的点:这台机器的 FADT 路径在第 2 步和第 3 步之间被掐断了。
6.2 根因:一次为虚拟机设计的检查,误伤了裸机
这个提交针对的是 VMware ESXi 一类虚拟机:这类虚拟机通常没有 _CST,FADT 里 C2 latency 填 101 µs、C3 latency 填 1001 µs。这两个数字看着像实测延迟,其实不是。ACPI 规范约定,C2 latency 超过 100 µs、C3 latency 超过 1000 µs 就表示该状态不可用,101 和 1001 正好是刚过线的最小整数,固件在用它们说"C2、C3 我没有"。上游认为这种机器不该由内核自建默认 C1 并加载 acpi_idle,于是加了拒绝检查:
if (!pr->power.states[ACPI_STATE_C2].address &&
!pr->power.states[ACPI_STATE_C3].address)
return -ENODEV;
提交进入 Linux 6.15,并回移到了 6.6.87。对比源码可以确认 6.6.86 还没有这段逻辑。这就解释了版本现象:5.x 内核一切正常,6.6(含该回移)开始回退。
这台 Hygon 的固件信息和上游要处理的虚拟机"撞脸"了:
- DSDT/SSDT 中没有
_CST; - Processor
P_BLK为0x810,长度 6; - FADT 里 C2 latency 为 101 µs,C3 latency 为 1001 µs,和虚拟机一模一样的"不支持"声明。
这不能说明 BIOS 填错了。_CST 在 ACPI 里是可选对象,固件可以不提供;FADT/P_BLK 虽然老,内核一直当兼容路径支持。从启动日志看,这台机器所有 CPU 走的都是 FADT discovery,于是同样撞上了那个拒绝检查。失败链是这样:
_CST不可用,退到 FADT 路径;- C2/C3 latency 超限(101 / 1001),对应地址被清零;
- 新加的拒绝检查命中,FADT 返回
-ENODEV; - 提前退出,默认 HLT C1 没有建立;
acpi_idle注册失败,driver 显示none。
把 C2/C3 标记为无效是对的,固件明确说了不支持。真正值得讨论的是该不该因此连默认 C1 也一起拒绝。对虚拟机,上游的答案是可以拒绝;对这台裸机,拒绝等于让 idle 行为整个变了样,30% 的 I/O 回退正是由此而来。这个判断只看了"固件长什么样",没看"它运行在什么平台上",于是把两类机器一起处理了。
6.3 从"进不了 C1"到"I/O 慢 30%"
就算 acpi_idle 没注册,idle loop 不还跑着吗?CPU 不还是会 HLT 吗?为什么会慢 30%?
- 5.x 内核:
acpi_idle注册成功,idle 走 cpuidle 框架,状态是 POLL + 默认 C1; - 6.6 内核:注册失败,idle 走
do_idle()里的架构默认路径(default_idle),没有框架、没有状态。这条路径依然会执行休眠指令让核停下来,CPU 并不原地空转。
两条路径都能让 CPU 停止执行,但进入方式和退出延迟不同。落到我们的 workload 上,主导因素大概率是频率。
我们的 I/O 由几个 polling reactor 核处理,这几个核 100% 占满、从不进入 idle,能跑多快取决于 package 的功耗与温度预算还剩多少。其他核空闲时进入 C1 做时钟门控,省下的功耗预算可以让 polling 核提到更高频率,第 4 节提到 idle=poll 会抑制 Turbo,就是这条链路的反面。注册失败后空闲核进不了 C1,这部分功耗省不下来,polling 核的可用频率被压低,IOPS 随之下降。
6.4 修复:只为目标裸机保留 C1 fallback
修复加在 FADT 最后的拒绝条件里:
if (!acpi_processor_allow_c1_fallback() &&
!pr->power.states[ACPI_STATE_C2].address &&
!pr->power.states[ACPI_STATE_C3].address)
return -ENODEV;
放行条件限定为裸机 Hygon,原来的边界全部保留:_CST → FADT 的顺序不变;C2/C3 仍然无效、不会被重新启用;缺 P_BLK 等前置错误照旧返回;虚拟机和非 Hygon 平台保持拒绝行为;目标裸机则可以继续建立默认 HLT C1。
修复后的运行状态:
current_driver: acpi_idle max_cstate: 8 states: POLL C1
128 个 CPU 都只有这两个状态,C1 使用计数持续增长。max_cstate=8 是上限,系统不一定真的提供更深的状态。这台机器的 C2/C3 已被固件标记为不支持,实际只会得到默认 C1。
7. 虚拟化场景:idle 行为遇到 hypervisor 之后
7.1 Guest 内的 idle 选择:haltpoll 与 CPUID 的门控作用
直觉上,guest 检测到自己在虚拟机里就该老老实实用 hlt 让核。Linux 并没有这个逻辑,实际机制是两层:
第一层:检测到 KVM 后,切换的是 cpuidle 驱动(haltpoll)
- 内核通过
kvm_para_available()(CPUID 里的 KVM 准虚拟化签名)发现自己跑在 KVM 上,且编译了CONFIG_HALTPOLL_CPUIDLE(x86 默认 y)时,自动注册 haltpoll driver + governor; - haltpoll 的行为:idle 后先在 guest 内自旋一小段时间(
guest_halt_poll_ns,上限默认 200µs),没等到唤醒事件才执行 hlt,是"快路径免 VM exit、慢路径再让核"的折中; - upstream 默认还要求宿主机通过
KVM_HINTS_REALTIME(libvirt 的hint-dedicated)声明 vCPU 独占物理核才启用,否则要用cpuidle_haltpoll.force=Y强开。内核社区认为"guest 内自旋"只适合独占核场景。
第二层:idle 指令本身(hlt 还是 mwait)由 CPUID 决定,与"是不是 guest"无关
KVM 默认不把 MONITOR/MWAIT 通告给 guest。CPUID leaf 5 在 KVM 内部直接返回全零,MWAIT 特性位被标成"已知但故意不暴露"。源码注释说得直白:
MONITOR (and MWAIT) are emulated as NOP, but not advertised to guests via CPUID!
道理也简单:KVM 把 MWAIT 模拟成 NOP,忠实模拟做不到;让 guest 原生执行,又等于把"pCPU 睡多深"的决定权交给了 guest。所以默认不给。
于是 guest 里到底跑 hlt 还是 mwait,取决于 VMM 有没有显式打开:
- 默认 QEMU/KVM 下 guest 拿不到这个 flag,
intel_idle根本不会用 MWAIT,走 hlt; - 要暴露它,宿主机侧得开 per-VM capability(
KVM_CAP_X86_DISABLE_EXITS的KVM_X86_DISABLE_EXITS_MWAIT)。而这个 capability 一开,拦截也一并关掉了。在 KVM 里,"暴露 mwait"和"直通 mwait"是同一件事,没有"只让 guest 看见但不让它真跑"这个中间态。 - 公有云上常见的是宿主机想压低独占核的唤醒延迟,主动开了直通,这时 guest 照常枚举 C-State,日常就在用 mwait。mwait-sched 论文研究的就是这类环境。
想确认自己手上的实例属于哪种,guest 里 grep mwait /proc/cpuinfo 最直接。
7.2 KVM 对 hlt / mwait 的处理:三种模式
| Guest 执行 | KVM 默认行为 | 调度后果 |
|---|---|---|
hlt | 无条件 VM exit → kvm_vcpu_halt 挂起 vCPU 线程(挂起前 host 侧会先做 halt polling,halt_poll_ns) | 显式让核,pCPU 可以分给别人,这是超卖下的"合作"路径 |
monitor/mwait | 默认拦截并模拟为 NOP(KVM_X86_QUIRK_MWAIT_NEVER_UD_FAULTS 默认开) | guest 的 idle 循环退化成自旋,vCPU 线程永不阻塞、持续烧 pCPU |
mwait(直通) | 配置 KVM_X86_DISABLE_EXITS_MWAIT 后不拦截,原生执行 | 零 exit、裸机级唤醒延迟,但空闲对 host 调度器完全不可见 |
于是出现了责任真空:guest 用什么 idle 指令,由 VMM 给的 CPUID 说了算;而 KVM 这边,没暴露 mwait 时它把 guest 盲目执行的 mwait 当 NOP 抹掉,暴露了(等于直通)它又彻底看不见空闲。两条路都不让核,这正是 mwait-sched 要补的窟窿。
7.3 生产观察:mwait-sched 论文
上海交大与阿里云的这篇论文系统研究了这个问题:
Yun Wang, Xingguo Jia, Ben Luo, Kenan Liu, Shengdong Dai, et al. What Are You (M)Waiting For: The Hidden Cost of Idle in the Hyperscale Cloud. OSDI '26 (20th USENIX Symposium on Operating Systems Design and Implementation), Shanghai Jiao Tong University & Alibaba Cloud. 论文页面:https://www.usenix.org/conference/osdi26/presentation/wang-yun
几个关键观察:
- 1:1 独占场景,mwait 直通很合适:guest 直接驱动硬件 idle 转换,消除 idle 引起的 VM exit,延迟接近裸机。
- 超卖场景,同一个机制直接崩坏。裸机上 mwait 是真正的阻塞原语(进 C-State、放弃执行权),但硬件级的阻塞无法被 hypervisor 观测。vCPU 从不让出 pCPU,空闲 vCPU 霸占核心,抬高整机的争抢、steal time、热迁移次数和 SLO 告警。受控实验复现:哪怕只是一个执行 mwait 的空闲 vCPU,也能把同核邻居的尾延迟拉高到 3 倍。
- 利用率与干扰是脱钩的。论文分析了三个生产 region(共约 320 万 pCPU):平均利用率很低,但超卖比例被死死压在约 1%。卡住它的不是 CPU 数量,再多卖就会触发争抢、违反延迟 SLO。按这个体量算,超卖比例每提高 1 个百分点,就多出约 3.2 万个可售 vCPU。
- 修复思路(mwait-sched):由 hypervisor 单方面补课,guest 不用改。三招:确定性定时器模拟(定时夺回控制权,区分 CPU 忙与 idle)、细粒度 idle 区间分类(mwait 空闲时长呈双峰分布,只聚合"稳定空闲"的 vCPU)、多地址 mwait-proxy(hypervisor 侧维护被监控地址链表,每次进入 hypervisor 时扫描,免去每 vCPU 定时器)。
- 效果:九个代表性负载 P99 延迟降 30–50%、steal ratio 降 30–40%;生产环境高争抢 steal 事件减少 80%+、日均热迁移降 30–50%、超卖比例从约 1% 提到 20.3%,相当于新增约 60 万个可售 vCPU。
同一个机制在两种环境下结果相反:1:1 独占时消除 VM exit、延迟接近裸机,超卖时放大争抢、把邻居尾延迟推高到 3 倍。guest 里看到的 steal time(top 里的 st),来源也可能在这条链路上,而不一定在 guest 自己的负载里。
8. 延迟 vs 功耗:怎么选
一个简化的决策思路:
- 默认就好:绝大多数场景下,
intel_idle+menugovernor 的预测已经相当准,深睡带来的 Turbo 预算收益甚至能反哺性能。 - 延迟敏感业务:先用
/dev/cpu_dma_latency或 sysfsdisable做定点限制,而不是全局idle=poll;再配合isolcpus+nohz_full+rcu_nocbs做核隔离。 - 基准测试 / profiling:
idle=poll可以消除 C-State 进出对性能计数器的干扰,但记得测完改回来。 - 怀疑硬件 errata:表现为随机卡顿、唤醒失败时,试
idle=nomwait或收紧intel_idle.max_cstate。
观测工具:
# 看每个核实际在各 C-State 的驻留比例(最直观) turbostat --interval 5 # 看 idle 统计与唤醒源 powertop # 看每个状态的命中次数和累计驻留时长 cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage cat /sys/devices/system/cpu/cpu0/cpuidle/state*/time # 直接读硬件的 C-State 驻留计数器(Intel,名字以 perf list 为准) perf stat -a -e 'power/cstate_core/c6-residency/' -- sleep 5 # 跟踪 idle 进出与唤醒延迟 perf sched record -- sleep 5
如果 turbostat 显示 CPU% 很低但 C6 驻留率接近 0,说明有东西在频繁唤醒(定时器、中断、用户态轮询),这比调整 idle 参数更值得先查。
9. 总结
睡太死也不行,不睡觉也不行,适度睡觉对身体好,身体是革命的本钱
参考
内核文档
- CPU Idle Time Management(admin-guide/pm/cpuidle)
- intel_idle CPU Idle Time Management Driver(admin-guide/pm/intel_idle)
- The kernel's command-line parameters(
idle=条目) - The Definitive KVM API Documentation(MWAIT 相关 quirk 与 exit 控制)
上游提交
496121c02127— ACPI: processor: idle: Allow probing on platforms with one ACPI C-state(Linux 5.8)9e9b893404d4— ACPI: processor: idle: Return an error if both P_LVL{2,3} idle states are invalid(Linux 6.15,回移 6.6.87)
邮件列表讨论(前两条 AMD 改动的出处)
- ACPI: processor_idle: Skip dummy wait for processors based on the Zen microarchitecture — Prateek Nayak(2022),含 tbench 对照数据
- x86: Prefer MWAIT over HLT on AMD processors (v3) — Wyes Karny(2022),含 Zen 3 退出延迟实测
论文
- Yun Wang, Xingguo Jia, et al. What Are You (M)Waiting For: The Hidden Cost of Idle in the Hyperscale Cloud. OSDI '26, SJTU & Alibaba Cloud. https://www.usenix.org/conference/osdi26/presentation/wang-yun
讨论
邮箱用于身份识别和回复通知,并由 Waline 存储。