☰
深度解析Linux内核runtime PM:核心机制与驱动落地实践
2026/10/8 13:47:21 网站建设 项目流程

做内核功耗这摊子事的人,迟早都要跟 runtime PM 打交道。不管你是调显示驱动、网络驱动,还是搞 MIPI 摄像头,只要芯片有低功耗需求,就一定绕不开这个机制。这篇文章是系列第七篇,专门把 runtime PM 这条线彻底捋一遍,从核心数据结构到 API 使用方式,再到驱动里怎么落地、出问题了怎么查,一次讲透。

1. runtime PM 在设计上解决了什么问题

在讲 API 和代码之前,先得把 runtime PM 出现的原因想清楚。很多做驱动的人一开始都会困惑:系统不是有 suspend/resume 吗?为什么还要搞一套 runtime PM?这两套机制完全是两码事。

系统级 suspend/resume 是全局操作,一旦触发,整个 SoC 都要进低功耗状态,所有设备按依赖关系排队挂起,这个过程通常很慢,动辄几百毫秒甚至几秒。而 runtime PM 解决的是单个设备在系统正常运行时的空闲功耗问题。比如一部手机亮屏待机时,系统整体在运行,但是触摸屏控制器其实已经没事干了,Sensor Hub 也只需要在数据变化时醒一下。这些设备如果不切到低功耗状态,芯片就会一直白白耗电。

两个机制叠加起来看,系统 suspend 像一个厂房整体断电,而 runtime PM 就是每个工位上不干活时随手关掉工位上的小灯。这个区别非常重要,因为两者在代码路径、锁机制、回调函数上有本质差异。

runtime PM 最核心的思路,是围绕设备的使用计数和状态机来运作。当一个设备不再被使用时,系统可以自动把它挂起;当需要操作它时,再把它唤醒。整个过程不需要用户态介入,驱动开发者在 probe 阶段把回调注册好、把自动挂起的参数配置好,内核会在适当时机完成后续工作。

这套机制最早在嵌入式平台上大规模落地,现在已经是 Linux 内核里标准化的功耗管理基础设施。从 kernel/drivers/base/power/runtime.c 的核心实现,到 include/linux/pm_runtime.h 暴露给驱动的 API,再到 Device Tree 里的电源域和状态节点配置,一整条链路都围绕着 runtime PM 展开。

对驱动开发者来说,理解 runtime PM 至少有三个实际好处:第一,能够正确管理设备功耗,延长电池续航或降低整机发热;第二,避免低功耗状态导致外设工作异常,比如 I2C 挂死、中断丢失;第三,能用标准机制替代自己手写的状态机,代码更简洁也更容易让后续维护的人看懂。

2. 核心机制拆解:状态机、引用计数与回调函数

2.1 设备状态机:ACTIVE、SUSPENDED 与 SUSPENDING

runtime PM 的内部状态是通过 enum rpm_status 来描述的,关键有三个状态:RPM_ACTIVE、RPM_SUSPENDED,以及两个中间状态 RPM_SUSPENDING 和 RPM_RESUMING。

RPM_ACTIVE 表示设备当前已经处于工作状态,时钟、电源、IO 全都正常,读写寄存器没有问题。RPM_SUSPENDED 则表示设备已被挂起,此时从软件层面看,对设备的访问需要先触发 resume 流程,否则操作很可能挂死或读回全 F。

两个中间态是异步操作时才会出现的。异步请求 resume 或 suspend 时,内部会先把状态置为 RESUMING 或 SUSPENDING,然后通过工作队列异步执行整个切换流程。同步操作虽然也会短暂经过这些状态,但因为调用者在原地等到切换完成,感知上就只有 active/suspended 两种。

需要特别留意的是,runtime PM 的状态只是一个软件状态记录,不代表硬件一定处于什么状态。比如你可以在 runtime_resume 回调里不实际开时钟、不拉电源,只是把状态标记为 active。这么做虽然能用,但功耗优化效果为零。真正的好坏,取决于回调函数里做了什么,所以驱动开发者不能只想着调用 PM 框架的 API,更得把回调里的硬件操作写到位。

