4、内核电源管理核心数据结构:struct dev_pm_ops、struct dev_pm_domain、power management workqueue等

Linux内核的电源管理框架并非一套松散的函数集合,而是建立在若干核心数据结构之上的精密工程体系。对于驱动开发者而言,struct dev_pm_ops是设备级电源管理的“操作契约”,struct dev_pm_domain是设备域级电源管理的“策略容器”,而电源管理专用工作队列(PM workqueue)则是异步挂起/恢复操作的“调度中枢”。理解这三者的设计意图、协作关系与使用边界,是深入剖析休眠唤醒机制的关键前提。本章将逐一拆解这些数据结构的字段语义、注册流程与实战陷阱,并给出可落地的工程判断方法。

Linux电源管理核心数据结构协作关系 struct device (嵌入于具体设备结构体) driver = &xxx_driver platform_data / of_node pm = &xxx_pm_ops pm_domain = &xxx_domain struct dev_pm_ops (设备级回调集合) .suspend / .resume .suspend_late / .resume_early .runtime_suspend / .runtime_resume struct dev_pm_domain (域级策略容器) ops = &domain_pm_ops detach / activate / sync device_initialize / device_add PM Workqueue (pm_wq) 异步挂起/恢复/运行时PM操作调度 dpm_suspend() dpm_resume() pm_runtime_*() 指针引用 域覆盖 回调查找 调度 域回调 电源管理核心:设备驱动注册pm_ops → 域提供策略覆盖 → 工作队列异步执行 设备层 PM Ops PM Domain Workqueue

上图展示了三个核心数据结构在电源管理框架中的位置与协作关系。设备驱动通过struct device中的pm字段挂载dev_pm_ops,而dev_pm_domain则作为更高层次的策略容器,可以覆盖或扩展设备级的回调。最终,所有挂起/恢复操作都会经由PM工作队列进行异步调度,避免在原子上下文中执行耗时操作。下面我们逐一深入分析。

4.1 struct dev_pm_ops:设备级电源回调的操作契约

核心原理:回调矩阵与执行阶段

struct dev_pm_ops定义在include/linux/pm.h中,是设备驱动与电源管理核心之间的“函数指针表”。它按照系统级休眠(suspend/resume)和运行时电源管理(runtime PM)两条主线组织回调,每条主线又细分为多个阶段。系统级休眠的回调顺序遵循“prepare → suspend → suspend_late → suspend_noirq”的递进关系,恢复则反向执行“resume_noirq → resume_early → resume → complete”。这种分层设计允许驱动在不同阶段处理不同优先级的事务:suspend阶段适合保存寄存器状态、停止数据传输;suspend_late阶段适合关闭中断、停止时钟;suspend_noirq阶段则只能进行最基础的内存操作。

运行时PM回调(runtime_suspend/runtime_resume/runtime_idle)则独立于系统休眠流程,由设备的使用状态触发。例如,当设备引用计数降为零时,内核会调用runtime_idle,驱动可选择进入低功耗状态或直接调用pm_runtime_suspend。值得注意的是,系统休眠过程中会先暂停所有运行时PM(通过pm_runtime_disable),恢复后再重新启用,以避免两者并发冲突。

判断方法:如何确定驱动需要实现哪些回调

并非所有回调都必须实现。内核提供了SET_SYSTEM_SLEEP_PM_OPS和SET_RUNTIME_PM_OPS宏来简化赋值,未实现的回调会被自动填充为NULL。判断驱动需要哪些回调,可遵循以下步骤:

  1. 检查设备是否支持唤醒:若设备需要作为唤醒源(如网卡、触摸屏),必须实现suspend和resume,并在suspend中调用enable_irq_wake。
  2. 检查设备是否有硬件状态需要保存:若设备有寄存器配置、FIFO数据或DMA描述符,必须在suspend_late或suspend阶段保存。
  3. 检查设备是否支持运行时电源管理:若设备支持动态开关时钟或电源域,应实现runtime_suspend/runtime_resume,并配合自动挂起(autosuspend)机制。
  4. 检查设备是否在noirq阶段有特殊需求:若设备与中断控制器紧密耦合(如GPIO控制器),可能需要suspend_noirq/resume_noirq。
经验提醒:对于大多数I2C/SPI外设,实现suspend和resume即可满足需求。不要盲目实现所有回调,多余的noirq回调会增加休眠延迟,且容易引入竞态条件。

案例拆解:一个I2C触摸控制器的PM回调实现

假设某I2C触摸控制器(设备地址0x38)需要支持系统休眠和运行时PM。其驱动初始化时注册dev_pm_ops如下:

static int touch_suspend(struct device *dev) {
    struct touch_chip *chip = dev_get_drvdata(dev);
    // 1. 保存触摸屏校准数据到内存
    memcpy(chip->calib_backup, chip->calib, sizeof(chip->calib));
    // 2. 关闭触摸中断
    disable_irq(chip->irq);
    // 3. 将芯片置于低功耗模式(通过I2C写寄存器)
    i2c_smbus_write_byte_data(chip->client, 0x01, 0x00);
    return 0;
}

