3、Linux内核看门狗框架:WATCHDOG子系统架构、核心数据结构、注册与注销接口
好,咱们今天聊聊Linux内核里的看门狗框架。说实话,这个框架设计得挺精巧的,但刚接触时容易一头雾水。我当年第一次看WATCHDOG子系统的代码,愣是翻了好几遍才理清脉络。今天我把核心的东西给你拆开讲。
3.1 子系统架构:分层设计的思想
Linux内核的看门狗框架,说白了就是一套标准化的接口。它把硬件相关的操作和上层用户空间的操作彻底分开了。你想想看,不同芯片的看门狗寄存器、喂狗方式千差万别,但用户程序调用的ioctl、write这些接口却是一致的。这就是框架的价值。
整个架构分三层:
- 底层:硬件驱动层,直接操作寄存器。这部分你写驱动时最头疼。
- 中间层:核心框架层,也就是
watchdog子系统。它负责注册、注销、超时管理等通用逻辑。 - 上层:用户空间接口层,通过
/dev/watchdog字符设备暴露给应用程序。
我个人习惯把中间层比作「调度员」。它不关心你的硬件是SPI接口还是内存映射,它只负责把上层请求翻译成底层回调。嗯,这里要注意:框架层做了很多安全检查,比如超时时间合法性校验、设备是否被占用等。这些你写驱动时不用重复造轮子。
核心要点:驱动开发者只需要实现struct watchdog_ops里的回调函数,剩下的框架帮你搞定。
3.2 核心数据结构:两个结构体撑起一片天
框架里最重要的两个结构体,一个是struct watchdog_device,另一个是struct watchdog_ops。我当年在项目里调试一个看门狗误复位问题,就是在这两个结构体的初始化上栽了跟头。
3.2.1 struct watchdog_device
这个结构体代表一个看门狗设备实例。它长这样:
struct watchdog_device {
int id; // 设备ID,通常自动分配
struct device *parent; // 父设备,指向你的platform_device
const struct watchdog_info *info; // 设备信息,比如固件版本
const struct watchdog_ops *ops; // 操作函数集
unsigned int timeout; // 当前超时时间(秒)
unsigned int min_timeout; // 最小超时时间
unsigned int max_timeout; // 最大超时时间
unsigned int pretimeout; // 预超时时间(有些芯片支持)
unsigned int status; // 设备状态标志
// ... 还有几个不常用的字段
};
这里我特别想提醒你注意min_timeout和max_timeout。我曾经遇到一个坑:某款芯片的看门狗最小超时是1秒,但我驱动里忘了设置min_timeout,结果框架默认允许了0.5秒的超时。用户程序一设置,芯片直接罢工。所以这两个字段一定要根据硬件手册填准确。
3.2.2 struct watchdog_ops
这是你真正要写代码的地方:
struct watchdog_ops {
int (*start)(struct watchdog_device *wdd); // 启动看门狗
int (*stop)(struct watchdog_device *wdd); // 停止看门狗
int (*ping)(struct watchdog_device *wdd); // 喂狗
unsigned int (*status)(struct watchdog_device *wdd); // 获取状态
int (*set_timeout)(struct watchdog_device *wdd, unsigned int t); // 设置超时
int (*set_pretimeout)(struct watchdog_device *wdd, unsigned int t);
unsigned int (*get_timeleft)(struct watchdog_device *wdd); // 剩余时间
// ... 其他可选回调
};
这些回调函数里,start、stop、ping是必须实现的。其他的像set_timeout,如果你不实现,框架会用默认的watchdog_set_timeout函数帮你处理。但说实话,我建议你尽量实现set_timeout,因为硬件通常有专门的超时寄存器,用软件模拟不太靠谱。
个人经验:写ping函数时,记得加个自旋锁或互斥锁。我遇到过喂狗函数被并发调用,导致寄存器写乱的情况。虽然看门狗本身有容错,但驱动层面还是要严谨。
3.3 注册与注销接口:让设备活起来
结构体填好了,怎么把它交给内核?两个函数搞定:
3.3.1 注册:watchdog_register_device
int watchdog_register_device(struct watchdog_device *wdd);
这个函数会做几件事:
- 检查
ops里是否实现了必要的回调 - 校验超时时间范围
- 创建设备节点
/dev/watchdog - 启动看门狗定时器(如果设备默认是使能的)
调用时机通常在驱动的probe函数里。我习惯在注册前先调用watchdog_init_timeout来初始化超时时间:
watchdog_init_timeout(wdd, timeout, dev);
这个函数会优先使用设备树里的timeout-sec属性,如果没有,就用你传的timeout参数。嗯,设备树真是个好东西。
3.3.2 注销:watchdog_unregister_device
void watchdog_unregister_device(struct watchdog_device *wdd);
这个函数在驱动的remove函数里调用。它会停止看门狗、删除设备节点、释放资源。有一点要注意:如果用户空间还开着/dev/watchdog,注销操作会被阻塞。所以最好在remove前先确保设备已关闭。
避坑指南:我曾经在remove函数里直接调watchdog_unregister_device,结果用户程序还在喂狗,导致内核报错。正确的做法是先调用watchdog_stop,再注销。或者用watchdog_dev_suspend这类辅助函数。
3.4 一个完整的注册示例
光说不练假把式。我给你看一个简化的注册流程:
static const struct watchdog_ops my_wdt_ops = {
.owner = THIS_MODULE,
.start = my_wdt_start,
.stop = my_wdt_stop,
.ping = my_wdt_ping,
.set_timeout = my_wdt_set_timeout,
};
static const struct watchdog_info my_wdt_info = {
.identity = "My Watchdog",
.options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING,
};
static int my_wdt_probe(struct platform_device *pdev)
{
struct watchdog_device *wdd;
int ret;
wdd = devm_kzalloc(&pdev->dev, sizeof(*wdd), GFP_KERNEL);
if (!wdd)
return -ENOMEM;
wdd->info = &my_wdt_info;
wdd->ops = &my_wdt_ops;
wdd->min_timeout = 1;
wdd->max_timeout = 60;
wdd->parent = &pdev->dev;
watchdog_init_timeout(wdd, 30, &pdev->dev);
ret = watchdog_register_device(wdd);
if (ret)
return ret;
platform_set_drvdata(pdev, wdd);
return 0;
}
你看,核心代码就这么几行。框架帮你处理了大部分脏活累活。你只需要关注硬件相关的start、stop、ping怎么实现。
3.5 调试小技巧
最后分享个调试技巧。如果你发现看门狗驱动加载后,/dev/watchdog没出现,先检查dmesg。框架会在注册失败时打印错误信息。我常用的排查步骤:
- 检查
ops里的start和ping是否都实现了 - 确认
min_timeout<=max_timeout - 看看设备树里
timeout-sec属性是否在范围内 - 用
cat /proc/devices确认看门狗主设备号是否注册成功
嗯,基本上这四步能解决90%的问题。剩下的10%,可能就是硬件本身没起振或者时钟没配好——那又是另一个故事了。