〇、全景:CPU 没活干时,不是"傻转",而是钻进一层层更深的省电状态
上一系列讲的是"怎么把 CPU 拉起来"(BSP/AP/hotplug)和"CPU 之间怎么喊话"(IPI)。但 CPU 大部分时间其实没活干——等待磁盘、等待网络、等待用户敲键盘。这时候如果让 CPU 空转(while(1)),功耗白白烧掉。
所以 x86 设计了一套C-state(CPU 空闲状态):CPU 空闲时进入越来越深的省电状态,深度越深越省电,但"醒过来"也越慢。而 Linux 用cpuidle 框架来管理这套状态——核心问题是:这一轮空闲会持续多久?该钻进多深的 C-state?
一句话主线:cpuidle 是"CPU 怎么省电"的状态机——idle 线程没活干时,先由governor 预测这轮空闲会持续多久,据此在 driver 提供的 C-state 列表里选一个"足够省电、又不会醒得太慢"的深度,然后通过mwait(或老的 hlt)指令真正钻进去,等中断来唤醒。
一、预备概念:C-state 与"三个状态机"
1.1 C-state:越深越省电,醒得越慢
x86 的 C-state 是一组功耗逐级降低、唤醒延迟逐级升高的空闲状态:
| 状态 | 名称 | 功耗 | 唤醒延迟 | 说明 |
|---|---|---|---|---|
| C0 | 运行态 | 最高 | 0 | 正常执行指令 |
| C1 | Halt | 低 | 极短 | 停时钟(hlt),几乎无副作用 |
| C1E | Enhanced Halt | 更低 | 短 | 额外降频降电压 |
| C2 | Stop Grant | 更低 | 较长 | 停部分时钟 |
| C6 | Deep Power Down | 很低 | 长 | 关核心电源,flush 缓存 |
关键规律:深度越深,省电越多,但"进入 + 退出"的代价越大。C6 能把核心电源都关掉,但退出要重新上电、恢复缓存,延迟可达几十微秒。所以"进多深"是个权衡——这正是 cpuidle 框架要解决的核心问题。
1.2 进入 C-state 的硬件手段:hltvsmwait
x86 提供两条指令让 CPU 进 C-state:
hlt:最老的空闲指令,让 CPU 停在 C1,等中断唤醒。简单但只能进 C1。monitor+mwait:更先进。monitor先设置一个监视地址,mwait让 CPU 进入 C-state(可带一个"hint"指定深度,如 C1/C1E/C6),当地址被写、或中断到来时自动唤醒。能进更深的 C-state,是现代 CPU 的首选。
1.3 三个"状态机"别搞混
写到这里必须钉死三个粒度的区别(这也是这个系列一直强调的):
| 状态机 | 管什么 | 核心问题 | 对应 |
|---|---|---|---|
CPU hotplug(cpuhp_state) | 怎么把 CPU拉起来/拆掉 | offline → online 的几十步 | CPU #3 hotplug |
| cpuidle(C-state) | CPU空闲时怎么省电 | 进多深的 C-state | 本篇 |
| cpufreq(P-state) | CPU跑多快(调频) | 多高的频率 | 下一篇 |
三者都是"状态",但 hotplug 是"存在与否"、cpuidle 是"睡多深"、cpufreq 是"跑多快"。别把cpuhp_state和 cpuidle 的 C-state 混为一谈。
二、cpuidle 框架三层:driver / device / state
cpuidle 框架把"进 C-state"这件事拆成三层,各管一段:
2.1cpuidle_state:一个 C-state 的参数
每个 C-state 用一个cpuidle_state描述,最关键的是两个时间参数:
// include/linux/cpuidle.h (v6.6, line 49)structcpuidle_state{charname[CPUIDLE_NAME_LEN];chardesc[CPUIDLE_DESC_LEN];s64 exit_latency_ns;// 退出这个 state 的延迟(多久能醒)s64 target_residency_ns;// 要停留多久才"划算"(能量回本)unsignedintflags;unsignedintexit_latency;// 旧版(us),已废弃intpower_usage;// 功耗(mW)unsignedinttarget_residency;// 旧版(us),已废弃int(*enter)(structcpuidle_device*dev,structcpuidle_driver*drv,intindex);// 真正钻进去的回调// …};exit_latency_ns:从这个 state 醒过来要多久。决定"会不会醒得太慢"。target_residency_ns:要在这个 state 里停留多久,省的能源才抵得上进出的开销(能量盈亏平衡点)。决定"值不值得进"。
这两个参数是理解 governor 选 state 的关键(见第三节)。
2.2cpuidle_driver:这台 CPU 有哪些 state
driver 提供一组 state(每台 CPU 型号不同),按功耗递减排序:
// include/linux/cpuidle.h (v6.6, line 152)structcpuidle_driver{constchar*name;/* states array must be ordered in decreasing power consumption */structcpuidle_statestates[CPUIDLE_STATE_MAX];// state 数组intstate_count;// 有几个 stateintsafe_state_index;// 最安全的浅 statestructcpumask*cpumask;constchar*governor;};注意注释states array must be ordered in decreasing power consumption——state 数组按功耗递减排序,即states[0]功耗最高(通常是 polling 或 C1),越往后越省电(C6 等),也越深。x86 上有两个 driver:老的acpi_idle(读 ACPI_CST表)和现代的intel_idle(直接用 mwait,见第五节)。
2.3cpuidle_device:每 CPU 的实例 + 统计
driver 是共享的(同型号 CPU 一份),device 是每 CPU 一份:
// include/linux/cpuidle.h (v6.6, line 93)structcpuidle_device{unsignedintregistered:1;unsignedintenabled:1;unsignedintcpu;ktime_tnext_hrtimer;intlast_state_idx;// 上次进了哪个 stateu64 last_residency_ns;// 上次实际停留了多久structcpuidle_state_usagestates_usage[CPUIDLE_STATE_MAX];// 每个 state 的统计// …};states_usage[]记录每个 state 的使用次数、实际停留时间、以及"选深了/选浅了"的次数(above/below,见 4.3)——这些统计正是 governor 校正自己预测的依据。
2.4cpuidle_governor:谁来决定进哪个 state
// include/linux/cpuidle.h (v6.6, line 288)structcpuidle_governor{charname[CPUIDLE_NAME_LEN];unsignedintrating;// 评分,高的优先int(*select)(structcpuidle_driver*drv,structcpuidle_device*dev,bool*stop_tick);// 选 statevoid(*reflect)(structcpuidle_device*dev,intindex);// 反馈};governor 有两个动作:select(选 state)和reflect(拿到实际停留时长后做校正)。v6.6 默认是menu governor(第三节),老的ladder已基本不用。
三、menu governor:怎么预测"这轮空闲会持续多久"
menu governor 的核心是预测 idle 时长,然后选一个"停留时间回本、又不会醒太慢"的 state。它自己的注释(menu.c:31-109)把决策因素总结成三个:
3.1 三个决策因素
- 能量盈亏平衡点(Energy break even):进/出 C-state 有能量开销,得停留足够久才划算——这个时长就是
target_residency。所以关键就是预测空闲时长。 - 性能影响(Performance impact):深 C-state 退出延迟大,会拖慢工作负载。越忙的系统越不能接受深的 state。
- 延迟容忍度(Latency tolerance):来自 pmqos 基础设施,用户/驱动可以声明"我能接受多长的延迟"。
3.2 怎么预测 idle 时长:两个预测器
menu 用一个predicted_ns作为预测,它来自两个预测器取最小值:
预测器 1:下一个定时器事件(next_timer_ns)× 校正因子
// drivers/cpuidle/governors/menu.c (v6.6, line 292)data->next_timer_ns=delta;// 最近的定时器/时钟事件data->bucket=which_bucket(data->next_timer_ns,nr_iowaiters);// 用历史校正因子修正这个估计timer_us=div_u64((RESOLUTION*DECAY*NSEC_PER_USEC)/2+data->next_timer_ns*data->correction_factor[data->bucket],RESOLUTION*DECAY*NSEC_PER_USEC);predicted_ns=min((u64)timer_us*NSEC_PER_USEC,predicted_ns);为什么需要校正因子?因为唤醒 CPU 的不只有定时器,还有中断。所以"下一个定时器"的估计偏乐观(实际往往更早被中断唤醒)。menu 用历史数据算一个校正因子(实际空闲时长 / 下一个定时器的比例),按"时长量级 + 是否有 IO 在等"分12 个 bucket分别维护。
预测器 2:重复间隔检测器(get_typical_interval)
// drivers/cpuidle/governors/menu.c (v6.6, line 171)staticunsignedintget_typical_interval(structmenu_device*data)有些场景"下一个定时器"完全不可用——比如鼠标、网络包这种固定间隔的硬件事件。menu 记录最近 8 次空闲间隔,如果这 8 次的标准差很小(很稳定),就用平均值作为预测。
3.3menu_select:选一个"最深但划算"的 state
拿到predicted_ns后,menu_select遍历 state 数组,选最深的、且满足两个约束的:
// drivers/cpuidle/governors/menu.c (v6.6, line 353)for(i=0;i<drv->state_count;i++){structcpuidle_state*s=&drv->states[i];if(dev->states_usage[i].disable)continue;if(idx==-1)idx=i;/* first enabled state */if(s->target_residency_ns>predicted_ns){// 停留时间回不了本,这个 state 太深了,break// …(细节略)}if(s->exit_latency_ns>latency_req)break;// 退出延迟超过容忍度,太深了idx=i;// 满足约束,继续往深处找}两个 break 条件正好对应两个参数:
target_residency_ns > predicted_ns:预测空闲不够长,停留回不了本 → 太深了,停。exit_latency_ns > latency_req:退出延迟超过延迟容忍度 → 会醒得太慢,停。
而latency_req还会被performance_multiplier进一步收紧(menu.c:155):
staticinlineintperformance_multiplier(unsignedintnr_iowaiters){/* for IO wait tasks (per cpu!) we add 10x each */return1+10*nr_iowaiters;}实际是predicted_ns除以multiplier 得到interactivity_req,然后latency_req = min(latency_req, interactivity_req)(menu.c:343)。所以nr_iowaiters越多(越忙),乘数越大,latency_req越可能被压到predicted_ns / multiplier这个更小的值,越难选到深的 state——这就是"越忙越不能睡深"的实现。
四、进入/退出流程:从 idle 循环到 mwait
4.1do_idle:idle 线程的主循环
每个 CPU 的 idle 线程在do_idle(kernel/sched/idle.c:237)里无限循环,核心是:
// kernel/sched/idle.c (v6.6, line 258)while(!need_resched()){rmb();local_irq_disable();// …if(cpu_idle_force_poll||tick_check_broadcast_expired()){tick_nohz_idle_restart_tick();cpu_idle_poll();// 轮询模式(不进 C-state)}else{cpuidle_idle_call();// 走 cpuidle 框架}// …}每次循环:关中断 → 调cpuidle_idle_call进 C-state(阻塞直到被唤醒)→ 醒来后继续判断有没有活干。
4.2cpuidle_idle_call:一次完整的"选 + 进 + 反馈"
// kernel/sched/idle.c (v6.6, line 146)staticvoidcpuidle_idle_call(void){structcpuidle_device*dev=cpuidle_get_device();structcpuidle_driver*drv=cpuidle_get_cpu_driver(dev);intnext_state,entered_state;if(need_resched()){// 又有活了,别进 idlelocal_irq_enable();return;}if(cpuidle_not_available(drv,dev)){default_idle_call();// 没有 cpuidle,兜底走 arch_cpu_idlegotoexit_idle;}// …next_state=cpuidle_select(drv,dev,&stop_tick);// ① governor 选 state// …entered_state=call_cpuidle(drv,dev,next_state);// ② 进入 statecpuidle_reflect(dev,entered_state);// ③ 反馈给 governor}三步:选(select)→ 进(enter)→ 反馈(reflect)。cpuidle_select只是转调 governor 的select(cpuidle.c:356)。
4.3cpuidle_enter_state:真正钻进去 + 事后统计
cpuidle_enter(cpuidle.c:372)→cpuidle_enter_state(cpuidle.c:211)是进入 state 的核心,也是"选深了还是选浅了"的统计发生地:
// drivers/cpuidle/cpuidle.c (v6.6, line 246)time_start=ns_to_ktime(local_clock_noinstr());// …entered_state=target_state->enter(dev,drv,index);// ← 真正钻进去(mwait)// …(醒来后)time_end=ns_to_ktime(local_clock_noinstr());// …diff=ktime_sub(time_end,time_start);// 实际停留了多久dev->last_residency_ns=diff;dev->states_usage[entered_state].time_ns+=diff;dev->states_usage[entered_state].usage++;if(diff<drv->states[entered_state].target_residency_ns){// 停留比预期短 → 这次"选深了"(above++)dev->states_usage[entered_state].above++;}elseif(diff>delay){// 停留足够久,也许更深的 state 更合适 → 这次"选浅了"(below++)dev->states_usage[entered_state].below++;}关键:进入 state 前后的时间差diff就是"实际空闲时长"。它被用来:
- 更新
states_usage[]的统计(time_ns、usage); - 标记"选深了"(
above,停留 <target_residency)还是"选浅了"(below,停留 >exit_latency且够得上更深的 state)。
这些above/below统计会在下次menu_update(menu.c:461)里被读出来,用于校正预测因子——这正是 cpuidle 反馈闭环的落点。
五、x86 底层:hlt与mwait
前面讲的都是框架,最后落地的还是 x86 的两条指令。
5.1default_idle(hlt)与mwait_idle(monitor/mwait)
// arch/x86/kernel/process.c (v6.6, line 740)void__cpuidledefault_idle(void){raw_safe_halt();// hlt 指令,进 C1raw_local_irq_disable();}mwait_idle用的是 monitor/mwait 指令对:
// arch/x86/kernel/process.c (v6.6, line 918)static__cpuidlevoidmwait_idle(void){if(!current_set_polling_and_test()){__monitor((void*)¤t_thread_info()->flags,0,0);// 监视 flags 地址if(!need_resched()){__sti_mwait(0,0);// mwait 进 C-state(hint=0 即 C1)raw_local_irq_disable();}}__current_clr_polling();}arch_cpu_idle(process.c:777)通过 static_call 间接调用实际例程,select_idle_routine(process.c:936)在启动时决定用 mwait 还是 hlt。
5.2intel_idle:用 mwait 的 hint 指定 C-state 深度
hlt/mwait_idle只能进浅的 C1。要进更深的 C-state(C1E/C6),得用intel_idledriver——它用 mwait 的 hint 参数指定深度:
// drivers/idle/intel_idle.c (v6.6, line 124)/* * MWAIT takes an 8-bit "hint" in EAX "suggesting" * the C-state (top nibble) and sub-state (bottom nibble) * 0x00 means "MWAIT(C1)", 0x10 means "MWAIT(C2)" etc. */#defineflg2MWAIT(flags)(((flags)>>24)&0xFF)// 从 flags 取 hint#defineMWAIT2flg(eax)((eax&0xFF)<<24)static__cpuidleintintel_idle(structcpuidle_device*dev,structcpuidle_driver*drv,intindex){structcpuidle_state*state=&drv->states[index];unsignedlongeax=flg2MWAIT(state->flags);// 这个 state 对应的 hintunsignedlongecx=1;/* break on interrupt flag */mwait_idle_with_hints(eax,ecx);// mwait,eax = C-state hintreturnindex;}mwait 的 hint 用一个字节表示,高 nibble 是主状态、低 nibble 是子状态:0x00=C1、0x01=C1E、0x10=C2、0x20=C6…… 这些 hint 编码在intel_idle的 state 表里(intel_idle.c:237起),比如 C6 对应MWAIT2flg(0x20),且带CPUIDLE_FLAG_TLB_FLUSHED(C6 会 flush TLB)。
所以整条链是:governor 选中 C6 → cpuidle_enter_state 调state->enter(= intel_idle)→mwait(0x20)钻进去。
六、为什么这样设计(Why 层)
6.1 为什么要有 governor,而不是一个固定阈值
“进多深"没有固定答案——这轮空闲可能 1 微秒(马上有活)也可能 100 毫秒(等磁盘)。固定阈值要么"太保守”(空闲长却只进 C1,浪费电),要么"太激进"(空闲短却钻进 C6,醒来慢还 flush 了 TLB)。所以必须预测。governor 就是那个预测者,而预测不可能完美,于是又有了above/below统计 + 校正因子的反馈闭环(4.3 → 3.2),让预测越用越准。
6.2 为什么是exit_latency和target_residency两个参数,不是一个
这两个参数回答了两个不同的问题:
target_residency回答"值不值":停留够久能量才回本,否则进出的能量开销比省的还多。exit_latency回答"允不允许":即使能量回本,如果醒来太慢拖累了关键路径(延迟敏感),也不能进。
一个是经济账(能量),一个是性能账(延迟)。menu 的两个 break 条件(3.3)正好对应这两本账。
6.3 为什么 mwait 比 hlt 好
hlt只能进 C1,且每次唤醒都要走完整的中断流程。mwait有三个优势:
- 能进更深的 state:hint 参数指定 C1E/C6 等,省电空间大得多;
- monitor 提供"地址监视":可以在进入前检查监视地址,避免"刚要睡就来了活"的竞态(
mwait_idle里__monitor后need_resched再查一次); - 自动唤醒:mwait 在"监视地址被写或中断到来"时自动醒来,不需要软件参与。
6.4 为什么"选深了/选浅了"要单独统计(above/below)
预测本质是对未来的猜测,总会错。above(选深了,实际停留 < target_residency)和below(选浅了)把错误分类记录下来,喂给 governor 的校正因子。这是整个 cpuidle 的精髓——它不追求一次预测准,而是靠反馈闭环让预测逐步收敛。没有这个统计,menu 就只是个"猜下一个定时器"的朴素预测器。
七、与相邻主题的边界
| 主题 | 边界 |
|---|---|
| CPU hotplug(#3) | hotplug 的cpuhp_state管"CPU 存不存在",cpuidle 的 C-state 管"空闲睡多深"——两套状态机,别混 |
| 中断与 IPI | 中断是唤醒idle 的手段(mwait 因中断而醒),IPI 里 reschedule 一类正是把睡着的 CPU 叫醒 |
| cpufreq(下一篇) | cpuidle 管"睡多深"(C-state),cpufreq 管"跑多快"(P-state 调频) |
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
cpuidle_state(exit_latency / target_residency / enter) | include/linux/cpuidle.h:49 |
cpuidle_driver(state 数组按功耗递减) | include/linux/cpuidle.h:152 |
cpuidle_device(per-CPU + states_usage 统计) | include/linux/cpuidle.h:93 |
cpuidle_governor(select / reflect) | include/linux/cpuidle.h:288 |
cpuidle_idle_call(选 → 进 → 反馈) | kernel/sched/idle.c:146 |
do_idle(idle 主循环) | kernel/sched/idle.c:237 |
cpuidle_select/cpuidle_enter/cpuidle_reflect | drivers/cpuidle/cpuidle.c:356/:372/:402 |
cpuidle_enter_state(进入 + above/below 统计) | drivers/cpuidle/cpuidle.c:211 |
menu_select(预测 + 选 state) | drivers/cpuidle/governors/menu.c:262 |
get_typical_interval/performance_multiplier | drivers/cpuidle/governors/menu.c:171/:155 |
default_idle(hlt) /mwait_idle(mwait) | arch/x86/kernel/process.c:740/:918 |
intel_idle(mwait hint 指定深度) | drivers/idle/intel_idle.c:159 |