static int touch_resume(struct device *dev) {
    struct touch_chip *chip = dev_get_drvdata(dev);
    // 1. 唤醒芯片
    i2c_smbus_write_byte_data(chip->client, 0x01, 0x01);
    // 2. 恢复校准数据
    memcpy(chip->calib, chip->calib_backup, sizeof(chip->calib));
    // 3. 重新使能中断
    enable_irq(chip->irq);
    return 0;
}

static int touch_runtime_suspend(struct device *dev) {
    // 关闭内部时钟,断开I2C时钟请求
    return 0;
}

static int touch_runtime_resume(struct device *dev) {
    // 重新使能内部时钟
    return 0;
}

static const struct dev_pm_ops touch_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(touch_suspend, touch_resume)
    SET_RUNTIME_PM_OPS(touch_runtime_suspend, touch_runtime_resume, NULL)
};

在系统休眠时,内核会先调用touch_suspend保存状态并关闭中断;恢复时调用touch_resume恢复状态。运行时PM则通过pm_runtime_put_sync和pm_runtime_get_sync触发。该案例中,suspend阶段执行了I2C操作,这是允许的,因为此时I2C控制器尚未挂起。若在suspend_late阶段执行I2C操作,则可能因I2C控制器已挂起而导致总线错误。

风险提示:在suspend_late及之后的阶段,I2C/SPI总线可能已不可用。若驱动必须在这些阶段访问硬件,应使用regmap的cache_only模式,或提前在suspend阶段完成所有硬件访问。

4.2 struct dev_pm_domain:域级电源策略的容器

核心原理:从设备到域的抽象提升

struct dev_pm_domain定义于include/linux/pm.h,它允许将一组设备纳入统一的电源管理策略。典型的应用场景包括:SoC中的GPU、VPU、ISP等IP核共享一个电源域;或者多个外设挂在同一个PMIC的LDO上。域结构体包含一个struct dev_pm_ops ops成员,以及detach、activate、sync等操作函数。当设备注册到内核时,若其pm_domain字段非空,电源管理核心会优先调用域的回调,而不是设备驱动自身的pm_ops。这种“域覆盖设备”的设计,使得平台级电源管理代码可以统一控制一组设备的电源状态,而不需要每个驱动单独实现。

域的回调通常由平台电源管理驱动(如drivers/base/power/domain.c中的genpd)实现。通用电源域框架(Generic PM Domain, genpd)是dev_pm_domain最典型的实现,它提供了pm_genpd_init、pm_genpd_add_device等API,支持设备状态跟踪、延迟挂起和唤醒源管理。

判断方法:何时使用dev_pm_domain而非直接实现pm_ops

场景特征推荐方案理由
多个设备共享同一电源开关dev_pm_domain域可以统一控制电源开关,避免每个驱动重复操作
设备有独立的时钟和电源控制dev_pm_ops驱动自行管理更灵活,不依赖域框架
设备需要参与系统级休眠顺序控制dev_pm_domain + genpdgenpd支持设备间依赖关系(如GPU需先于显示控制器挂起)
设备支持运行时PM且需要自动挂起dev_pm_ops + autosuspend运行时PM的自动挂起机制与域框架集成度较低

案例拆解:基于genpd的GPU电源域管理

某移动SoC的GPU与VPU共享一个电源域(VDD_GPU),由PMIC的LDO供电。平台驱动创建genpd并添加设备:

static struct generic_pm_domain gpu_domain = {
    .name = "VDD_GPU",
    .power_off = gpu_domain_power_off,
    .power_on = gpu_domain_power_on,
    .states = &gpu_domain_states[0],
    .state_count = ARRAY_SIZE(gpu_domain_states),
};

static int platform_probe(struct platform_device *pdev) {
    struct device *gpu_dev = &pdev->dev;
    pm_genpd_init(&gpu_domain, NULL, false);
    pm_genpd_add_device(&gpu_domain, gpu_dev);
    return 0;
}

当GPU驱动调用pm_runtime_put_sync后,genpd会检查域内所有设备是否都已挂起,若是则调用gpu_domain_power_off关闭LDO。该方案的优势在于:GPU驱动无需关心LDO的具体操作,只需实现自己的runtime_suspend回调来停止内部时钟即可。域框架自动处理了设备计数和电源开关时序。

关键点:当设备同时具有pm_domain和pm_ops时,域的回调优先执行。域回调内部通常会调用设备的pm_ops(通过pm_generic_suspend等辅助函数),形成“域→设备”的调用链。驱动开发者不应假设自己的pm_ops会被直接调用,而应通过dev_pm_domain_attach确认设备是否已绑定到某个域。

4.3 电源管理工作队列(PM Workqueue)

核心原理:异步化的必要性

系统休眠过程中,dpm_suspend会遍历所有设备并调用其suspend回调。如果某个设备的suspend回调需要等待硬件完成操作(如等待DMA传输结束),同步等待会阻塞整个休眠流程,导致系统响应变慢。为此,内核提供了电源管理专用工作队列pm_wq(定义于kernel/power/main.c),允许驱动将耗时操作放入工作队列异步执行。工作队列运行在进程上下文,可以睡眠,因此适合执行I2C读写、等待中断等操作。

