3、设备驱动休眠回调:dev_pm_ops结构体、suspend/resume回调函数的注册与执行顺序
驱动层的休眠回调,是整个休眠唤醒流程里最贴近硬件的环节。说白了,dev_pm_ops 就是内核给驱动开发者留的一扇门——你想让设备在休眠前做什么、唤醒后做什么,都通过这扇门告诉内核。
我刚开始接触电源管理时,总觉得这玩意儿不就是注册几个回调函数嘛,能有多复杂?直到有一次,一个网卡驱动在休眠后死活唤不醒,查了三天才发现是回调注册错了位置。嗯,从那以后我再也不敢小看这个结构体了。
3.1 dev_pm_ops 结构体:驱动休眠的“控制面板”
dev_pm_ops 定义在 linux/pm.h 中,它包含了系统休眠和运行时电源管理所需的全部回调函数指针。我个人习惯把它理解成一张“菜单”——内核在合适的时机,会按顺序“点菜”。
先看核心定义:
struct dev_pm_ops {
int (*suspend)(struct device *dev);
int (*resume)(struct device *dev);
int (*freeze)(struct device *dev);
int (*thaw)(struct device *dev);
int (*poweroff)(struct device *dev);
int (*restore)(struct device *dev);
int (*suspend_late)(struct device *dev);
int (*resume_early)(struct device *dev);
int (*freeze_late)(struct device *dev);
int (*thaw_early)(struct device *dev);
int (*poweroff_late)(struct device *dev);
int (*restore_early)(struct device *dev);
int (*suspend_noirq)(struct device *dev);
int (*resume_noirq)(struct device *dev);
int (*freeze_noirq)(struct device *dev);
int (*thaw_noirq)(struct device *dev);
int (*poweroff_noirq)(struct device *dev);
int (*restore_noirq)(struct device *dev);
int (*runtime_suspend)(struct device *dev);
int (*runtime_resume)(struct device *dev);
int (*runtime_idle)(struct device *dev);
};
你可能会问:怎么这么多回调?其实它们分成了三组:
| 阶段 | 回调函数 | 说明 |
|---|---|---|
| suspend/resume | suspend → suspend_late → suspend_noirq |
标准休眠/唤醒路径,中断逐渐关闭 |
| freeze/thaw | freeze → freeze_late → freeze_noirq |
用于 hibernation(休眠到磁盘)的快照阶段 |
| poweroff/restore | poweroff → poweroff_late → poweroff_noirq |
系统关机时的电源管理路径 |
关键点:大多数驱动只需要实现 suspend 和 resume 就够了。只有那些对时序敏感的硬件(比如 I2C 控制器、DMA 引擎)才需要用到 _late 和 _noirq 版本。
3.2 回调函数的注册方式
注册方式有两种,我建议你优先用第二种——SET_SYSTEM_SLEEP_PM_OPS 宏。
方式一:手动赋值
static const struct dev_pm_ops mydev_pm_ops = {
.suspend = mydev_suspend,
.resume = mydev_resume,
};
方式二:使用宏(推荐)
static const struct dev_pm_ops mydev_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(mydev_suspend, mydev_resume)
};
这个宏的好处是:当内核配置了 CONFIG_PM_SLEEP 时,它才会展开成实际的赋值;否则就是空操作。你想想看,如果你的驱动不需要休眠功能,这个宏能让代码更干净。
最后,把 dev_pm_ops 挂到驱动结构体上:
static struct platform_driver mydev_driver = {
.driver = {
.name = "my_device",
.pm = &mydev_pm_ops,
},
.probe = mydev_probe,
.remove = mydev_remove,
};
我的习惯:在 probe 函数里用 dev_pm_set_driver_flags() 明确告诉内核你的设备支持哪些电源管理状态。这能避免一些奇怪的兼容性问题。
3.3 回调执行顺序:谁先睡,谁先醒?
这是面试常考题,也是实际调试中最容易踩坑的地方。执行顺序分两个维度:设备间的顺序 和 设备内的顺序。
3.3.1 设备间的顺序:依赖关系决定一切
内核按照设备的 初始化顺序 反向执行休眠。什么意思呢?
- 后注册的设备,先进入 suspend
- 先注册的设备,后进入 suspend
唤醒时正好相反:先 suspend 的设备,后 resume;后 suspend 的设备,先 resume。
举个例子:
注册顺序: A → B → C
休眠顺序: C → B → A
唤醒顺序: A → B → C
为什么会这样?因为设备之间有依赖关系。比如 I2C 控制器必须在 I2C 设备之前唤醒,否则设备无法与控制器通信。内核通过 device_links 机制来管理这种依赖,但默认情况下,它就按初始化顺序的逆序来。
我曾经踩过的坑:一个 MMC 驱动依赖 GPIO 控制器,但 GPIO 控制器注册得比 MMC 晚。结果休眠时 GPIO 先睡了,MMC 后睡——看起来没问题。但唤醒时 MMC 先醒,它想拉一个 GPIO 引脚,结果 GPIO 控制器还没醒,直接返回 -EPERM。解决方案是用 device_link_add() 显式声明依赖关系。
3.3.2 设备内的顺序:三个阶段,层层递进
每个设备的休眠回调分三个阶段执行:
- suspend(早期阶段):设备仍可响应中断,可以执行一些简单的操作,比如保存寄存器、停止数据传输。
- suspend_late(晚期阶段):中断仍然可用,但内核已经停止了大部分调度活动。适合执行一些需要原子上下文的操作。
- suspend_noirq(无中断阶段):所有中断都被屏蔽。这是最后一道防线,通常只用来设置硬件进入最低功耗模式。
唤醒时反过来:resume_noirq → resume_early → resume。
我画了一张图,帮你理清这个流程:
3.4 回调函数的实现要点
写回调函数时,有几个原则我建议你刻在脑子里:
- 不要睡眠:suspend 回调里不能调用
msleep()或wait_event(),因为此时内核的调度器可能已经停止。用mdelay()代替。 - 保存/恢复要成对:suspend 里保存的寄存器,resume 里一定要恢复。我见过有人只保存不恢复,结果设备唤醒后工作异常。
- 错误处理要干净:如果 suspend 失败,返回负的错误码。内核会尝试恢复已经 suspend 的设备。但注意——如果你的设备已经进入了某种不可逆的低功耗状态,返回错误也没用。
一个典型的 suspend/resume 实现:
static int mydev_suspend(struct device *dev)
{
struct mydev *myd = dev_get_drvdata(dev);
if (!myd)
return -EINVAL;
/* 保存硬件状态 */
myd->saved_regs.ctrl = readl(myd->base + CTRL_REG);
myd->saved_regs.status = readl(myd->base + STATUS_REG);
/* 停止硬件工作 */
writel(0, myd->base + CTRL_REG);
dev_dbg(dev, "device suspended\n");
return 0;
}
static int mydev_resume(struct device *dev)
{
struct mydev *myd = dev_get_drvdata(dev);
if (!myd)
return -EINVAL;
/* 恢复硬件状态 */
writel(myd->saved_regs.ctrl, myd->base + CTRL_REG);
writel(myd->saved_regs.status, myd->base + STATUS_REG);
dev_dbg(dev, "device resumed\n");
return 0;
}
3.5 调试技巧:如何验证回调是否被正确调用?
调试驱动休眠回调,我最常用的方法就三个:
- 加 dev_dbg 日志:在 suspend/resume 的入口和出口各加一条。配合
dynamic_debug可以在运行时控制日志输出。 - 看 /sys/kernel/debug/suspend_stats:这个文件记录了每次休眠的统计信息,包括每个设备的 suspend/resume 耗时和错误码。
- 用 ftrace 跟踪:设置
echo suspend_resume > /sys/kernel/debug/tracing/set_ftrace_filter,然后看 trace 输出。你能看到每个回调的精确调用时间和执行时长。
我的调试习惯:在开发阶段,我会在 suspend 回调里故意返回 -EBUSY,然后看内核怎么处理。这能帮你验证错误恢复路径是否正常。当然,记得在产品发布前去掉这个测试代码。
嗯,关于 dev_pm_ops 的核心内容就这些。记住一句话:回调函数的注册决定了内核能不能找到你的驱动,而执行顺序决定了你的设备能不能稳定地睡下和醒来。这两点搞清楚了,驱动层的休眠唤醒就成功了一大半。