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 设备内的顺序:三个阶段,层层递进

每个设备的休眠回调分三个阶段执行:

  1. suspend(早期阶段):设备仍可响应中断,可以执行一些简单的操作,比如保存寄存器、停止数据传输。
  2. suspend_late(晚期阶段):中断仍然可用,但内核已经停止了大部分调度活动。适合执行一些需要原子上下文的操作。
  3. suspend_noirq(无中断阶段):所有中断都被屏蔽。这是最后一道防线,通常只用来设置硬件进入最低功耗模式。

唤醒时反过来:resume_noirq → resume_early → resume。

我画了一张图,帮你理清这个流程:

设备休眠唤醒回调执行顺序 休眠路径 (Suspend) suspend() suspend_late() suspend_noirq() 唤醒路径 (Resume) resume_noirq() resume_early() resume() 设备间执行顺序(假设注册顺序:A → B → C) 休眠:C → B → A(后注册的先睡) 唤醒:A → B → C(先注册的先醒) 每个设备内部:suspend 按三个阶段依次执行,resume 反向执行 中断状态:suspend 阶段中断可用 → suspend_late 阶段中断受限 → suspend_noirq 阶段中断关闭 唤醒时中断状态逐步恢复:noirq → early → 正常中断

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 调试技巧:如何验证回调是否被正确调用?

调试驱动休眠回调,我最常用的方法就三个:

  1. 加 dev_dbg 日志:在 suspend/resume 的入口和出口各加一条。配合 dynamic_debug 可以在运行时控制日志输出。
  2. 看 /sys/kernel/debug/suspend_stats:这个文件记录了每次休眠的统计信息,包括每个设备的 suspend/resume 耗时和错误码。
  3. 用 ftrace 跟踪:设置 echo suspend_resume > /sys/kernel/debug/tracing/set_ftrace_filter,然后看 trace 输出。你能看到每个回调的精确调用时间和执行时长。

我的调试习惯:在开发阶段,我会在 suspend 回调里故意返回 -EBUSY,然后看内核怎么处理。这能帮你验证错误恢复路径是否正常。当然,记得在产品发布前去掉这个测试代码。

嗯,关于 dev_pm_ops 的核心内容就这些。记住一句话:回调函数的注册决定了内核能不能找到你的驱动,而执行顺序决定了你的设备能不能稳定地睡下和醒来。这两点搞清楚了,驱动层的休眠唤醒就成功了一大半。