PM工作队列与普通工作队列(system_wq)的关键区别在于:pm_wq在休眠过程中会被冻结(freeze),确保没有新的工作项在设备挂起期间启动。驱动应使用pm_schedule_work而非schedule_work来提交电源管理相关任务,以保证任务在正确的电源状态上下文中执行。

操作流程:如何正确使用PM工作队列

  1. 初始化:在驱动探测时使用INIT_WORK(&dev->pm_work, pm_work_handler)初始化工作项。
  2. 提交:在需要异步执行的操作中调用pm_schedule_work(&dev->pm_work)。
  3. 取消:在suspend回调中调用cancel_work_sync(&dev->pm_work)确保没有未完成的工作项。
  4. 清理:在remove回调中再次调用cancel_work_sync并释放相关资源。

案例拆解:网卡驱动的异步唤醒处理

某以太网卡支持WOL(Wake-on-LAN),在休眠前需要将固件下载到网卡。固件下载耗时约200ms,若同步执行会显著增加休眠时间。驱动采用PM工作队列异步处理:

static void nic_fw_download_work(struct work_struct *work) {
    struct nic_dev *nic = container_of(work, struct nic_dev, fw_work);
    // 执行固件下载(I2C/SPI操作,可睡眠)
    nic_write_firmware(nic);
    // 下载完成后使能WOL
    nic_enable_wol(nic);
}

static int nic_suspend(struct device *dev) {
    struct nic_dev *nic = dev_get_drvdata(dev);
    // 取消未完成的工作
    cancel_work_sync(&nic->fw_work);
    // 提交固件下载任务到PM工作队列
    pm_schedule_work(&nic->fw_work);
    return 0;
}

该设计使休眠流程不必等待固件下载完成,系统可以继续挂起其他设备。但需要注意:suspend回调返回后,设备可能立即进入D3状态,此时固件下载可能尚未完成。因此,驱动必须在suspend_late阶段调用flush_work等待工作完成,或者确保网卡硬件在固件未就绪时不会进入低功耗状态。实际工程中,更稳妥的做法是在suspend阶段同步下载固件,但将下载过程拆分为多个小步骤,每步之间让出CPU。

常见误区:不要使用schedule_work替代pm_schedule_work。普通工作队列在休眠过程中不会被冻结,可能导致工作项在设备挂起后仍访问硬件,引发总线错误或数据损坏。此外,cancel_work_sync必须在suspend回调中调用,而不是在suspend_late中,因为后者可能处于原子上下文。

4.4 三者的协作机制与工程实践

核心原理:调用链的完整路径

当系统执行echo mem > /sys/power/state时,电源管理核心会依次执行:dpm_prepare(调用所有设备的prepare回调)→ dpm_suspend(调用suspend回调)→ dpm_suspend_late → dpm_suspend_noirq。在每个阶段,核心会先检查设备的pm_domain是否存在,若存在则调用域的回调;否则调用设备自身的pm_ops。域回调内部通常会调用pm_generic_suspend,该函数会进一步调用设备的pm_ops中的对应回调。因此,一个设备的完整挂起调用链为:dev_pm_domain->ops->suspend → dev->pm->suspend。

判断方法:调试电源管理问题的切入点

当设备无法正常挂起或恢复时,可按以下顺序排查:

  • 检查设备是否绑定了pm_domain:通过/sys/kernel/debug/pm_genpd/pm_genpd_summary查看设备所属域。
  • 检查pm_ops是否注册成功:在驱动探测时打印dev->pm指针是否非空。
  • 检查工作队列状态:通过cat /proc/workqueue查看pm_wq是否有挂起的工作项。
  • 检查回调执行顺序:在suspend和resume回调中添加dev_info打印,确认调用顺序是否符合预期。

案例拆解:一个典型的休眠失败排查过程

某嵌入式设备在休眠时出现“系统挂起后无法唤醒”的问题。排查过程如下:

步骤操作发现
1查看dmesg中的PM错误无明确错误,但发现PM: suspend entry后系统卡死
2检查设备树中设备的power-domains属性发现GPU设备未绑定任何电源域
3检查GPU驱动是否注册了pm_ops驱动未实现suspend回调,导致GPU在休眠时未关闭电源
4在GPU驱动中添加suspend回调,关闭GPU时钟问题解决,系统可正常唤醒

该案例表明,设备未绑定电源域且驱动未实现pm_ops时,内核会跳过该设备的电源管理操作,导致硬件状态不一致。通过dev_pm_domain_attach将设备绑定到genpd,或实现驱动级pm_ops,是解决问题的两种途径。

总结复盘:在Linux电源管理框架中,dev_pm_ops是驱动与内核的“契约”,dev_pm_domain是平台级策略的“容器”,PM工作队列是异步操作的“调度器”。三者的正确协作需要驱动开发者理解:①回调的执行阶段与上下文限制;②域覆盖设备的优先级规则;③工作队列的冻结机制。建议在驱动开发初期就明确设备的电源管理需求,绘制一张“设备→域→工作队列”的映射表,避免在休眠路径上引入不必要的同步等待或竞态条件。