4、休眠流程实现:系统Suspend入口函数(suspend_enter)、设备驱动suspend回调执行顺序、NoIRQ阶段与syscore阶段
好,咱们接着聊休眠流程的具体实现。上一章我们把整个休眠的框架搭起来了,从用户空间的 echo mem > /sys/power/state 一路追到了内核的 suspend_enter。这一章,我们就深入到这个函数内部,看看它到底干了什么。
说白了,suspend_enter 就是整个休眠流程的「总指挥」。它负责协调所有设备、所有子系统,按部就班地进入低功耗状态。我个人习惯把这一章的内容分成三个部分来理解:入口函数本身、设备驱动的回调顺序、以及两个特殊阶段——NoIRQ 和 syscore。
4.1 suspend_enter:休眠的「总指挥」
先看入口函数。在 kernel/power/suspend.c 里,suspend_enter 的代码其实不长,但每一步都很关键。我把它简化一下,核心流程是这样的:
static int suspend_enter(suspend_state_t state, bool *wakeup)
{
int error;
// 1. 冻结用户空间进程和内核线程
error = suspend_freeze_processes();
if (error)
goto Freeze;
// 2. 调用平台相关的准备函数
error = suspend_ops->prepare();
if (error)
goto Platform_prepare;
// 3. 暂停所有设备(正常阶段)
error = dpm_suspend(PMSG_SUSPEND);
if (error)
goto Devices_suspend;
// 4. 关闭非引导CPU
error = suspend_disable_secondary_cpus();
if (error)
goto Enable_cpus;
// 5. 设备进入NoIRQ阶段
error = dpm_suspend_noirq(PMSG_SUSPEND);
if (error)
goto Platform_wake;
// 6. syscore阶段:暂停核心系统组件
error = syscore_suspend();
if (error)
goto Syscore_suspend;
// 7. 平台进入休眠(写寄存器、切电源等)
error = suspend_ops->enter(state);
if (!error)
*wakeup = true;
// 8. 恢复流程(反向操作)
syscore_resume();
dpm_resume_noirq(PMSG_RESUME);
suspend_enable_secondary_cpus();
dpm_resume(PMSG_RESUME);
suspend_ops->finish();
suspend_thaw_processes();
return error;
}
嗯,这里要注意,每一步都有对应的错误处理。一旦某一步失败,系统会立即回滚到上一个稳定状态。我曾经在调试一个车载项目时,就因为某个网卡驱动在 dpm_suspend 阶段超时,导致整个休眠流程卡死。排查了半天才发现是驱动里有个自旋锁没释放。
4.2 设备驱动suspend回调执行顺序
设备驱动的 suspend 回调,不是随便乱调的。内核有一套严格的顺序,说白了就是「先注册的先休眠,后注册的后休眠」——但这是针对同一优先级的设备。
实际上,设备 suspend 的顺序由两个因素决定:
- 设备注册顺序:同一总线上,后注册的设备先 suspend(LIFO 原则)
- 设备依赖关系:通过
device_links机制,消费者先于供应商 suspend
你想想看,为什么消费者要先 suspend?因为消费者依赖供应商提供的资源(比如时钟、电源)。如果供应商先关了,消费者还在操作硬件,那就出大问题了。我在项目中遇到过类似的情况:一个 SPI 控制器(供应商)和一个 SPI 触摸屏(消费者),因为依赖关系没配好,休眠时 SPI 控制器先关了,触摸屏在 suspend 回调里写寄存器直接写飞了。
具体到回调函数,每个设备驱动需要实现 dev_pm_ops 结构体:
static const struct dev_pm_ops my_device_pm_ops = {
.suspend = my_device_suspend,
.suspend_noirq = my_device_suspend_noirq,
.resume = my_device_resume,
.resume_noirq = my_device_resume_noirq,
};
内核在 dpm_suspend 阶段调用 .suspend,在 dpm_suspend_noirq 阶段调用 .suspend_noirq。这两个阶段的区别,我们马上讲。
4.3 NoIRQ阶段:关中断后的「最后冲刺」
NoIRQ 阶段,顾名思义,就是「没有中断」的阶段。在这个阶段之前,系统已经关闭了所有非引导 CPU,并且禁止了大部分中断。为什么要有这个阶段?
说白了,有些设备在正常 suspend 阶段(有中断时)不能完成所有操作。比如一些 DMA 控制器、定时器,它们需要在完全无中断干扰的环境下,做最后的寄存器保存工作。
在 dpm_suspend_noirq 阶段,内核会遍历所有设备的 .suspend_noirq 回调。这个回调的执行顺序和 dpm_suspend 阶段一样,也是 LIFO + 依赖关系。
关键点:NoIRQ 阶段是设备休眠的「最后一道防线」。如果这个阶段失败了,系统会尝试恢复,但恢复的成功率不高。我建议你在调试时,重点关注这个阶段的日志,用 dmesg 查看是否有设备返回错误。
4.4 syscore阶段:内核的「最后心跳」
NoIRQ 阶段之后,就是 syscore 阶段。这个阶段更底层,它负责暂停那些「内核核心子系统」——比如时钟管理、中断控制器、定时器等等。
syscore 的 suspend 回调是通过 register_syscore_ops 注册的:
static struct syscore_ops my_syscore_ops = {
.suspend = my_syscore_suspend,
.resume = my_syscore_resume,
};
static int __init my_syscore_init(void)
{
register_syscore_ops(&my_syscore_ops);
return 0;
}
syscore 阶段的执行顺序和设备驱动不同——它是按照注册顺序执行的(FIFO)。为什么?因为 syscore 组件之间通常没有复杂的依赖关系,而且数量很少,用 FIFO 更简单可靠。
我记得有一次调试一个车载娱乐系统,休眠后无法唤醒。查了三天,最后发现是 syscore 阶段里,某个 GPIO 控制器的 suspend 回调把唤醒源对应的引脚电平给改了。嗯,从那以后我每次写 syscore 回调,都会再三确认不会影响唤醒逻辑。
4.5 三个阶段对比
为了方便你理解,我把这三个阶段整理成了一张表:
| 阶段 | 中断状态 | 执行顺序 | 典型操作 | 失败后果 |
|---|---|---|---|---|
| dpm_suspend | 中断使能 | LIFO + 依赖关系 | 保存寄存器、停止数据传输 | 可恢复,影响不大 |
| dpm_suspend_noirq | 中断禁止 | LIFO + 依赖关系 | DMA停止、时钟关闭 | 恢复困难,可能死锁 |
| syscore_suspend | 中断禁止 | FIFO(注册顺序) | 系统时钟、中断控制器休眠 | 系统可能无法唤醒 |
避坑指南:我曾经在调试一个项目时,发现某个设备在 dpm_suspend_noirq 阶段返回了 -EBUSY。原因是该设备的 .suspend_noirq 回调里尝试获取一个已经被其他设备占用的锁。记住:NoIRQ 阶段不能使用任何可能引起睡眠的锁(比如 mutex),只能用自旋锁。
4.6 实战建议
最后,给你几个我在实际项目中总结的建议:
- 先看日志:休眠失败时,第一时间看
dmesg。重点关注PM: suspend和PM: noirq开头的日志,它们会告诉你卡在哪一步。 - 测试依赖关系:如果你的设备有明确的依赖关系(比如 A 依赖 B 的时钟),一定要在设备树里用
power-domains或clocks属性声明。否则休眠顺序可能出错。 - 注意唤醒源:在 syscore 阶段,确保唤醒源对应的 GPIO 或中断控制器没有被关闭。否则你按了唤醒按钮,系统也醒不过来。
- 模拟异常:我习惯在开发阶段,故意让某个设备的 suspend 回调返回错误,测试系统的回滚机制是否正常。这能帮你提前发现资源泄漏或死锁问题。
好了,这一章的内容就到这里。下一章我们会讲休眠唤醒的另一个关键部分——唤醒源的配置与处理。到时候我会结合一个具体的车载项目案例,带你看看实际开发中会遇到哪些坑。