2.2 引用计数:usage_count 与 disable_depth

每个设备的 struct device 结构体里有一个 dev_pm_info 类型的 power 字段,它内部维护了 usage_count、disable_depth、runtime_status 等关键信息。usage_count 是门卫,设备只要有一个引用者,就坚决不能挂起。

有几个 API 直接操作这个计数:pm_runtime_get 把 usage_count 加一,pm_runtime_put 把 usage_count 减一。当计数从 1 变到 0 时,内核会触发挂起流程;当计数从 0 变到 1 时,设备如果处于挂起状态,就会触发恢复流程。

使用计数的设计逻辑非常像 C++ 的 shared_ptr 或者 Python 的引用计数。多个驱动模块可以同时引用同一个设备,比如一个 MIPI DSI 显示屏驱动被 framebuffer 和 DSI controller 同时引用,只要还有人在用,设备就不会被挂起。只有所有引用者都释放了,设备才有资格进入低功耗状态。

disable_depth 则是另一个维度的控制。它表示 runtime PM 功能是否被使能。初始状态是 1,设备创建后 runtime PM 默认是禁用的。驱动必须在 probe 过程调用 pm_runtime_enable,将 disable_depth 减到 0,运行时电源管理才算真正打开。反过来,remove 时调用 pm_runtime_disable,把功能关闭。

如果忘了调 pm_runtime_enable,后面无论怎么 get/put,设备都不会挂起或恢复,所有调用都会静默失败。这个问题我在实际代码里见过不止一次,而且很难发现,因为内核日志上没有任何报错。

2.3 回调函数:runtime_suspend、runtime_resume 和 runtime_idle

设备驱动的 dev_pm_ops 结构体里有三个与运行时电源管理相关的回调:

static const struct dev_pm_ops xxx_pm_ops = { .runtime_suspend = xxx_runtime_suspend, .runtime_resume = xxx_runtime_resume, .runtime_idle = xxx_runtime_idle, };

runtime_suspend 在 usage_count 降到 0 时调用,负责关时钟、关电源域、停 DMA、把寄存器保存下来等。runtime_resume 则在 usage_count 从 0 变 1 时调用,负责重新开时钟、恢复电源、写回寄存器、重建 DMA 通道。runtime_idle 的语义更柔和:它通知驱动设备已经空闲,但还没真正挂起,驱动可以选择现在就开始做准备,也可以忽略这个通知。

runtime_idle 有一个容易误用的点。如果驱动不提供 runtime_idle 回调,内核会马上执行自动挂起;如果提供了但回调返回 0,内核会认为驱动已经处理了空闲事件,不再往下走到 runtime_suspend。很多驱动为了让设备立即进入挂起状态,会故意让 runtime_idle 返回 -EBUSY 或直接不注册。想利用 autosuspend 延迟挂起,就会在回调里启动一个延迟定时器。这些用法没有绝对的对错,只要跟产品的功耗策略匹配就好。

2.4 父子设备依赖与唤醒机制

runtime PM 框架还处理了设备的层级依赖问题。一个设备挂起前,先检查它的 child 设备是否都已经挂起,如果有子设备还在活跃状态,父设备就不允许挂起。这是在 struct dev_pm_info 内部的 child_count 以及 ignore_children 标志配合下实现的。

反过来,父设备恢复时,会先递归恢复所有子设备。这个顺序很讲究,因为很多硬件依赖是父设备提供的时钟、电源或 IO 域。父设备没准备好,子设备就算调用了 resume 回调和硬件沟通也会失败。这跟系统 suspend 里的 dpm_list 顺序是一致的,只不过 runtime PM 是每个设备独立触发,更加细粒度。

对于唤醒设备,runtime PM 也有对应的 wakeup 机制。设备可以设置 wakeup source,在挂起状态下产生唤醒信号,把系统或某个电源域拉起来。enable_irq_wake 这类操作在 runtime_resume 回调里经常出现,尤其是对触控、传感器这类需要超低功耗待机的外设来说,是很常规的做法。

