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: suspendPM: noirq 开头的日志,它们会告诉你卡在哪一步。
  • 测试依赖关系:如果你的设备有明确的依赖关系(比如 A 依赖 B 的时钟),一定要在设备树里用 power-domainsclocks 属性声明。否则休眠顺序可能出错。
  • 注意唤醒源:在 syscore 阶段,确保唤醒源对应的 GPIO 或中断控制器没有被关闭。否则你按了唤醒按钮,系统也醒不过来。
  • 模拟异常:我习惯在开发阶段,故意让某个设备的 suspend 回调返回错误,测试系统的回滚机制是否正常。这能帮你提前发现资源泄漏或死锁问题。

好了,这一章的内容就到这里。下一章我们会讲休眠唤醒的另一个关键部分——唤醒源的配置与处理。到时候我会结合一个具体的车载项目案例,带你看看实际开发中会遇到哪些坑。