ARTICLE DETAIL

资讯详情

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

Linux内核system sleep深度解析:整机休眠唤醒全链路与驱动调优实践

Linux内核system sleep深度解析:整机休眠唤醒全链路与驱动调优实践 干了这么多年Linux功耗调优每次有人跟我说“休眠不生效”“唤醒就死屏”“一睡不醒”我第一反应不是查代码而是先问一句你确定自己用对了system sleep的基本模型吗整机休眠唤醒这事看着就是往/sys/power/state里写个mem实际背后是进程冻结、设备suspend分级回调、中断唤醒仲裁、CPU电源域切换的一套组合拳任何一个环节错位表现出来就是各种诡异的“睡眠瘫痪症”。这篇是Linux内核功耗子系统系列的第十四篇专门把system sleep整机休眠唤醒这条链路完整拆开从用户态写那个state节点开始一路跟到CPU进入睡眠、再从唤醒源把系统拉起来的全过程。适合正在做嵌入式Linux产品、Android底层、以及服务器电源管理的开发者。说的都是我实际调板子、翻内核源码、抓trace时验证过的内容不是文档翻译。1. 整机休眠唤醒到底在干什么从“关屏”到“全系统电源状态迁移”很多人容易把“整机休眠”和“关屏幕”“CPU降频”混为一谈。实际上system sleep的核心是整个系统进入一种可恢复的低功耗状态CPU停止执行指令、大部分外设断电或进入低功耗模式、内存要么自刷新保持、要么整体搬到磁盘之后通过特定唤醒源重新恢复执行。1.1 system sleep状态族谱s2idle / standby / mem / diskLinux内核通过/sys/power/state暴露当前支持的睡眠状态常见的是这四个freeze、standby、mem、disk。不同内核配置下显示项不一样比如没开休眠镜像支持的board就看不到disk。freezesuspend-to-idle也叫s2idleCPU进入idle循环设备不强制断电靠cpuidle把CPU留在浅睡眠状态。这个状态唤醒最快、功耗降低有限主要给一些不想动设备电源状态、只求快速待机的场景用。standbyACPI S1CPU停掉大部分执行单元内存保持供电设备状态由各自驱动决定。功耗比freeze低一点但恢复还是很快。memsuspend-to-RAMACPI S3最常用的“深度睡眠”。内存进入自刷新CPU和绝大多数外设掉电或断电只保留必要唤醒源供电。恢复时CPU从复位向量重新执行再从内存恢复现场。diskhibernateACPI S4把内存镜像写入swap或专门镜像文件然后整机断电。恢复时从磁盘读回镜像相当于一次“快速冷启动”。这个属于镜像恢复范畴细节以后单独开篇。日常嵌入式设备里最常见的是memAndroid系统里叫deep配合RTC、GPIO等唤醒源使用。很多人说“休眠唤醒”默认就是S3这条路径。1.2 为什么不要把“整机休眠”简单等同于“关屏”我见过不止一个人把屏幕背光关了、CPU调成低主频就说自己做了“休眠”。这是完全不同的东西。关屏只涉及display子系统和背光驱动功耗降不了多少真正的system sleep要做到1. 所有用户进程冻结不再获得调度 2. 设备suspend回调按依赖顺序执行分阶段关停 3. CPU offline到只剩boot cpu其余核停放 4. 中断关闭仅保留标记为唤醒源的中断 5. SoC电源域按策略下电内存进入自刷新 6. 唤醒事件到来后CPU复位启动反向恢复设备这已经不是“某个驱动怎么处理”的层级而是内核PM核心框架调度所有子系统协同配合的过程。任何一个驱动没实现suspend回调或者回调里做了阻塞操作整条链路都会卡死。1.3 谁需要关心system sleep的细节如果你是搞嵌入式Linux产品IPC、工控板、智能硬件需要实现“低功耗待机”那这篇值得从头读如果你只是用标准PC装了个Linux主要想搞清楚为什么合盖睡眠老失效也能从中找到排查思路如果你在写驱动那就更得看——驱动不配合整机休眠就是不完整的。2. 休眠入口用户态写一次 /sys/power/state 之后发生了什么整机休眠在用户态看来就是一条命令echo mem /sys/power/state但这行命令进去之后内核要跑完一整套流程任何一个环节报错状态都会回滚表现为“command write failed”或者干脆系统hang住。2.1 /sys/power/state 背后的状态机这个节点在内核源码kernel/power/main.c里实现写操作最终调用pm_suspend()。入口代码核心逻辑大致是static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t n) { suspend_state_t state decode_state(buf, n); if (state PM_SUSPEND_MAX) { error pm_suspend(state); } return error ? error : n; }pm_suspend()进入enter_state()这个函数维护了一个有限状态机suspend_prepare-suspend_devices_and_enter-suspend_finish。每一步都有对应的回滚路径。比如进程冻结失败就会回到PM_SUSPEND_PREPARE的notifier阶段把已经冻结的进程解冻然后返回错误。这个状态机设计的一个关键点要么不进睡眠要么完整走完睡眠唤醒绝对不允许停留在“半睡半醒”的中间态。如果你看到系统卡在某个驱动回调里出不来了基本上就是状态机卡在了某个transition上的典型症状。2.2 进程冻结先把天下稳住再关灯enter_state()里第一个重头戏是freeze_processes()。这个动作的意思是把系统中所有用户空间进程和可冻结内核线程先放进一种“冻结”状态——不参与调度、不执行用户代码、暂停所有工作等唤醒后再统一解冻。为什么必须先冻结进程因为设备suspend过程中外设正在一个接一个关电内存控制器的行为也在变化。如果此时还有进程在跑比如正在往串口写日志、正在访问一个还没来得及suspend的设备就会产生竞态设备已经关了进程还在访问直接总线异常或者hang住。我实际调试中遇到过最典型的场景一个后台打印进程一直往串口写echo mem之后系统直接hang死。后来看日志发现串口驱动已经在suspend回调里关了时钟而打印进程还没被冻结住抢先触发了外设访问。内核的进程冻结机制就是用来避免这种问题的。冻结过程不是瞬间完成的freeze_processes()会遍历所有进程、发送freeze信号并等待它们进入冻结状态有超时控制。这段期间用户态其实已经开始“凝固”了你在shell里敲的命令不会再响应。2.3 从 suspend_prepare 到 suspend_enter 的完整接力冻结完进程之后suspend_prepare阶段还做了几件事static int suspend_prepare(suspend_state_t state) { if (!sleep_state_supported(state)) return -EPERM; pm_prepare_console(); suspend_console(); pm_notifier_call_chain(PM_SUSPEND_PREPARE); freeze_processes(); return 0; }注意pm_prepare_console()和suspend_console()。这两个调用把内核console切到虚拟终端并暂停console输出。为什么因为设备suspend阶段如果有驱动在里面打log串口console还在工作那么串口驱动自己也要被suspend就会出现“输出途中突然断线”的乱码。所以先冻结console保证休眠过程中没有内核日志往外涌。之后进入核心的suspend_devices_and_enter()这里流程更细static int suspend_devices_and_enter(suspend_state_t state) { int error; error platform_suspend_begin(state); if (error) goto Close; dpm_suspend_start(PMSG_SUSPEND); suspend_enter(state, wakeup_mask); dpm_finish(PMSG_RESUME); platform_suspend_end(); Close: return error; }dpm_suspend_start()这里就开始对全设备树做suspend也就是第3章要详细讲的内容。suspend_enter()内部再往下关闭非boot CPU、关中断、执行dpm_suspend_end()处理late和noirq阶段最后调用平台相关的suspend ops真正把SoC放进睡眠。等唤醒发生后再反过来执行dpm_finish()恢复设备。这里要特别提醒platform_suspend_begin和platform_suspend_end中间的代码是整个机器最危险的区域。到了enter阶段大部分设备已经suspend中断也被处理过如果此时有驱动访问一个已经suspend的设备没有任何人帮你兜底——除了内核的panic。3. 设备suspend阶段驱动回调的执行逻辑与顺序陷阱整机休眠唤醒能不能成功80%的坑埋在设备suspend/resume回调里。很多驱动作者只写了runtime_suspend和runtime_resume没写system sleep相关的回调或者写了但回调里有严重阻塞操作这些都会被PM核心暴露出来要么睡眠后设备没真正关电要么唤醒后设备状态不对直接挂死。3.1 dpm_list 的排序规则suspend是反着来的内核维护了一条全局设备链表dpm_list所有注册了dev_pm_ops的设备都会挂进来。设备suspend的顺序不是随机的而是按dpm_list从尾部往头部遍历也就是后进入链表、依赖别人较少、通常是叶子节点外设的设备先suspend上游驱动和控制器后suspend。这个顺序很关键。比如一个板子上的I2C触摸屏触摸屏是I2C设备挂在I2C控制器下面。suspend时先关触摸屏、再关I2C控制器。反过来就完了——先把你访问外设的总线关了触摸屏的suspend回调还想通过I2C写寄存器直接卡住。这个链表顺序在设备模型里是靠device_initialize()入链顺序加device_links设备链接一起维护的。如果你的驱动想要强制某种顺序可以通过device_link_add()建立DL_FLAG_PM_RUNTIME等标志的链接关系告诉PM核心“我这个设备必须在那个设备之前suspend”。3.2 suspend / suspend_late / suspend_noirq 三个层级的分工每个设备的dev_pm_ops里其实有不止一个suspend回调经常被忽略的是这三个struct dev_pm_ops { int (*suspend)(struct device *dev); int (*suspend_late)(struct device *dev); int (*suspend_noirq)(struct device *dev); int (*resume_noirq)(struct device *dev); int (*resume_early)(struct device *dev); int (*resume)(struct device *dev); ... };它们执行时机不同suspend在中断仍然启用的阶段执行。设备还能正常与处理器通信可以做慢速操作比如把寄存器状态保存到内存、通知Firmware进入低功耗模式。suspend_late在CPU非boot核已经下线、但irq仍然开着的late阶段执行。不能做太重的操作尤其是不能依赖其他还没suspend的设备。suspend_noirq在中断关闭之后执行这是设备suspend最后一道关卡基本只允许做最轻量的操作篡改寄存器、直接内存访问、让设备进入硬复位状态。对应地resume时是反过来的resume_noirq最早执行、之后resume_early、最后resume。这种分层设计是为了把设备的电源控制器操作分散到不同中断环境中避免并发访问。3.3 runtime PM与system sleep的恩怨很多人混淆runtime PM和system sleep的suspend。runtime PM是设备运行期间针对空闲状态的电源管理比如某外设闲置5秒就关闭时钟system sleep是整机睡眠带来的全局suspend两者机制不同、回调也分开。但两者会互相影响。最常见的问题是设备在runtime suspend状态已经关闭了时钟此时整机进入system sleep内核拿到设备时发现它已经runtime suspended还会不会再调用system sleep的suspend回调答案是会PM核心会先把它runtime resume到active再走system sleep的suspend路径。这个逻辑在dpm_prepare()阶段处理。我见过一个驱动没处理好这种“resume后再suspend”的状态转换结果每次整机休眠都被唤醒一次功耗数据很难看。4. resume反向链路为什么唤醒比休眠更容易翻车休眠的流程再复杂好歹是按设计走完的resume才是真正的“现场恢复”任何一个设备恢复不到位系统虽然醒了但跑起来全是问题。我自己血泪教训是suspend问题通常能log定位resume问题经常是无声无息的“假醒”——系统看起来起来了但某块外设哑了。4.1 唤醒点CPU从哪条指令恢复执行在suspend_enter的最后阶段CPU执行到suspend_ops-enter(state)这个平台相关函数。对ARM SoC来说这个函数内部实现通常是关闭内存控制器自刷新前的缓存、把CPU状态保存到特定位置然后执行wfi指令等待唤醒事件。唤醒事件比如RTC中断、GPIO上升沿到来后CPU从wfi醒来经历芯片独有的唤醒流程可能要先恢复时钟、可能要从ROM里的一段唤醒代码开始之后跳转到内核的cpu_suspend恢复代码把CPU寄存器、栈指针恢复过来之后重新使能中断接着走到suspend路径的下一行代码——也就是说唤醒后不是从头启动而是从休眠时被打断的地方接着往下执行。这就是为什么resume路径不能简单理解为“重新跑一遍初始化”。它在恢复现场哪怕驱动被要求重新初始化硬件内核线程、内存映射、设备模型的抽象层面都已经恢复到睡眠前的样子了驱动如果擅自改变状态机的假设就会出现各种奇怪的“半初始化”问题。4.2 resume的反向顺序为什么是设备依赖的另一头先动suspend是叶子设备先关、根设备后关resume正好反过来根设备先恢复、叶子设备后恢复。比如I2C控制器先resume、触摸屏后resume。这样保证设备恢复时它依赖的总线已经能用了。具体到内核代码dpm_resume()执行顺序是从dpm_list头往尾部遍历也就是根设备先resume。每个设备同样分resume_noirq、resume_early、resume三个阶段依次推进。这样做还有个原因irq_resume阶段在resume_noirq之后系统需要尽早恢复中断控制器的状态让外设中断重新可用。resume阶段驱动要做的事通常比suspend更复杂要重新初始化硬件寄存器、恢复中断状态、还要处理睡眠期间可能发生的硬件状态变化比如外部sensor芯片还在继续跑产生了缓存数据。所以resume回调里面最忌讳的事情就是“简单把初始化流程重新跑一遍”——有些硬件经不住这么折腾有些外设时序上还没准备好跑一半就挂了。4.3 实测中常见的resume卡死原因我总结了三次实际项目里最常撞上的resume卡死类型锁依赖循环驱动A的resume回调里拿了一把锁这个锁又被睡眠前还没释放的驱动B持有。等resume到B时需要等待A让出锁直接死锁。内核里对这种情况的防护很弱除非你打开PROVE_LOCKING提前测出来。外设时钟没恢复有些SoC的suspend会让PLL掉电resume时bootloader或内核平台代码恢复时钟。但如果你板子上的某个外设时钟是由PMIC管着的而PMIC的resume回调排在后面才执行那前面resume的外设访问就是白跑。休眠前没flush数据比如MMC设备在suspend前还有写cache没刷干净唤醒后访问文件系统直接报错。这个不属于“卡死”但表现跟卡死很像系统挂着日志一直在刷IO error。排查这类问题我建议优先看dmesg里的PM: dpm_run_handler()耗时统计然后对照设备树逐个确认谁在resume阶段耗时异常。5. 唤醒源设计从RTC、GPIO到wakeup source框架系统能睡下去不算本事能按预期醒过来才算完成整条链路。唤醒源设计是整个system sleep里最容易和硬件打架的地方因为芯片手册上写的唤醒能力和内核里真正管用的中断配置往往隔着一层。5.1 IRQF_NO_SUSPEND 与 irq_set_irq_wake 的差别内核里和唤醒相关的中断标志经常被搞混我把几个关键点列出来IRQF_NO_SUSPEND这个中断在system sleep过程中不会被disable它允许中断在睡眠期间触发。但注意它不等于“能唤醒系统” —— 它只是说这个irq处于启用状态如果SoC的中断控制器没把这个irq配置成唤醒源CPU照样不醒。 irq_set_irq_wake()这个才是真正的“配置唤醒源”操作它调用irq_chip底层的irq_set_wake回调把中断控制器的对应引脚设置为唤醒使能并同时会管理一个wakeup source。所以正确做法是一个GPIO要当唤醒源驱动里应该用enable_irq_wake(irq)等价于irq_set_irq_wake(irq, 1)而不是只设IRQF_NO_SUSPEND。很多驱动把人家的IRQF_NO_SUSPEND抄过来就以为能唤醒结果系统进mem后睡得死死的就是这个原因。5.2 wakeup source框架与wakelock内核提供了一个统一的wakeup_source框架设备可以注册一个struct wakeup_source通过__pm_stay_awake()和__pm_relax()来告诉PM核心“我现在还在处理事件别睡”。这在Android的wakelock机制里用得最多。在标准linux内核上如果你想查看当前有哪些wakeup source在阻止睡眠cat /sys/kernel/debug/pm_debug/wakeup_sources或者看/sys/kernel/debug/wakeup_sources内核配置不同路径有点差异。这个表非常有用可以看到每个wakeup source的active_count、last time等信息快速定位“为什么系统休眠不了”。我调试嵌入式板时发现的一个高频问题某个触摸屏I2C设备注册了wakeup source但因为中断线悬空、有噪声导致__pm_stay_awake()被频繁调用系统永远进不了深度睡眠。查这个文件一眼就看到了不用猜。5.3 唤醒电平的选择level触发为什么容易死循环选GPIO唤醒触发方式是个容易被忽视的细节。用level触发高电平或低电平唤醒经常遇到一个问题唤醒源拉高后系统醒来开始resume但GPIO电平还没恢复于是又立刻触发了一次唤醒中断系统又回到睡眠又醒来回反复功耗还高。要解决这个常见的做法是把GPIO配置为edge触发上升沿/下降沿事件产生的是一个脉冲中断处理完就没有持续电平了或者维持level触发但在resume早期先disable_irq()这个唤醒源处理完事件再enable。我这里更推荐edge触发因为它在内核里少一个“state machine”维护工作量省得你调试“刚醒又被睡回去”的毛病。另外RTC唤醒是嵌入式产品里最常用的定时唤醒方案。配置方式很简单echo 0 /sys/class/rtc/rtc0/wakealarm echo 30 /sys/class/rtc/rtc0/wakealarm echo mem /sys/power/state这条命令组合的意思是30秒后RTC产生唤醒事件拉系统起来。很多产品做“定时开机”“定时采集”就是用这个套路。6. 排错实战定位一次休眠唤醒异常的标准姿势最后把这几年调休眠唤醒问题沉淀下来的一套方法写出来基本套路没变过先用pm_test分层切再靠dmesg和ftrace定位最后锁定内核版本和代码改动比对。很多时候问题就出在“你改了一个驱动没意识到它跟睡眠链路纠缠在一起”。6.1 pm_test 分层切割内核的睡眠过程是分层的从freeze_processes到dpm_suspend、suspend_noirq、cpu offline、实际enter每一层都可能出问题。为了快速定位内核提供了/sys/power/pm_test调试入口echo core /sys/power/pm_test echo mem /sys/power/state可选的层级有freezer、devices、platform、processors、core。拿core来说它会把suspend流程跑到实际enter之前就结束然后立刻resume回来用来验证“深度那个点是否卡住”。一层一层往下测就能知道是进程冻结阶段挂了、设备suspend阶段挂了、还是真正的SoC下电阶段出了问题。这个手段特别适合快速确认“是驱动问题还是平台问题”。如果devices这个level都过不去基本就是设备驱程有bug不用往下查别的了。6.2 dmesg 中的关键标记完整的休眠唤醒日志里内核会在kernel/power/suspend.c和kernel/power/main.c里打印关键节点格式大致是PM: suspend entry (deep) PM: suspend exit在PM: suspend entry之前你会看到进程冻结的log之后是设备suspend时可能出现的PM: dpm_run_handler或者drivers_probe类报错。如果要更细致地看每个设备的suspend执行时间需要打开CONFIG_PM_DEBUG和CONFIG_PM_SLEEP_DEBUG然后查看cat /sys/kernel/debug/pm_debug/devices这个文件会列出每个设备的suspend/resume时间和状态是定位“哪个设备拖慢了休眠”的利器。比如某个网络驱动suspend耗时1200ms那整个系统进睡眠就得等这么久一眼能看出来。还有一个容易被忽略的标记Freezing user space processes ...之后的Restarting tasks ... done。如果在Restarting tasks之后系统立刻panic多半是某个内核线程在冻结期间没处理好锁这类问题跟设备驱动无关和时间敏感型代码有关。6.3 ftrace 测量 suspend/resume 耗时dmesg只能看到分段的整体耗时真正要追踪哪一行代码卡住得用ftrace。我最常用的方式echo 0 /sys/kernel/debug/tracing/tracing_on echo function_graph /sys/kernel/debug/tracing/current_tracer echo suspend_enter /sys/kernel/debug/tracing/set_graph_function echo 1 /sys/kernel/debug/tracing/tracing_on echo mem /sys/power/state sleep 5 cat /sys/kernel/debug/tracing/trace /tmp/suspend_trace.txt这样能抓到suspend核心函数的函数调用栈。能看到具体是哪一步、哪个驱动的调用延迟住了。resume侧追踪类似抓dpm_resume或者resume_early相关函数。实际项目中我遇到过的最难定位的一个问题某驱动suspend回调只有2ms但整机suspend总耗时却到800ms。用ftrace一拉发现是多核同步等待时某个CPU在cpuidle里耗了太久。这种问题光看dmesg永远找不到因为每个设备的回调都正常问题是调度时序。6.4 几个常见坑的处理实例最后分享三个实际项目里我遇到过的典型case仅供参考Case 1suspend后系统自动苏醒功耗全局拉高排查方式先看/sys/kernel/debug/wakeup_sources发现是某个PMIC的power key中断一直active进mem后PMIC因为硬件配置还在输出电平导致中断不断触发。解决该中断只做“获知按键状态”不作为唤醒源用disable_irq_wake()去掉唤醒能力。Case 2resume后HDMI无输出典型原因是display驱动的resume回调在resume_early阶段访问了I2C DDC通道而I2C控制器的resume还没执行完。解决检查设备树中HDMI和I2C控制器的device_link依赖关系或者把DDC访问挪到更靠后的阶段。Case 3echo mem后卡在Freezing user space processes这个现象是某个用户进程不响应冻结信号。一般是进程阻塞在内核态无法返回比如在读写一个不响应IO的设备。解决查cat /proc/*/status里的State和frozen标记找出那个卡住的进程修复它访问的驱动。一些实际体会系统sleep这个子系统我刚开始调的时候最不适应的一点是它不像驱动代码那样“改了马上能验证”而是一环扣一环睡眠失败的原因经常藏在另一个子系统的细节里。后来习惯了先把整条链路吃透、再用工具分层定位问题基本都能在一天内锁定。对做产品的朋友说句实在话休眠唤醒的稳定性必须在项目早期就纳入测试计划。等到功能都写完了再补suspend/resume等着你的往往是无穷无尽的外设状态恢复问题那时候想改硬件配置都已经来不及了。
返回列表