3. 核心 API 梳理:从 get 到 put 的完整闭环

3.1 同步接口与异步接口

pm_runtime_get_sync 和 pm_runtime_put_sync 是同步接口,调用后会阻塞直到设备完全进入目标状态。pm_runtime_get_sync 在执行中会等 runtime_resume 返回,pm_runtime_put_sync 会等 runtime_suspend 返回。

这两个接口在正常的进程上下文用最保险。我建议驱动的文件操作接口、IOCTL、以及可以睡眠的线程上下文中,优先使用同步接口。它们虽然慢一点,但语义清晰,出问题时好排查。

异步接口 pm_runtime_get 和 pm_runtime_put 只调整引用计数并下发请求,实际的恢复或挂起操作放到内核工作队列 pm_wq 里异步执行。异步接口可以在中断上下文调用,比如网卡收包中断里,通过 pm_runtime_get 唤醒设备,等唤醒完成后再读 FIFO,这个流程如果用同步接口就会导致睡眠在原子上下文中直接崩溃。

kernel 文档里有一张经典表格,列出了每个接口对应在何种上下文安全使用,我直接抄在这里供参考:

接口中断上下文原子上下文进程上下文
pm_runtime_get_sync禁止禁止允许
pm_runtime_put_sync禁止禁止允许
pm_runtime_get允许允许允许
pm_runtime_put允许允许允许

实际写代码时,我经常看到有人不分场景,一律用同步接口。短时间看没出问题,但在某些高速中断里就会偶发 panic。测试通常很难压出来,等到量产现场才爆雷。所以在驱动里定义一套自己的封装习惯非常重要:凡是能在进程上下文处理的,统一用 get_sync/put_sync;可能进中断路径的地方,替换成异步接口加一个完成量或者 refcount 来保证时序。

3.2 引用配对与 error code 的陷阱

pm_runtime_get_sync 的返回值需要特别小心。这个函数如果在设备恢复过程中出错,除了返回错误码,还会把 usage_count 减回去。也就是说,它实际上是 get 操作,如果失败就会变回"没拿过"的状态。所以正常写法必须判断返回值,不然你觉得自己持有了设备引用,实际上并没有,后续操作设备可能白忙活甚至踩坏状态。

相反,pm_runtime_put_sync 不管内部是否出错,都会把计数减一。如果 put 之后设备立刻被再次 get,两次调用之间也要保证时序正确。很多 bug 出现在两个线程同时操作一个设备时,一边 get 一边 put,usage_count 就像仰卧起坐一样一直在 1 和 2 之间跳。要避免这个问题,必须给引用操作加合适的锁或者用 atomic 语义。

还有一个容易疏忽的地方是自动挂起的延迟机制。如果启用了 autosuspend,调用 pm_runtime_put 后设备不会立刻挂起,而是等 autosuspend_delay 超时后才挂起。有一种典型用法:用户关闭一个节点,驱动里 put 一下,然后立刻手动调用 pm_runtime_suspend 来强制挂起。这会导致 autosuspend 的延迟毫无意义,甚至可能跟后面的异步挂起请求冲突。要么就不开 autosuspend,要么就不要手动强制挂起,二选一。

3.3 autosuspend 的配置与场景选择

autosuspend 的核心价值是避免设备频繁地在 active 和 suspended 间乒乓切换。每次切换都有成本,尤其是 MIPI 或 PCIe 这类需要重新协商链路的设备,几十毫秒的恢复时间期间,如果数据又来,系统会陷入"歇一下又忙起来"的恶性循环。

pm_runtime_set_autosuspend_delay 用来设置超时时间,pm_runtime_use_autosuspend 用来使能自动挂起模式。我通常习惯把网卡、蓝牙这类流量有突发性的设备设置 100ms 到 500ms 的延迟,把触摸屏这类交互设备设置 1 到 2 秒。具体数值一般通过 modprobe 参数或者设备树属性暴露,方便后期调试时改。

