1. 唤醒源框架到底在解决什么问题第一次接触wakeup source的人大多是在调试一个“设备明明该睡了却总是被莫名其妙唤醒”的问题。你可能会在dmesg里看到类似PM: Wakeup source ...的打印也可能在/sys/kernel/debug/wakeup_sources里看到一堆名字和计数但真要问“这个框架是怎么运转的、我写的驱动该怎么接进去”很多人就卡住了。wakeup source是 Linux 电源管理PM Core里一个非常核心但又容易被忽略的子系统。它要解决的核心问题只有一个当系统准备进入低功耗状态尤其是 suspend 或 autosleep时内核必须知道“当前有没有事件正在阻止睡眠”以及“是谁把系统从睡眠里叫醒的”。没有这套机制系统要么睡不下去要么睡下去之后被某个设备反复唤醒却查不出原因功耗优化就无从谈起。这套框架适合三类人深入一是做嵌入式 Linux 的驱动工程师你的设备GPIO 按键、触摸屏、传感器、WiFi 模块几乎都要注册 wakeup source二是做系统功耗优化的工程师你需要靠它定位“谁在偷偷唤醒系统”三是准备内核/驱动方向面试的人wakeup source和autosleep、runtime PM的关系是高频考点。我下面会从设计思路、核心数据结构、注册与使用流程、事件上报机制、和 autosleep 的联动一直到实际调试中踩过的坑完整梳理一遍。内容基于内核通用实现以drivers/base/power/wakeup.c为主线不同版本细节会有差异但主干逻辑是一致的。2. 框架整体设计与思路拆解2.1 为什么需要“唤醒源”这一层抽象在没有统一框架之前每个子系统各自维护“我能不能睡”的状态比如某个驱动自己搞一个标志位suspend 时去检查。问题是设备多了之后谁也没法全局判断而且“谁唤醒了我”这个信息完全丢失。内核的做法是引入一个全局的、带引用计数的唤醒事件管理机制把“阻止睡眠”这件事抽象成一个可计数、可追踪的对象也就是struct wakeup_source。它的设计哲学可以类比成“会议室占用牌”每个可能阻止系统睡眠的设备都拿一块牌子。只要有一块牌子是“占用”状态系统就不能进会议室不能 suspend。当所有牌子都放下系统才能睡。而每次有人推门进来唤醒事件门卫会记一笔“是谁推的门”方便事后追责。这个抽象带来三个直接好处第一全局可见PM Core 能统一判断能否睡眠第二可追踪每个唤醒源的激活次数、总时长、最后一次激活时间都有统计第三可组合一个设备可以注册多个唤醒源也可以多个设备共享一个。2.2 两种唤醒源的区分设备唤醒源与逻辑唤醒源实际使用中唤醒源大致分两类。一类是设备唤醒源直接对应一个物理设备比如struct device里的wakeup字段通过device_init_wakeup()和device_set_wakeup_enable()来管理。这类唤醒源和设备的power/wakeupsysfs 属性绑定用户空间可以通过echo enabled /sys/.../power/wakeup来开关。另一类是逻辑唤醒源它不一定对应具体设备而是代表某类事件比如wakeup_source_register(NULL, eventpoll)这种常见于epoll、alarmtimer、input子系统。逻辑唤醒源更灵活适合那些“事件来源不是单一设备”的场景。注意设备唤醒源在注册时会自动关联到设备的dev-power.wakeup而逻辑唤醒源需要自己管理生命周期。混用两者容易导致引用计数错乱这是新手最常见的坑之一。2.3 与 runtime PM、autosleep 的边界很多人会把wakeup source和runtime PM搞混。简单说runtime PM管的是单个设备在系统运行期间的空闲休眠粒度是设备级而wakeup source管的是整个系统能否进入 suspend粒度是系统级。两者会协作一个设备 runtime suspend 了但如果它使能了 wakeup它仍然可以成为系统唤醒源。autosleep则是建立在wakeup source之上的“自动睡眠”机制。它的逻辑是当没有任何激活的唤醒源时自动触发 suspend。所以wakeup source的计数是否准确直接决定 autosleep 会不会误睡或睡不着。这也是为什么调试 autosleep 问题时第一步永远是看/sys/kernel/debug/wakeup_sources。3. 核心数据结构与关键字段解析3.1 struct wakeup_source 的字段含义这个结构体定义在include/linux/pm_wakeup.h核心字段如下不同版本略有增减struct wakeup_source { const char *name; struct list_head entry; spinlock_t lock; struct wakeup_irq_node *wakeirq; struct timer_list timer; unsigned long timer_expires; ktime_t total_time; ktime_t max_time; ktime_t last_time; ktime_t start_prevent_time; ktime_t prevent_sleep_time; unsigned long event_count; unsigned long active_count; unsigned long relax_count; unsigned long expire_count; unsigned long wakeup_count; bool active:1; bool autosleep_enabled:1; };几个字段值得单独说。active表示当前是否处于激活状态也就是“是否正在阻止睡眠”。event_count是事件上报次数active_count是激活次数relax_count是主动释放次数wakeup_count是唤醒系统次数。total_time和max_time用来统计激活总时长和单次最长时长是功耗分析的关键数据。prevent_sleep_time和start_prevent_time是后来加入的用来统计“因为该唤醒源导致系统无法睡眠”的累计时间对定位“谁在长期阻止睡眠”特别有用。3.2 全局链表与锁的保护所有唤醒源通过entry挂到全局链表wakeup_sources上由wakeup_sources_lock保护。这个链表是调试接口/sys/kernel/debug/wakeup_sources的数据来源。遍历时要注意链表很长时读这个文件会有开销生产环境不建议频繁读取。static LIST_HEAD(wakeup_sources); static DEFINE_SPINLOCK(wakeup_sources_lock);锁的粒度是全局的这意味着在高频事件场景下比如网络包持续到达wakeup_source的激活/释放会有锁竞争。内核后来引入了wakeup_irq_node和更细的优化但整体上这个锁仍是热点之一。实测中如果发现wakeup_source相关路径 CPU 占用偏高可以考虑合并事件上报减少__pm_stay_awake的调用频率。3.3 与 device 的关联结构设备侧的关联在struct dev_pm_info里struct dev_pm_info { ... struct wakeup_source *wakeup; bool wakeup_path:1; ... };wakeup指向该设备的唤醒源wakeup_path表示该设备是否在唤醒路径上。device_init_wakeup(dev, true)会同时设置dev-power.can_wakeup和dev-power.wakeup_path并注册唤醒源。理解这个关联是写驱动时正确调用 API 的前提。4. 唤醒源的注册与使用实操4.1 设备唤醒源的注册流程给一个设备注册唤醒源标准做法是static int my_probe(struct platform_device *pdev) { struct device *dev pdev-dev; int ret; device_init_wakeup(dev, true); ret dev_pm_set_wake_irq(dev, irq); if (ret) return ret; return 0; }device_init_wakeup(dev, true)内部会调用wakeup_source_register()创建唤醒源并把dev-power.wakeup指向它。dev_pm_set_wake_irq()则把中断和唤醒源绑定这样中断触发时会自动上报唤醒事件。提示device_init_wakeup的第二个参数是can_wakeup设为 true 只是“允许”该设备唤醒系统真正是否使能还要看device_set_wakeup_enable()或用户空间的power/wakeup属性。很多驱动只调了前者忘了后者结果设备根本唤不醒系统。4.2 逻辑唤醒源的注册与释放逻辑唤醒源不依赖设备直接注册struct wakeup_source *ws; ws wakeup_source_register(NULL, my_logical_ws); if (!ws) return -ENOMEM; /* 使用中 */ __pm_stay_awake(ws); /* ... 处理事件 ... */ __pm_relax(ws); /* 不再需要时 */ wakeup_source_unregister(ws);第一个参数传 NULL 表示不关联设备。名字要唯一否则调试时无法区分。注册后要记得在模块卸载或退出路径里wakeup_source_unregister否则会内存泄漏而且链表里会残留一个永远激活的唤醒源导致系统再也睡不下去。4.3 激活与释放的引用计数语义__pm_stay_awake()和__pm_relax()是成对使用的前者增加激活计数后者减少。注意它们不是简单的布尔开关而是有引用计数语义的多次stay_awake需要同样次数的relax才能真正释放。这一点和pm_runtime_get/put类似。__pm_stay_awake(ws); /* active_count */ __pm_stay_awake(ws); /* 再次 */ __pm_relax(ws); /* 减一仍 active */ __pm_relax(ws); /* 减到零真正 relax */如果计数不平衡比如多 relax 了一次内核会打印警告Unbalanced pm_runtime_...类似的提示具体取决于版本并且唤醒源状态会错乱。我在实际项目里见过因为异常路径漏掉 relax导致系统连续几天无法进入睡眠的案例最后就是靠active_count和relax_count对不上定位到的。5. 事件上报与唤醒路径实现5.1 __pm_stay_awake 与 __pm_wakeup_event 的区别这两个 API 经常被混用但语义完全不同。__pm_stay_awake()是“我要保持唤醒直到我主动 relax”适合处理一个需要持续一段时间的事件比如 DMA 传输。__pm_wakeup_event(ws, msec)是“我上报一个事件并在 msec 毫秒后自动 relax”适合瞬时事件比如一次按键中断。/* 持续型传输期间保持唤醒 */ __pm_stay_awake(ws); start_dma_transfer(); /* 传输完成回调里 */ __pm_relax(ws); /* 瞬态型上报后自动超时释放 */ __pm_wakeup_event(ws, 2000); /* 2 秒后自动 relax */选错会导致两种问题用stay_awake处理瞬态事件忘了 relax 就永久阻止睡眠用wakeup_event处理持续事件超时太短会在事件还没处理完就允许睡眠造成数据丢失。5.2 中断与唤醒源的绑定机制dev_pm_set_wake_irq()会把中断描述符和唤醒源关联。当该中断触发时如果系统处于 suspend 状态会走唤醒路径如果系统在运行会调用__pm_wakeup_event上报。这个绑定是通过irq_set_irq_wake()和wakeup_source的wakeirq字段实现的。int dev_pm_set_wake_irq(struct device *dev, int irq) { struct wake_irq *wirq; ... wirq-dev dev; wirq-irq irq; irq_set_status_flags(irq, IRQ_DISABLE_UNLAZY); ... }注意不是所有中断都适合设为唤醒中断。共享中断线上如果挂了非唤醒设备可能导致误唤醒。设置前要确认该中断线在 suspend 期间确实只由唤醒设备触发。5.3 唤醒事件的统计与调试接口每个唤醒源的统计信息可以通过 debugfs 查看cat /sys/kernel/debug/wakeup_sources输出大致如下nameactive_countevent_countwakeup_countactive_sincetotal_timemax_timelast_changemy_ws12345012342005678eventpoll38204561009012active_since非零表示当前仍处于激活状态这个值就是它已经激活了多久毫秒。如果发现某个唤醒源active_since一直很大基本可以断定它忘了 relax。wakeup_count是真正把系统从 suspend 唤醒的次数是定位“谁在半夜叫醒系统”的直接证据。6. 与 autosleep 的联动机制6.1 autosleep 的触发条件autosleep的核心逻辑在kernel/power/autosleep.c。它维护一个工作队列当wakeup_source的激活计数归零时尝试触发 suspend。判断逻辑大致是static void try_to_suspend(struct work_struct *work) { unsigned int initial_count, final_count; if (!autosleep_enabled) return; initial_count pm_autosleep_lock(); if (pm_autosleep_state() ! PM_SUSPEND_ON) goto out; /* 检查是否还有激活的唤醒源 */ if (pm_wakeup_pending()) goto out; pm_suspend(PM_SUSPEND_MEM); ... }pm_wakeup_pending()会遍历所有唤醒源只要有一个active为真就返回 true阻止睡眠。所以唤醒源的计数准确性直接决定 autosleep 的行为。6.2 唤醒源如何阻止 autosleep当某个唤醒源被__pm_stay_awake激活后pm_wakeup_pending()返回 trueautosleep 的工作队列会重新排队等待。当所有唤醒源 relax 后工作队列再次运行此时才真正进入 suspend。这个“等待-重试”的机制保证了不会在事件处理中途睡眠。实际调试中如果 autosleep 一直不触发第一步就是看wakeup_sources里有没有active_since非零的项。有的话顺着名字找到对应驱动检查它的stay_awake/relax是否配对。6.3 autosleep 与 wakeup_count 的配合/sys/power/wakeup_count是另一个关键接口。用户空间在写mem到/sys/power/state之前通常先读wakeup_count处理完事件后再写回。如果期间有唤醒事件发生写回会失败需要重新读取。这个机制避免了“检查完没事件刚要睡事件来了”的竞态。# 典型的使用序列 count$(cat /sys/power/wakeup_count) echo mem /sys/power/state # 如果期间有事件上面的写会失败wakeup_count的递增由pm_wakeup_event等上报路径触发和wakeup_source的event_count是联动的。理解这一点才能明白为什么有些系统用 autosleep 很稳有些却总是睡不下去。7. 常见问题与排查技巧实录7.1 系统无法进入 suspend 的排查路径这是最高频的问题。排查顺序我一般这样走看/sys/kernel/debug/wakeup_sources找active_since非零的项。如果有记下名字去驱动代码里搜wakeup_source_register或device_init_wakeup。检查该驱动的stay_awake/relax是否配对异常路径是否漏了 relax。如果没有 active 项但pm_wakeup_pending()仍返回 true检查是否有wakeup_count未消费的事件。最后看dmesg里 suspend 失败的具体打印通常是某个设备的suspend回调返回了-EBUSY。实操心得我习惯在调试阶段临时把可疑唤醒源的active_count和relax_count打印出来两者差值就是当前未释放的引用数。这个方法比看active_since更直接。7.2 误唤醒问题的定位方法误唤醒是指系统睡了之后很快被唤醒wakeup_count不断增加。定位方法是# 睡眠前记录 cat /sys/kernel/debug/wakeup_sources /tmp/before.txt echo mem /sys/power/state # 唤醒后对比 cat /sys/kernel/debug/wakeup_sources /tmp/after.txt diff /tmp/before.txt /tmp/after.txtwakeup_count增加的那个唤醒源就是“肇事者”。常见的有GPIO 按键抖动、触摸屏中断未屏蔽、WiFi 模块心跳包、RTC 闹钟未清除。找到后针对性处理比如在 suspend 回调里屏蔽中断、清除 RTC 标志。7.3 唤醒源计数不平衡的典型场景计数不平衡几乎都出在异常路径。我总结了几种典型场景表现解决中断处理中 stay_awake错误返回时漏 relaxactive_count 持续增长用 goto 统一清理多线程共享唤醒源各自 stay/relax计数交叉错乱每个线程独立唤醒源模块卸载未 unregister链表残留永久 active卸载路径补 unregisterwakeup_event 超时太短事件未处理完就睡眠增大超时或改 stay_awake提示内核在__pm_relax里对未激活的唤醒源 relax 会有警告开启CONFIG_PM_DEBUG和CONFIG_PM_TRACE能拿到更详细的调用栈定位不平衡的源头非常有用。7.4 调试接口使用注意事项/sys/kernel/debug/wakeup_sources读取时会持有全局锁链表长时可能阻塞其他唤醒源操作。生产环境不要写脚本高频轮询这个文件否则可能引入额外的延迟甚至死锁风险。需要长期监控的话建议用tracepointpower:wakeup_source_activate、power:wakeup_source_deactivate替代开销小得多。# 用 tracepoint 监控唤醒源激活 echo 1 /sys/kernel/debug/tracing/events/power/wakeup_source_activate/enable cat /sys/kernel/debug/tracing/trace_pipe这个方式我在多个项目里用过比读 debugfs 稳定而且能看到调用上下文定位问题快很多。8. 驱动接入唤醒源的完整实操示例8.1 一个 GPIO 按键驱动的接入过程假设有一个 GPIO 按键需要在按下时唤醒系统。完整接入步骤如下struct my_button { struct device *dev; int irq; struct wakeup_source *ws; }; static irqreturn_t my_button_irq(int irq, void *data) { struct my_button *btn data; /* 上报唤醒事件2 秒后自动释放 */ __pm_wakeup_event(btn-ws, 2000); /* 处理按键逻辑 */ input_report_key(btn-input, KEY_POWER, 1); input_sync(btn-input); input_report_key(btn-input, KEY_POWER, 0); input_sync(btn-input); return IRQ_HANDLED; } static int my_button_probe(struct platform_device *pdev) { struct my_button *btn; int ret; btn devm_kzalloc(pdev-dev, sizeof(*btn), GFP_KERNEL); btn-dev pdev-dev; btn-irq platform_get_irq(pdev, 0); ret devm_request_irq(pdev-dev, btn-irq, my_button_irq, IRQF_TRIGGER_FALLING, my_button, btn); if (ret) return ret; device_init_wakeup(pdev-dev, true); ret dev_pm_set_wake_irq(pdev-dev, btn-irq); if (ret) return ret; btn-ws dev-power.wakeup; return 0; }关键点device_init_wakeup必须在dev_pm_set_wake_irq之前调用因为后者依赖前者创建的唤醒源。__pm_wakeup_event的超时时间要根据按键处理耗时来定太短会在处理中途允许睡眠太长会浪费功耗。8.2 参数选择与超时时间的计算超时时间不是拍脑袋定的。以按键为例从按下到用户空间处理完典型耗时包括中断处理几十微秒、input 子系统上报几百微秒、用户空间读取几毫秒到几十毫秒。所以 2 秒的超时是相当宽裕的实际可以设到 500ms 甚至更短。计算原则是超时时间 事件从产生到被消费的最坏路径耗时。如果事件会被用户空间异步处理还要考虑调度延迟。我一般先用一个偏大的值比如 2 秒保证功能正常再用 tracepoint 测量实际耗时逐步收紧。8.3 实测数据与效果验证接入后验证三步# 1. 确认唤醒源已注册 cat /sys/kernel/debug/wakeup_sources | grep my_button # 2. 确认设备使能了唤醒 cat /sys/devices/platform/my_button/power/wakeup # 应输出 enabled # 3. 实测唤醒 echo mem /sys/power/state # 按下按键系统应被唤醒 dmesg | tail # 应看到唤醒相关打印实测中如果按键唤不醒先检查power/wakeup是否为 enabled再检查中断是否在 suspend 期间被屏蔽。有些平台的 GPIO 中断控制器在 suspend 时会关闭需要在suspend回调里重新配置。9. 面试与原理延伸要点9.1 高频面试问题梳理围绕wakeup source面试常问的有wakeup source和runtime PM的区别与联系。__pm_stay_awake和__pm_wakeup_event的语义差异。autosleep 如何判断能否睡眠。唤醒源计数不平衡会怎样如何排查。wakeup_count的作用和竞态避免机制。回答时抓住“全局计数 事件追踪”这个核心再展开具体 API 和调试手段基本就能覆盖。9.2 从框架设计看内核的通用模式wakeup source体现了内核里很典型的一种设计模式用引用计数管理资源状态用全局链表做统一视图用 debugfs/tracepoint 做可观测性。类似的模式在runtime PM、regulator、clk框架里都能看到。理解了一个其他的触类旁通。我个人在实际项目里的体会是wakeup source的坑大多不在框架本身而在驱动接入时的生命周期管理。注册了忘了注销、激活了忘了释放、异常路径没清理这三类问题占了九成以上。把active_count和relax_count当成一对必须配平的账写代码时像对待kmalloc/kfree一样对待它们基本就不会出大问题。另外调试功耗问题时tracepoint 永远比 debugfs 更值得信赖前者开销小、信息全后者只适合快速看一眼当前状态。