3. Wakeup源驱动框架:Linux内核的Wakeup源驱动模型
好,我们直接进入正题。Wakeup源驱动框架,说白了就是内核里一套管理「谁能唤醒系统」的机制。你想想看,手机待机时,为什么按一下电源键就能亮屏?为什么插上充电器也能唤醒?这些背后都是Wakeup源在起作用。
我个人习惯把Wakeup源驱动模型理解成「门卫登记系统」。每个能唤醒系统的硬件,都得在门卫(内核)那里登记一下。系统要休眠时,门卫会检查:还有没有人(Wakeup源)在活动?如果有,就不让睡。这套机制,就是今天我们要聊的核心。
3.1 struct wakeup_source结构体详解
先看最核心的数据结构。内核里用 struct wakeup_source 来描述一个Wakeup源。我当年第一次看这个结构体时,觉得它挺简单,后来踩过坑才发现,细节都在字段里。
struct wakeup_source {
const char *name; // 名字,调试时靠它认人
struct list_head entry; // 链表节点,所有ws挂在一个全局链表上
spinlock_t lock; // 自旋锁,保护内部状态
struct wake_irq *wakeirq; // 关联的硬件中断(可选)
struct timer_list timer; // 定时器,用于自动超时
unsigned long timer_expires; // 定时器到期时间
ktime_t last_time; // 上次状态变更时间
ktime_t start_prevent_time; // 开始阻止休眠的时间
ktime_t prevent_sleep_time; // 累计阻止休眠的总时长
unsigned long event_count; // 事件计数
unsigned long active_count; // 活跃状态计数
bool active:1; // 当前是否活跃
bool autosleep_enabled:1; // 是否启用自动休眠
};
嗯,这里要注意 active 这个字段。它是个布尔值,表示当前这个Wakeup源是不是「醒着」的。如果 active = true,系统就不能进入深度休眠。我曾经在项目里遇到过一个bug,某个外设的驱动忘了把 active 置回false,结果手机一晚上掉电30%。排查了半天,最后发现是某个传感器在休眠前没释放唤醒锁。
event_count 和 active_count 这两个计数器也很有用。它们记录了Wakeup源被触发的次数和活跃的次数。调试时,我经常通过 /sys/kernel/debug/wakeup_sources 看这些计数,来判断某个外设是不是异常频繁地唤醒系统。
核心要点:struct wakeup_source 就是内核里每个唤醒源的「身份证」。它记录了名字、状态、统计信息。驱动开发者需要正确维护 active 状态,否则系统休眠会出问题。
3.2 wakeup_source_register/unregister API
有了结构体,接下来就是怎么注册和注销。内核提供了两个核心API:wakeup_source_register 和 wakeup_source_unregister。
先看注册函数:
struct wakeup_source *wakeup_source_register(struct device *dev,
const char *name)
{
struct wakeup_source *ws;
int ret;
ws = kzalloc(sizeof(*ws), GFP_KERNEL);
if (!ws)
return NULL;
ws->name = name;
ws->active = false;
spin_lock_init(&ws->lock);
timer_setup(&ws->timer, wakeup_source_deactivate_timer, 0);
ret = wakeup_source_add(ws);
if (ret) {
kfree(ws);
return NULL;
}
device_init_wakeup(dev, true);
dev->power.wakeup->ws = ws;
return ws;
}
EXPORT_SYMBOL_GPL(wakeup_source_register);
这段代码做了几件事:
- 分配内存,初始化结构体字段
- 初始化自旋锁和定时器
- 调用
wakeup_source_add把ws挂到全局链表 - 通过
device_init_wakeup把设备和Wakeup源关联起来
我建议你在写驱动时,尽量在 probe 函数里调用 wakeup_source_register。这样设备一加载,Wakeup源就注册好了。我曾经见过有人把注册放在 open 函数里,结果设备文件没打开时,系统就无法识别这个唤醒源,导致休眠异常。
再看注销函数:
void wakeup_source_unregister(struct wakeup_source *ws)
{
if (ws)
__wakeup_source_remove(ws);
}
EXPORT_SYMBOL_GPL(wakeup_source_unregister);
这个就简单多了。它会从全局链表里移除ws,并释放相关资源。注意,调用前要确保没有其他线程还在使用这个ws。我习惯在 remove 函数里调用它,顺序是:先关中断,再注销Wakeup源,最后释放其他资源。
避坑指南:我曾经在项目里犯过一个错误——在注销Wakeup源后,中断处理函数还在尝试访问它。结果内核直接panic。正确的做法是:先禁用设备中断,确保不会再产生唤醒事件,然后再注销Wakeup源。
3.3 Wakeup源驱动模型的核心逻辑
好了,结构体和API都讲完了。我们来看看整个驱动模型是怎么串起来的。下面这张图是我自己画的,能帮你快速理解整体框架。
从这张图你能看到,整个模型分四层:
- 硬件层:各种外设,比如GPIO按键、RTC闹钟、充电器插入检测
- 中断处理:硬件触发中断后,驱动在中断上下文调用
__pm_wakeup_event() - Wakeup源管理层:维护每个ws的状态,提供注册/注销/激活/去激活接口
- 电源管理核心:在suspend流程中检查是否有活跃的ws,决定是否继续休眠
这里有个关键点:中断处理函数里不能直接调用 wakeup_source_register。为什么?因为注册函数可能会分配内存,而中断上下文不允许睡眠。正确的做法是:在 probe 时注册好,中断里只调用 __pm_wakeup_event 来标记事件。
个人经验:调试Wakeup源问题时,我经常用这个命令:cat /sys/kernel/debug/wakeup_sources。它会列出所有注册的Wakeup源,以及它们的活跃次数、总阻止休眠时间等。如果某个设备的 active_count 一直在涨,但系统却无法休眠,那八成就是它的问题。
3.4 驱动开发中的最佳实践
最后,我总结几条实战经验,希望能帮你少走弯路:
| 场景 | 推荐做法 | 常见坑 |
|---|---|---|
| 设备初始化 | 在probe中调用wakeup_source_register | 在open或中断中注册,导致竞态 |
| 中断处理 | 调用__pm_wakeup_event,不直接操作ws | 在中断里调用register或unregister |
| 设备移除 | 先禁中断,再unregister | 顺序反了,导致use-after-free |
| 调试排查 | 查看debugfs中的wakeup_sources | 只靠printk,效率太低 |
嗯,说到调试,我再分享一个技巧。如果你发现系统无法休眠,可以先检查 /sys/power/wakeup_count 这个文件。如果它一直在变化,说明有Wakeup源在不断产生事件。然后去 debugfs 里看具体是哪个设备在搞鬼。这套排查流程,我在项目里用过无数次,屡试不爽。
好了,关于Wakeup源驱动框架,我们就聊到这里。记住三个核心:struct wakeup_source 是身份证,register/unregister 是登记注销,__pm_wakeup_event 是事件上报。把这三点搞明白,你就能驾驭大部分Wakeup源的开发工作了。