Linux 6.6内核 CPU 深度解析(五):CPU 空闲状态机 cpuidle

发布时间:2026/10/4 6:34:26
Linux 6.6内核 CPU 深度解析(五):CPU 空闲状态机 cpuidle
〇、全景CPU 没活干时不是傻转而是钻进一层层更深的省电状态上一系列讲的是怎么把 CPU 拉起来BSP/AP/hotplug和CPU 之间怎么喊话IPI。但 CPU 大部分时间其实没活干——等待磁盘、等待网络、等待用户敲键盘。这时候如果让 CPU 空转while(1)功耗白白烧掉。所以 x86 设计了一套C-stateCPU 空闲状态CPU 空闲时进入越来越深的省电状态深度越深越省电但醒过来也越慢。而 Linux 用cpuidle 框架来管理这套状态——核心问题是这一轮空闲会持续多久该钻进多深的 C-statedo_idle 循环idle 线程没活干cpuidle_selectgovernor 选一个 C-statemenu governor 预测这次空闲会持续多久cpuidle_enter进入选中的 C-statestate-enterintel_idle → mwait中断到来 → 唤醒回到运行态一句话主线cpuidle 是CPU 怎么省电的状态机——idle 线程没活干时先由governor 预测这轮空闲会持续多久据此在 driver 提供的 C-state 列表里选一个足够省电、又不会醒得太慢的深度然后通过mwait或老的 hlt指令真正钻进去等中断来唤醒。一、预备概念C-state 与三个状态机1.1 C-state越深越省电醒得越慢x86 的 C-state 是一组功耗逐级降低、唤醒延迟逐级升高的空闲状态状态名称功耗唤醒延迟说明C0运行态最高0正常执行指令C1Halt低极短停时钟hlt几乎无副作用C1EEnhanced Halt更低短额外降频降电压C2Stop Grant更低较长停部分时钟C6Deep Power Down很低长关核心电源flush 缓存关键规律深度越深省电越多但进入 退出的代价越大。C6 能把核心电源都关掉但退出要重新上电、恢复缓存延迟可达几十微秒。所以进多深是个权衡——这正是 cpuidle 框架要解决的核心问题。1.2 进入 C-state 的硬件手段hltvsmwaitx86 提供两条指令让 CPU 进 C-statehlt最老的空闲指令让 CPU 停在 C1等中断唤醒。简单但只能进 C1。monitormwait更先进。monitor先设置一个监视地址mwait让 CPU 进入 C-state可带一个hint指定深度如 C1/C1E/C6当地址被写、或中断到来时自动唤醒。能进更深的 C-state是现代 CPU 的首选。1.3 三个状态机别搞混写到这里必须钉死三个粒度的区别这也是这个系列一直强调的状态机管什么核心问题对应CPU hotplugcpuhp_state怎么把 CPU拉起来/拆掉offline → online 的几十步CPU #3 hotplugcpuidleC-stateCPU空闲时怎么省电进多深的 C-state本篇cpufreqP-stateCPU跑多快调频多高的频率下一篇三者都是状态但 hotplug 是存在与否、cpuidle 是睡多深、cpufreq 是跑多快。别把cpuhp_state和 cpuidle 的 C-state 混为一谈。二、cpuidle 框架三层driver / device / statecpuidle 框架把进 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;// 功耗mWunsignedinttarget_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 有哪些 statedriver 提供一组 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_nsdelta;// 最近的定时器/时钟事件data-bucketwhich_bucket(data-next_timer_ns,nr_iowaiters);// 用历史校正因子修正这个估计timer_usdiv_u64((RESOLUTION*DECAY*NSEC_PER_USEC)/2data-next_timer_ns*data-correction_factor[data-bucket],RESOLUTION*DECAY*NSEC_PER_USEC);predicted_nsmin((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(i0;idrv-state_count;i){structcpuidle_state*sdrv-states[i];if(dev-states_usage[i].disable)continue;if(idx-1)idxi;/* first enabled state */if(s-target_residency_nspredicted_ns){// 停留时间回不了本这个 state 太深了break// …细节略}if(s-exit_latency_nslatency_req)break;// 退出延迟超过容忍度太深了idxi;// 满足约束继续往深处找}两个 break 条件正好对应两个参数target_residency_ns predicted_ns预测空闲不够长停留回不了本 → 太深了停。exit_latency_ns latency_req退出延迟超过延迟容忍度 → 会醒得太慢停。而latency_req还会被performance_multiplier进一步收紧menu.c:155staticinlineintperformance_multiplier(unsignedintnr_iowaiters){/* for IO wait tasks (per cpu!) we add 10x each */return110*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 循环到 mwait4.1do_idleidle 线程的主循环每个 CPU 的 idle 线程在do_idlekernel/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*devcpuidle_get_device();structcpuidle_driver*drvcpuidle_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_statecpuidle_select(drv,dev,stop_tick);// ① governor 选 state// …entered_statecall_cpuidle(drv,dev,next_state);// ② 进入 statecpuidle_reflect(dev,entered_state);// ③ 反馈给 governor}三步选select→ 进enter→ 反馈reflect。cpuidle_select只是转调 governor 的selectcpuidle.c:356。4.3cpuidle_enter_state真正钻进去 事后统计cpuidle_entercpuidle.c:372→cpuidle_enter_statecpuidle.c:211是进入 state 的核心也是选深了还是选浅了的统计发生地// drivers/cpuidle/cpuidle.c (v6.6, line 246)time_startns_to_ktime(local_clock_noinstr());// …entered_statetarget_state-enter(dev,drv,index);// ← 真正钻进去mwait// …醒来后time_endns_to_ktime(local_clock_noinstr());// …diffktime_sub(time_end,time_start);// 实际停留了多久dev-last_residency_nsdiff;dev-states_usage[entered_state].time_nsdiff;dev-states_usage[entered_state].usage;if(diffdrv-states[entered_state].target_residency_ns){// 停留比预期短 → 这次选深了abovedev-states_usage[entered_state].above;}elseif(diffdelay){// 停留足够久也许更深的 state 更合适 → 这次选浅了belowdev-states_usage[entered_state].below;}关键进入 state 前后的时间差diff就是实际空闲时长。它被用来更新states_usage[]的统计time_ns、usage标记选深了above停留 target_residency还是选浅了below停留 exit_latency且够得上更深的 state。这些above/below统计会在下次menu_updatemenu.c:461里被读出来用于校正预测因子——这正是 cpuidle 反馈闭环的落点。五、x86 底层hlt与mwait前面讲的都是框架最后落地的还是 x86 的两条指令。5.1default_idlehlt与mwait_idlemonitor/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*)current_thread_info()-flags,0,0);// 监视 flags 地址if(!need_resched()){__sti_mwait(0,0);// mwait 进 C-statehint0 即 C1raw_local_irq_disable();}}__current_clr_polling();}arch_cpu_idleprocess.c:777通过 static_call 间接调用实际例程select_idle_routineprocess.c:936在启动时决定用 mwait 还是 hlt。5.2intel_idle用 mwait 的 hint 指定 C-state 深度hlt/mwait_idle只能进浅的 C1。要进更深的 C-stateC1E/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)((eax0xFF)24)static__cpuidleintintel_idle(structcpuidle_device*dev,structcpuidle_driver*drv,intindex){structcpuidle_state*statedrv-states[index];unsignedlongeaxflg2MWAIT(state-flags);// 这个 state 对应的 hintunsignedlongecx1;/* break on interrupt flag */mwait_idle_with_hints(eax,ecx);// mwaiteax C-state hintreturnindex;}mwait 的 hint 用一个字节表示高 nibble 是主状态、低 nibble 是子状态0x00C1、0x01C1E、0x10C2、0x20C6…… 这些 hint 编码在intel_idle的 state 表里intel_idle.c:237起比如 C6 对应MWAIT2flg(0x20)且带CPUIDLE_FLAG_TLB_FLUSHEDC6 会 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有三个优势能进更深的 statehint 参数指定 C1E/C6 等省电空间大得多monitor 提供地址监视可以在进入前检查监视地址避免刚要睡就来了活的竞态mwait_idle里__monitor后need_resched再查一次自动唤醒mwait 在监视地址被写或中断到来时自动醒来不需要软件参与。6.4 为什么选深了/选浅了要单独统计above/below预测本质是对未来的猜测总会错。above选深了实际停留 target_residency和below选浅了把错误分类记录下来喂给 governor 的校正因子。这是整个 cpuidle 的精髓——它不追求一次预测准而是靠反馈闭环让预测逐步收敛。没有这个统计menu 就只是个猜下一个定时器的朴素预测器。七、与相邻主题的边界主题边界CPU hotplug#3hotplug 的cpuhp_state管CPU 存不存在cpuidle 的 C-state 管空闲睡多深——两套状态机别混中断与 IPI中断是唤醒idle 的手段mwait 因中断而醒IPI 里 reschedule 一类正是把睡着的 CPU 叫醒cpufreq下一篇cpuidle 管睡多深C-statecpufreq 管跑多快P-state 调频附本篇关键源码索引符号位置cpuidle_stateexit_latency / target_residency / enterinclude/linux/cpuidle.h:49cpuidle_driverstate 数组按功耗递减include/linux/cpuidle.h:152cpuidle_deviceper-CPU states_usage 统计include/linux/cpuidle.h:93cpuidle_governorselect / reflectinclude/linux/cpuidle.h:288cpuidle_idle_call选 → 进 → 反馈kernel/sched/idle.c:146do_idleidle 主循环kernel/sched/idle.c:237cpuidle_select/cpuidle_enter/cpuidle_reflectdrivers/cpuidle/cpuidle.c:356/:372/:402cpuidle_enter_state进入 above/below 统计drivers/cpuidle/cpuidle.c:211menu_select预测 选 statedrivers/cpuidle/governors/menu.c:262get_typical_interval/performance_multiplierdrivers/cpuidle/governors/menu.c:171/:155default_idlehlt /mwait_idlemwaitarch/x86/kernel/process.c:740/:918intel_idlemwait hint 指定深度drivers/idle/intel_idle.c:159