深入理解watchdog1-框架(watchdog mode)
概述
watchdog是一种硬件电路,在软件出现故障时可以重置计算机系统。
通常情况下,用户空间的守护进程通过 /dev/watchdog 设备文件定期通知 kernel watchdog驱动程序表明系统正常。如果系统异常(如内存错误、内核漏洞等)时导致无法发送心跳信号,硬件watchdog会在超时后重置系统(如重启)。
如常见的车机系统组成有MCU(控制器)和SOC(系统芯片),MCU中包含有硬件watchdog定时器,SOC会定期向MCU发送心跳(heartbeat),如果超时未发送心跳,MCU会通过引脚拉低电压重启SOC,避免车机一直卡死情况。
工作流程:
- 开机启动,watchdog定时器设置timeout值和timeout回调函数,当倒计时结束后会触发回调函数重启SOC;
- soc定时向watchdog硬件发送心跳信号(如调用driver ioctl接口),当watchdog收到信号后重置定时器为最初的时间;此过程反复循环,watchdog便无法倒计时结束,从而无法触发回调函数;
- SOC系统异常后无法向watchdog发送心跳信号。watchdog计时器结束后触发了回调函数,从而重启系统。
框架
分为3个部分:
- 硬件:对应具体的硬件型号,以raspberry pi4为例,对应的型号为bcm2835_wdt,驱动实现为:bcm2835_wdt.c;
- kernel:包括watchdog框架(core)、kthread内核线程和基于软件的实现softdog;
- user:通过watchdog driver提供给用户空间的字符设备实现心跳;
watchdog硬件驱动
linux源码提供了较多的watchdog实现,如stm32_iwdg、qcom_wdt等。
raspberry pi4采用的为博通bcm2835型号,启动watchdog需要在config.txt中添加以下参数:
dtparam=watchdog=on
bcm2835驱动实现路径为:drivers\watchdog\bcm2835_wdt.c。
主要的结构体和函数如下:
static const struct watchdog_ops bcm2835_wdt_ops = {
.owner = THIS_MODULE,
.start = bcm2835_wdt_start,
.stop = bcm2835_wdt_stop,
.get_timeleft = bcm2835_wdt_get_timeleft,
.restart = bcm2835_restart,
};
操作集合,供上层使用
- start:通过writel_relaxed接口向硬件寄存器写入timeout值并启动;
- stop:通过writel_relaxed接口向对应位写入PM_RSTC_RESET;
- get_timeleft:获取倒计时剩余时间;
- restart:重启采用的方法同start,只是timeout设置为10 ticks (~150us),会立刻重启;
static struct watchdog_device bcm2835_wdt_wdd = {
.info = &bcm2835_wdt_info,
.ops = &bcm2835_wdt_ops,
.min_timeout = 1,
.max_timeout = WDOG_TICKS_TO_SECS(PM_WDOG_TIME_SET),
.timeout = WDOG_TICKS_TO_SECS(PM_WDOG_TIME_SET),
};
具体硬件的抽象结构体,供kernel core使用。这里主要说明下timeout,默认值为0x000fffff>>16=15,即超时时间为15s,另外有一些驱动是通过dts配置的。
static int bcm2835_wdt_probe(struct platform_device *pdev)
{
...
watchdog_init_timeout(&bcm2835_wdt_wdd, heartbeat, dev);
watchdog_set_nowayout(&bcm2835_wdt_wdd, nowayout);
if (bcm2835_wdt_is_running(wdt)) {
set_bit(WDOG_HW_RUNNING, &bcm2835_wdt_wdd.status);
}
watchdog_stop_on_reboot(&bcm2835_wdt_wdd);
err = devm_watchdog_register_device(dev, &bcm2835_wdt_wdd);
当设备被系统探测到会执行probe方法进行设备初始化,有如下步骤:
- watchdog_init_timeout:初始化timeout,可以通过module install传递,也可以解析dts字段timeout-sec(如果存在),以上2种情况都不存在,则使用默认值15s。
- watchdog_set_nowayout:设置nowayout特性,开启后watchdog将无法停止。
- 如果watchdog没有实现stop方法,必须要设置WDOG_HW_RUNNING状态。在watchdog启动后,如果设置有该字段,kernel会通过watchdogd进程发送心跳信号,如果没有设置watchdogd则不会工作。该状态对用户空间不可见,不能通过ioctl进行设置,只能在驱动中设置。
- 更新status为WDOG_STOP_ON_REBOOT,即启动的时候需要停止计时;
- devm_watchdog_register_device:注册设备节点/dev/watchdogX,注意如果只有一个硬件,命名为/dev/watchdog0,并且同时会创建/dev/watchdog(id为0的情况下才会创建,为了兼容老版本),watchdog0和watchdog表示同一个设备。如果有2个硬件,第二个会注册为/dev/watchdog1。注册完成后会通过hrtimer定时器投递一个ping work到队列中,watchdogd获取后执行ping work发送心跳信号。
watchdog内核框架
框架层是用户空间和硬件驱动的桥梁,用户空间通过框架层提供的接口执行驱动中的具体实现。框架层提供了操作集合,供用户空间使用,从这里入手,更容易梳理。
// drivers\watchdog\watchdog_dev.c
static const struct file_operations watchdog_fops = {
.owner = THIS_MODULE,
.write = watchdog_write,
.unlocked_ioctl = watchdog_ioctl,
.open = watchdog_open,
.release = watchdog_release,
};
- watchdog_open
用户空间打开设备时open("/dev/watchdogX")最终会调用到该函数。
- 设备只能打开一次,若已经打开直接返回;
- 判断硬件设备是否存在,不存在返回;
- 判断watchdog是否激活(WDOG_ACTIVE),不同于WDOG_HW_RUNNING,该状态是对用户空间可见的。
- 如果没有激活,则调用ping或start进行激活。ping和start对应具体的硬件驱动实现。
- watchdog_write
- 判断写入的字符是否为'V',若是则在下次用户空间调用close(fd)时会stop watchdog(nowayout没设置情况下)。注意close(watchdog_fd)和watchdog stop没有必然的关联性,若想在close的时候stop watchdog需要满足一定的条件。
- 写入的字符非'V',则发送心跳信号ping,status更改为保活状态(_WDOG_KEEPALIVE);
- 计算心跳时间是否满足条件(心跳间隔要大于min_hw_heartbeat_ms,心跳之间最小时间间隔的硬件限制),若最接近的一次心跳信号时间还没有满足,则通过hrtimer_start启动定时器把ping work放到workqueue中,最终通过kthread来完成;若时间已经满足则发送心跳信号。
- 发送心跳信号后,需要更新下次心跳的时间。这里有个判断为watchdog_need_worker,即当硬件启动后并且用户空间没有专门的发送心跳进程的情况下是需要worker(kthread),这时就需要启动定时器把ping work放到对应的workqueue等待kthread调度。
- watchdog_ioctl
该接口提供了丰富的功能,用户空间可以使用该接口获取更多信息:
- WDIOC_SETOPTIONS:stop or start watchdog;
- WDIOC_KEEPALIVE:ping watchdog;
- WDIOC_SETTIMEOUT:设置timeout值;
- WDIOC_GETTIMEOUT:获取timeout值;
- WDIOC_GETTIMELEFT:剩余到期时间;
- WDIOC_GETSUPPORT:获取watchdog info;
- WDIOC_GETSTATUS:获取watchdog status;
- WDIOC_SETPRETIMEOUT:设置预超时时间(pretimeout),用于在超时启动前需要收集一些调试信息。预超时间是在超时范围内,如timeout=10s,pretimeout=1s,在倒计时达到9s的时候会触发预超时回调函数进行收集信息。
- watchdog_release
在没有设置nowayout特性的前提下,通过写入'V'会设置可释放标志位_WDOG_ALLOW_RELEASE。当用户空间close(fd)时会通过系统调用走到驱动侧的watchdog_release,watchdog_release会检测status是否设置有可释放标志位,若成立则调用watchdog_stop。
用户空间实现
内核源码中提供了一个简单的demo:
// samples\watchdog\watchdog-simple.c
int main(void)
{
int fd = open("/dev/watchdog", O_WRONLY);
...
while (1) {
ret = write(fd, "\0", 1);
...
sleep(10);
}
close(fd);
return ret;
}
逻辑比较简单,循环中每隔10s发送心跳信号(俗称'喂狗')。实际项目中间隔需要结合硬件中定义的timeout值,该进程一般拥有较高的优先级,如果系统中出现了异常导致进程无法运行,watchdog超时后会重置系统。
kthread内核进程
这里在单独对kthread的作用说明下,可能有些人会比较疑惑该进程具体是什么作用。
设想一个场景:当硬件watchdog启动后,timeout比较小1s,用户空间的喂狗进程启动比较慢5s,那么开机后用户空间定义的喂狗进程还没有启动,硬件watchdog超时后会马上重启系统,系统就会一直处于重启的循环中。为了在用户空间进程接手之前或者未定义用户空间喂狗进程的情况下保证系统正常喂狗,内核默认会创建一个内核进程进行该工作。
kthread其实是一个kworker,对应的work为watchdog_ping_work,即发送心跳信号:
这里以raspberry pi4为例说明:
- bcm2835 probe初始化时会调用devm_watchdog_register_device注册字符设备,并且会启动一个定时器立刻发送心跳信号:
// devm_watchdog_register_device-...->watchdog_cdev_register
if (watchdog_hw_running(wdd)) {
...
if (handle_boot_enabled)
hrtimer_start(&wd_data->timer, 0,HRTIMER_MODE_REL_HARD);
...
}
- 定时器的回调函数为 watchdog_timer_expired,会把watchdog_ping_work放入到workqueue中等待kthread执行:
// 1. 定义work
kthread_init_work(&wd_data->work, watchdog_ping_work);
// 2. 赋值定时器到期回调函数
wd_data->timer.function = watchdog_timer_expired;
// 3. 回调函数把work放入到workqueue
static enum hrtimer_restart watchdog_timer_expired(struct hrtimer *timer)
{
...
kthread_queue_work(watchdog_kworker, &wd_data->work);
}
- watchdog_ping_work发送信号需要满足条件之一:
- 存在用户空间喂狗进程并且执行了open(即status为active);
- 没有用户空间喂狗进程,硬件watchdog处于运行状态(running),需要借助kthread进行喂狗。
static bool watchdog_worker_should_ping(struct watchdog_core_data *wd_data)
{
...
if (watchdog_active(wdd))
return true;
return watchdog_hw_running(wdd) && !watchdog_past_open_deadline(wd_data);
}
static void watchdog_ping_work(struct kthread_work *work)
{
...
if (watchdog_worker_should_ping(wd_data))
__watchdog_ping(wd_data->wdd);
}
- 在执行完__watchdog_ping后需要更新下次发送心跳信号的时间watchdog_update_worker,这里有个关键的信息,是否需要kthread工作watchdog_need_worker:
- 用户设定的timeout值比硬件支持的最大心跳时间还要大,那么就没必要单独使用用户空间喂狗进程了,kthread就能满足,kthread的喂狗间隔永远不会超时;
- 不存在用户空间喂狗进程,需要kthread进行喂狗;
static inline void watchdog_update_worker(struct watchdog_device *wdd)
{
if (watchdog_need_worker(wdd)) {
ktime_t t = watchdog_next_keepalive(wdd);
if (t > 0)
hrtimer_start(&wd_data->timer, t,
HRTIMER_MODE_REL_HARD);
} else {
hrtimer_cancel(&wd_data->timer);
}
}
- kthread的喂狗间隔有watchdog_next_keepalive控制,为硬件定义的timeout值的一半,如timeout为15s,kthread每7.5s就会喂狗一次。
hw_heartbeat_ms = min_not_zero(timeout_ms, wdd->max_hw_heartbeat_ms);
keepalive_interval = ms_to_ktime(hw_heartbeat_ms / 2);