与 autosuspend 相关但常被忽略的一个接口是 pm_runtime_idle。在非 autosuspend 模式下,设备空闲后内核会先调用 runtime_idle 回调,如果回调返回 0 才会继续 runtime_suspend;如果驱动希望延迟挂起,可以在 runtime_idle 里返回 -EBUSY,让内核放弃本次挂起尝试,然后驱动自己安排定时器,超时后再强制挂起。这种做法在 WiFi 驱动里很普遍,因为网卡一旦挂起再恢复,重新关联 AP 的时间成本非常高。

3.4 force 系列接口:什么时候需要绕过引用计数

pm_runtime_suspend、pm_runtime_resume 和 pm_runtime_forbid 这些接口不修改 usage_count,而是直接强制设备进入某个状态。pm_runtime_suspend 会忽略 usage_count,直接调用 runtime_suspend 回调把设备挂起。pm_runtime_resume 亦然。

这类强制接口适合设备进入低功耗功能模式时使用,比如传感器要进入 one-shot 采样模式,内核先把设备强制挂起,然后通过一个特殊寄存器唤醒做单次采集,采集完成后再次强制挂起。这种场景并不改变设备的"被使用"语义,而是在功耗上做精细控制。

但强制接口用起来要非常谨慎。如果代码中同时存在 get/put 引用和 force 操作,很容易出现状态错乱。我一个同事就因为在一个驱动里混用这两种方式,结果出现偶发的 resume 请求在 suspend 过程中被打断,设备状态变得既不是 active 也不是 suspended,后续操作全部超时。直到加了完整的状态机日志才定位到问题。

3.5 runtime PM 与系统 suspend/resume 的联动

runtime PM 不是孤立运行的,它与系统级 PM 之间有明确的交互逻辑。系统 suspend 流程中,__device_suspend 会先看设备是不是因为 runtime PM 已经挂起了,如果是,就直接跳过 suspend 回调;如果没有挂起,则正常执行 suspend。同样在系统 resume 时,如果设备之前是 runtime suspended,resume 回调会触发一次 runtime_resume,然后系统再执行完整的 resume 回调。

这套设计的关键点是 runtime PM 的挂起和系统 suspend 的挂起不能互相覆盖。如果驱动在 runtime_suspend 里关了中断、停了 DMA,但在系统 suspend 回调里没有做对应处理,恢复时就会出现设备不可用的隐患。所以大多数复杂驱动在 system suspend 前会先调用 pm_runtime_resume 或 pm_runtime_get_sync 强制设备回到 active,保证系统 suspend 时设备处于一个已知状态。

4. 实操:在平台驱动中完整落地 runtime PM

4.1 基本框架:probe、remove、open 和 release 中的配对

用一个虚拟的平台设备举例。假设这个设备是一个 SPI 外设接口,内部有独立的 SRAM,不工作时可以关掉 PLL 和 GPIO 使能脚。

驱动 probe 中的初始化操作通常是这样的:

static int foo_probe(struct platform_device *pdev) { struct foo_dev *foo; struct device *dev = &pdev->dev; int ret; foo = devm_kzalloc(dev, sizeof(*foo), GFP_KERNEL); if (!foo) return -ENOMEM; platform_set_drvdata(pdev, foo); mutex_init(&foo->lock); init_completion(&foo->pm_completion); // 注册各种子设备、初始化硬件寄存器等 foo->regmap = devm_regmap_init_spi(...); if (IS_ERR(foo->regmap)) return PTR_ERR(foo->regmap); // 关键:允许设备运行时电源管理 pm_runtime_set_active(dev); pm_runtime_enable(dev); pm_runtime_set_autosuspend_delay(dev, 100); pm_runtime_use_autosuspend(dev); return 0; }

pm_runtime_set_active 这一步容易被忽略,但意义重大。它把设备的初始状态标记为 active,并绕过一次 runtime_resume 调用。如果不调用,设备初始状态是 suspended,但硬件时钟和寄存器并没有真正初始化,后续对设备的访问会因为设备处于超低功耗状态而失败。

remove 时也要倒过来操作:

static int foo_remove(struct platform_device *pdev) { struct foo_dev *foo = platform_get_drvdata(pdev); struct device *dev = &pdev->dev; pm_runtime_disable(dev); pm_runtime_put_sync(dev); // 卸载中断、释放资源等 free_irq(foo->irq, foo); return 0; }

这里有个细节:pm_runtime_disable 会把 disable_depth 重新加一,让 runtime PM 停止工作。但在 disable 之前,应该把引用计数清干净,所以前面放了 pm_runtime_put_sync。顺序反了的话,disable 之后 put_sync 虽然不会报错,但整个引用计数看起来是残的,容易对后来排查问题的人产生误导。

4.2 文件操作接口里的 get 与 put

设备对应的 open 和 release 操作是引用计数最常见的切入点。当一个应用程序打开设备节点时,设备必须活跃;关闭时,可以让它重新进入可挂起状态。

static int foo_open(struct inode *inode, struct file *filp) { struct foo_dev *foo = container_of(inode->i_cdev, struct foo_dev, cdev); struct device *dev = foo->dev; int ret; ret = pm_runtime_get_sync(dev); if (ret < 0) { dev_err(dev, "failed to resume: %d\n", ret); pm_runtime_put_sync(dev); return ret; } filp->private_data = foo; return 0; } static int foo_release(struct inode *inode, struct file *filp) { struct foo_dev *foo = filp->private_data; struct device *dev = foo->dev; pm_runtime_put_sync(dev); return 0; }

在 open 里我最常用的写法是 get_sync 然后判断返回值,失败就 put 回去。这看起来有点绕,但实际是标准做法。因为 pm_runtime_get_sync 在内核源码中带有一个隐含规则:如果它返回错误,就不能假设设备处于 active 状态,内部已经自动执行了 put,此时再手动 put 一次就会把计数减成负数。等于是额外留了一个对称性保护。

release 里的 put_sync 则触发 runtime PM 自动挂起流程。如果开启了 autosuspend,这里不会立刻挂起,而是进入延迟计时,这非常适合交互类设备。

4.3 中断路径中的异步恢复处理

有些设备在挂起后仍然能产生中断,比如一个低功耗按键检测模块。此时在中断上下文,不能直接调用 pm_runtime_get_sync。业界普遍的处理方式是用 pm_runtime_get 加一个完成量,在 resume 完成后通过完成量通知中断线程。

static irqreturn_t foo_irq_handler(int irq, void *data) { struct foo_dev *foo = data; struct device *dev = foo->dev; int ret; ret = pm_runtime_get(dev); if (ret < 0) return IRQ_HANDLED; wait_for_completion(&foo->resume_done); // 此时设备一定处于 active,可以读取 FIFO、处理事件 foo_read_status(foo); pm_runtime_put(dev); return IRQ_HANDLED; }

注意这里的 wait_for_completion 必须在进程上下文可睡眠的前提下才能使用。如果你现在已经在 hardirq 上下文,就不能 sleep,那么这套方案就不适用了。正确做法是只用 pm_runtime_get 开启异步恢复,然后把实际的数据读取挪到 workqueue 或 threaded irq 中。这是中断编程和 PM 框架交织时最容易犯错的地方,要格外小心。

4.4 runtime PM 回调的落位

驱动的 runtime_suspend 主要做两件事:保存硬件状态、关闭硬件电源来源。

static int foo_runtime_suspend(struct device *dev) { struct foo_dev *foo = dev_get_drvdata(dev); // 保存关键寄存器,以便 resume 时恢复 foo->saved_reg = readl(foo->base + FOO_CFG_REG); // 停 DMA、关中断、关时钟 dmaengine_terminate_all(foo->dma_chan); clk_disable_unprepare(foo->clk); // 关电源域或使能脚 regulator_disable(foo->vddio); return 0; }

runtime_resume 则是反过来的操作:

static int foo_runtime_resume(struct device *dev) { struct foo_dev *foo = dev_get_drvdata(dev); // 恢复电源域 regulator_enable(foo->vddio); // 重新开时钟、开 DMA clk_prepare_enable(foo->clk); writel(foo->saved_reg, foo->base + FOO_CFG_REG); return 0; }

这段代码看起来简单,但有两个容易弄反的地方。第一,时钟和电源域的操作顺序,一般是先电源后时钟,因为很多时钟模块本身依赖电源域稳定供电。第二,如果设备使用 DMA,必须在 DMA 通道重建之后再触发上一次未完成的传输,否则数据会丢。这些细节在常规文档里不会写,但都是实际调试中频繁踩坑的点。

4.5 与 regmap/pm_domain 的衔接

大多数现代驱动都用 regmap 操作寄存器,而 regmap 本身是支持 runtime PM 的。驱动器通过 regmap_init_i2c 或 regmap_init_spi 初始化 regmap 时,可以把设备的 power ops 接到 regmap 的 read/write 回调上。

实际使用中,我常常把 runtime PM 的 get/put 包在 regmap 的 read/write 前后:

static int foo_regmap_read(void *context, unsigned int reg, unsigned int *val) { struct foo_dev *foo = context; struct device *dev = foo->dev; int ret; ret = pm_runtime_get_sync(dev); if (ret < 0) return ret; ret = regmap_raw_read(foo->regmap, reg, val, 4); pm_runtime_put_sync(dev); return ret; }

这套封装可以确保任何通过 regmap 访问寄存器的路径都不会在设备挂起时直接去访问硬件,有效避免了系统挂死和总线错误。总线级 PM 域(genpd)通常也会在设备挂起时把整条总线电源关掉,所以这一步非常关键。

5. 常见问题与排查技巧实录

5.1 runtime PM 导致驱动 probe 阶段崩溃

很多驱动一开始在 probe 阶段添加 runtime PM 代码后,发现系统在启动时直接崩了。一个常见原因是 pm_runtime_set_active 和 pm_runtime_enable 的调用顺序反了。必须先 set_active 再 enable,因为 enable 之后框架就认为这个设备已经可以被挂起,如果它的初始状态是 suspended,但又没有对应的恢复路径,第一次 get 就会异常。

另一个原因是 probe 函数在拿到任何资源之前就启用了 runtime PM,然后某个资源申请失败,走 error path 时又恰恰触发了挂起流程,此时设备的内部结构还没有初始化完全,一执行 runtime_suspend 就崩溃。所以建议在 probe 函数的资源申请全部成功之后,再考虑开启 runtime PM。

5.2 usage_count 一直为 1 导致设备永不挂起

设备出不来低功耗状态,读 /sys/devices/.../power/runtime_status 时老显示 active,用 cat /sys/devices/.../power/usage_count 看到计数是 1,怎么都不归零。这种情况通常是驱动代码在某个路径里做了 get 但对应的 put 被丢掉了。

常见的位置是错误处理分支。比如一个 write 操作,函数前半段 get_sync 成功,中间某个判断失败提前 return,直接跳过最后的 put_sync。要根除这类问题,可以在开发阶段给常用驱动路径加上 WARN_ON(atomic_read(&dev->power.usage_count) < 0) 这类断言,或者直接在 release 函数里检查计数并打印日志,逐步缩小排查范围。

5.3 resuming/suspending 状态卡死

如果 runtime_status 一直停留在 suspending 或 resuming,说明异步任务卡住了。最常见的原因是 runtime_suspend 回调里等待了一个永远等不到的事件,比如等一个 DMA 完成中断,但 DMA 在挂起流程中已经被终止了。

排查思路是看内核日志有没有对应驱动打印的阻塞等待信息,或者用 crash 工具抓当前任务栈。有一种比较隐蔽的情况是回调函数里调用了某个会睡眠的接口,但当前处于 atomic 上下文,比如在 timer 回调中触发了挂起流程。这样连调度器都起不来,状态机就彻底卡住了。处理方法是检查所有 pm_runtime_suspend/pm_runtime_resume 的调用点,确保它们都来自允许睡眠的上下文。

5.4 autosuspend 延迟参数太短造成的性能抖动

曾经把一块 MIPI DSI 触摸屏的 autosuspend_delay 设置成 10ms,结果屏幕操作时明显卡顿。原因其实很简单:每次触摸抬起后 10ms 设备就挂起,下一次触摸到来时又需要重新 resume,MIPI 链路重新同步的时间比人手的触控间隔还长。

对交互类设备,建议把 autosuspend_delay 调到 500ms 到 2000ms 左右;对网卡这类有週期性流量但又不想频繁开关的设备,500ms 通常合适;对传感器这类数据频率固定的设备,100ms 到 300ms 一般够了。具体的值要结合硬件恢复延迟和业务流量模式综合判断,开发时最好做成 module 参数方便现场调优。

5.5 调试手段:sysfs、tracepoint 和 ftrace

runtime PM 的调试信息非常丰富,第一个必看的地方是每个设备目录下的 power 节点:

cat /sys/devices/.../power/runtime_status cat /sys/devices/.../power/runtime_suspended_time cat /sys/devices/.../power/runtime_active_time cat /sys/devices/.../power/usage_count cat /sys/devices/.../power/control

control 文件可以切换 auto/on 来控制设备是否允许 runtime PM 决定挂起。在开发阶段现场怀疑设备因为挂起而出问题时,直接把 control 改成 on 可以强制设备一直 active,快速排查问题是否和 PM 相关。这个手段非常高效,我经常用它做二分定位。

如果要在代码层面观察 PM 切换的完整过程,可以打开 ftrace 的 power 事件:

echo power:pm_runtime_begin > /sys/kernel/debug/tracing/set_event echo power:pm_runtime_end >> /sys/kernel/debug/tracing/set_event cat /sys/kernel/debug/tracing/trace

这样能看到每次设备的挂起/恢复请求是谁发起的,耗时多久,成功率如何。配合 function_graph 跟踪某个驱动的 pm_runtime_* 函数调用栈,基本能定位绝大多数 runtime PM 异常。

6. 驱动开发者的几个实践心得

做了几年驱动,被 runtime PM 坑过很多次,也帮别人救过不少现场。总结几条经验。

第一,probe 之后立即 pm_runtime_enable,但在所有资源就绪前不要让设备进入挂起状态。一个稳妥的办法是 probe 最前面先 pm_runtime_set_active,资源全部初始化完再 enable,这样即使中途失败,设备也始终停留在 active 状态,不会触发空回调。

第二,所有访问硬件的路径,不管是在字符设备接口、中断线程、定时器回调还是 regmap 封装里,都必须统一通过 runtime PM 的引用计数来保护。千万不要只在某几个操作里加保护,因为硬件挂起之后,任何未保护的寄存器访问都会造成不可预期的结果,这种 bug 最难复现,也最难以排查。

第三,给 runtime_suspend/runtime_resume 回调函数加充足的日志,尤其在新芯片 bringup 阶段。内核的 PM 日志经常被裁剪,可以自己在回调里用 dev_dbg 或 trace_printk 记录关键节点。量产时再把日志关掉,不影响性能。

第四,多设备协同场景下充分考虑父子设备的依赖关系。如果某个外设挂在 I2C 总线上,I2C controller 本身也有 runtime PM 管理,那么每次总线空闲都可能把 controller 挂起,子设备的操作就会变慢。这时候要么在子设备 get 的同时也 get 父设备,要么把父设备的 ignore_children 置位,避免父设备提前挂起影响子设备。

最后,遇到奇怪的硬件异常时,先把 runtime PM 关掉并强制设备 active,跑一遍业务流程。如果问题消失了,大概率是 PM 挂起/恢复的时序或者状态保存出了问题。这套思路在我手上定位过至少四五个疑难杂症,比单纯看代码逻辑要快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询