树莓派启用看门狗watchdog

树莓派内核默认没有启用看门狗功能,当内核挂死时将进入“死机”状态或kgdb调试状态,并不会自动重启系统。本文为树莓派开启看门狗功能并通过内核线程周期性喂狗,当出现系统崩溃时会自动重启Linux系统。

环境说明:(1)单板:树莓派b

                    (2)Linux内核:Linux-4.1.15

                    (3)Bootloader:u-boot-2015.10

源码文件:linux-rpi-4.1.y/drivers/watchdog/bcm2835_wdt.c


1、看门狗驱动源码分析


树莓派的看门狗驱动程序为内核drivers/watchdog/bcm2835_wdt.c文件,该驱动程序实现了开关看门狗和喂狗的功能(不提供喂狗策略),它向内核看门狗子系统注册驱动设备,将喂狗策略移交应用程序,由应用程序打开/dev/watchdogX标准接口并完成周期性喂狗的操作。简单分析一下该驱动程序的源码:

static const struct of_device_id bcm2835_wdt_of_match[] = {
	{ .compatible = "brcm,bcm2835-pm-wdt", },
	{},
};
MODULE_DEVICE_TABLE(of, bcm2835_wdt_of_match);

static struct platform_driver bcm2835_wdt_driver = {
	.probe		= bcm2835_wdt_probe,
	.remove		= bcm2835_wdt_remove,
	.shutdown	= bcm2835_wdt_shutdown,
	.driver = {
		.name =		"bcm2835-wdt",
		.of_match_table = bcm2835_wdt_of_match,
	},
};
module_platform_driver(bcm2835_wdt_driver);
驱动程序通过platform driver实现,同时支持设备数dtb添加platform device,匹配名称为"brcm,bcm2835-pm-wdt"。

module_param(heartbeat, uint, 0);
MODULE_PARM_DESC(heartbeat, "Initial watchdog heartbeat in seconds");

module_param(nowayout, bool, 0);
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
				__MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
驱动程序提供了两个可调参数,其中heartbeat表示看门狗设置的超时时间,默认为15s;nowayout是一个bool型变量;如若设置了就表示该看门狗一旦开启将不再向应用提供关闭的功能,只能通过不断的喂狗操作来保证系统不会重启。下面分析一下其中的probe初始化函数:

static int bcm2835_wdt_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct device_node *np = dev->of_node;
	struct bcm2835_wdt *wdt;
	int err;

	wdt = devm_kzalloc(dev, sizeof(struct bcm2835_wdt), GFP_KERNEL);
	if (!wdt)
		return -ENOMEM;
	platform_set_drvdata(pdev, wdt);

	spin_lock_init(&wdt->lock);

	wdt->base = of_iomap(np, 0);
	if (!wdt->base) {
		dev_err(dev, "Failed to remap watchdog regs");
		return -ENODEV;
	}

	watchdog_set_drvdata(&bcm2835_wdt_wdd, wdt);
	watchdog_init_timeout(&bcm2835_wdt_wdd, heartbeat, dev);
	watchdog_set_nowayout(&bcm2835_wdt_wdd, nowayout);
	err = watchdog_register_device(&bcm2835_wdt_wdd);
	if (err) {
		dev_err(dev, "Failed to register watchdog device");
		iounmap(wdt->base);
		return err;
	}

	dev_info(dev, "Broadcom BCM2835 watchdog timer");
	return 0;
}
该初始化函数完成以下功能:(1)分配bcm2835_wdt结构体内存空间,动态映射寄存器的虚拟内存空间到wdt->base中,后续通过向该地址空间写入数据即可完成寄存器的操作。(2)初始化看门狗子系统的watchdog_device结构,根据输入参数调整看门狗超时时间和设置status状态标志位。(3)向看门狗子系统注册看门口设备。

其中的watchdog_device结构实例bcm2835_wdt_wdd定义如下:

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),
};
这里的ops结构为驱动程序向子系统注册的驱动控制函数结构,子系统在在接收到用户的Ioctl和write控制指令后会调用该注册函数。由于子系统向用户开放了调整看门狗超时时间的设置接口,这里定义了最大和最小的超时时间限制(min_timeout和max_timeout),在调整timeout值时会进行保护判断。

static struct watchdog_ops bcm2835_wdt_ops = {
	.owner =	THIS_MODULE,
	.start =	bcm2835_wdt_start,
	.stop =		bcm2835_wdt_stop,
	.set_timeout =	bcm2835_wdt_set_timeout,
	.get_timeleft =	bcm2835_wdt_get_timeleft,
};
树莓派的驱动程序向子系统提供了以上4种接口(其他接口未实现),其中start接口表示启动看门狗和喂狗操作,stop接口表示关闭看门狗,set_timeout表示调整看门狗超时时间,get_timeleft表示查询距离看门狗超时还剩多少时间,各个函数的具体实现基本就是设置芯片的寄存器,就不仔细分析了。

bcm2835_wdt_probe函数在完成看门狗的注册之后,在文件系统的/dev目录下就会生成watchdog0设备文件,后面应用程序就可以通过操作它来实现看门狗的控制了。Linux内核的看门狗子系统实现在drivers/watchdog/目录下的watchdog_core.c和watchdog_dev.c文件中,其中watchdog_core.c实现了子系统的初始化以及提供了面向驱动的注册接口函数watchdog_register_device等;watchdog_dev.c实现了字符设备的初始化和注册,同时提供了设备文件控制接口watchdog_fops:

static const struct file_operations watchdog_fops = {
	.owner		= THIS_MODULE,
	.write		= watchdog_write,
	.unlocked_ioctl	= watchdog_ioctl,
	.open		= watchdog_open,
	.release	= watchdog_release,
};
可以看到这里实现了open、close、write和ioctl的控制接口,另外在watchdog.h中定义了Ioctl的标准控制定义:

#define	WDIOC_GETSUPPORT	_IOR(WATCHDOG_IOCTL_BASE, 0, struct watchdog_info)	//获取看门狗info信息
#define	WDIOC_GETSTATUS		_IOR(WATCHDOG_IOCTL_BASE, 1, int)					//查询看门狗status状态信息
#define	WDIOC_GETBOOTSTATUS	_IOR(WATCHDOG_IOCTL_BASE, 2, int)					//查询看门狗bootstatus信息
......			
#define	WDIOC_SETOPTIONS	_IOR(WATCHDOG_IOCTL_BASE, 4, int)					//设置开关看门狗
#define	WDIOC_KEEPALIVE		_IOR(WATCHDOG_IOCTL_BASE, 5, int)					//喂狗
#define	WDIOC_SETTIMEOUT        _IOWR(WATCHDOG_IOCTL_BASE, 6, int)				//设置看门狗超时时间
#define	WDIOC_GETTIMEOUT        _IOR(WATCHDOG_IOCTL_BASE, 7, int)				//查询看门狗超时时间
......	
#define	WDIOC_GETTIMELEFT	_IOR(WATCHDOG_IOCTL_BASE, 10, int)					//查询看门狗距离超时的剩余时间
这些ioctl控制由watchdog_ioctl函数负责处理,它会转接调用驱动程序中注册的ops函数接口。关于看门狗子系统中驱动的注册以及用户控制调用流程为典型的Linux驱动子系统架构,同前博文 《构建Linux内核驱动demo子系统示例》中分析总结的驱动子系统实现如出一辙(不同之处在于没有实现sys和proc接口),本文不再详细分析,具体看以参见watchdog_core.c和watchdog_dev.c中的源码。


2、看门狗驱动源码修改


由于看门狗驱动程序并没有实现喂狗策略,因此要启用看门狗可以通过编写应用程序,在应用层创建一个守护进程实现周期喂狗操作。但本文并不通过该方式实现,而是修改该驱动程序,在内核中创建一个内核线程来实现喂狗的动作。在驱动源码中添加以下3个函数:

static void bcm2835_wdt_set_prio(unsigned int policy, unsigned int prio)
{
	struct sched_param param = { .sched_priority = prio };

	sched_setscheduler(current, policy, ¶m);
}

static int bcm2835_wdt_kthread(void *data)
{
	struct watchdog_device *wdog = (struct watchdog_device *)data;

	/* 调整内核线程的调度策略和优先级 */
	bcm2835_wdt_set_prio(SCHED_FIFO, MAX_RT_PRIO - 1);
	
	while(1) {
		if (kthread_should_stop()) {
			bcm2835_wdt_stop(wdog);
			break;
		}
	
		if (bcm2835_wdt_wdd.timeout != 0) {
			(void)bcm2835_wdt_start(&bcm2835_wdt_wdd);
			msleep((bcm2835_wdt_wdd.timeout >> 2)*1000);			
			dev_dbg(wdog->dev, "BCM2835 ping watchdog");
		} else {
			msleep(1000);
		}
	}
	
	return 0;
}

static void bcm2835_wdt_start_kthread(struct watchdog_device *wdog)
{
	/* 启动看门狗并创建内核线程执行喂狗操作 */
	mutex_lock(&bcm2835_wdt_lock);
	if (kwdt_task == NULL) {
		kwdt_task = kthread_run(bcm2835_wdt_kthread, &bcm2835_wdt_wdd, "bcm2835_kwdt");
		if (IS_ERR(kwdt_task)) {
			dev_err(wdog->dev, "Failed to Create BCM2835 kernel watchdog thread");
			kwdt_task = NULL;
		}

		dev_info(wdog->dev, "Create BCM2835 kernel watchdog thread OK!"
				" TimeOut = %d sec", bcm2835_wdt_wdd.timeout);
	}
	mutex_unlock(&bcm2835_wdt_lock);
}
其中bcm2835_wdt_start_kthread函数用于创建内核喂狗守护进程bcm2835_kwdt,该线程执行函数bcm2835_wdt_kthread,它首先调整自身的调度策略为FIFO策略(软实时),并提高进程的优先级,这样可以使该进程在系统较为繁忙时也能确保其调度性,然后在该函数周期的实现喂狗动作,喂狗的时间间隔为超时时间的1/4。如果线程需要销毁会先执行关闭看门狗的动作,防止系统重启。

在看门狗启动函数bcm2835_wdt_start中添加如下:

static int bcm2835_wdt_start(struct watchdog_device *wdog)
{
	/* 喂狗操作 */
	......
	
	/* added by zhangyi 2016.5.7 */
	bcm2835_wdt_start_kthread(wdog);
	/* * */
	
	return 0;
}
这里添加启动内核线程的函数,如此可以通过"echo xx > /dev/watchdog0"启动该内核线程,最后在驱动的release函数中增加释放程序:

static int bcm2835_wdt_remove(struct platform_device *pdev)
{
	struct bcm2835_wdt *wdt = platform_get_drvdata(pdev);

	/* added by zhangyi 2016.5.7 */
	mutex_lock(&bcm2835_wdt_lock);
	if (kwdt_task) {
		kthread_stop(kwdt_task);
		kwdt_task = NULL;
	}
	mutex_unlock(&bcm2835_wdt_lock);
	/* * */
	
	watchdog_unregister_device(&bcm2835_wdt_wdd);
	iounmap(wdt->base);

	return 0;
}
这样在内核卸载该驱动程序时会销毁喂狗内核线程并关闭看门狗。


3、看门狗实验效果


(1)编译以上修改后的看门狗驱动程序,将生成的模块.ko文件拷贝到树莓派的根文件系统的/lib/modules/4.1.15/kernel/drivers/watchdog/目录下(若不需要udev自动加载则任意目录皆可)。


(2)修改dts文件arch/arm/boot/dts/bcm2708_common.dtsi,将其中的watchdog部分修改如下:

                watchdog: watchdog@7e100000 {
                        compatible = "brcm,bcm2835-pm-wdt";
                        reg = <0x7e100000 0x28>;
                        timeout-sec = <15>;    //added by zhangyi 2016.5.7
                        //status = "disabled";
                };
默认dts是不使能watchdog的,这里将他启用,同时设置超时时间为15s(在驱动程序的bcm2835_wdt_probe->watchdog_init_timeout()函数中可能会用到)。修改完成后重新编译dtb文件并拷贝到U盘中。


(3)启动看门狗

启动树莓派进入Linux系统后,可以发现在看门狗驱动模块已经顺利加载了:

root@apple:~# lsmod
Module                  Size  Used by
...
bcm2835_wdt             4142  0 
...

内核启动日志输出如下:

[   12.868529] bcm2835-wdt 20100000.watchdog: Broadcom BCM2835 watchdog timer

下面启动看门狗内核线程,超时时间为15s

root@apple:~# echo 0 > /dev/watchdog0
root@apple:~# dmesg -c

[  958.846139] watchdog watchdog0: Create BCM2835 kernel watchdog thread OK! TimeOut = 15 sec


(4)手动触发看门狗复位

接下来为了验证看门狗的功能,这里手动触发内核kernel panic,使得内核进程无法在调度,从而无法执行喂狗,最终系统重启。

root@apple:~# echo c > /proc/sysrq-trigger 

[ 1299.165014] sysrq: SysRq : Trigger a crash
[ 1299.169352] Unable to handle kernel NULL pointer dereference at virtual address 00000000
[ 1299.177544] pgd = c5ee8000
[ 1299.180279] [00000000] *pgd=05e50831, *pte=00000000, *ppte=00000000
[ 1299.186665] Internal error: Oops: 817 [#1] ARM


Entering kdb (current=0xc5838da0, pid 507) Oops: (null)
due to oops @ 0xc0318624
CPU: 0 PID: 507 Comm: bash Tainted: G           O    4.1.15 #5
Hardware name: BCM2708
task: c5838da0 ti: c5ed0000 task.ti: c5ed0000
PC is at sysrq_handle_crash+0x28/0x34
LR is at __handle_sysrq+0x9c/0x16c
pc : [<c0318624>]    lr : [<c0318edc>]    psr: 60000013
sp : c5ed1e60  ip : c5ed1e70  fp : c5ed1e6c
r10: 00000002  r9 : c5ed0000  r8 : 00000000
r7 : 00000005  r6 : 00000063  r5 : c0bc6bc4  r4 : c0c0cb48
r3 : 00000000  r2 : 00000001  r1 : c0c32590  r0 : 00000063
Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user
Control: 00c5387d  Table: 05ee8008  DAC: 00000015
CPU: 0 PID: 507 Comm: bash Tainted: G           O    4.1.15 #5
Hardware name: BCM2708
[<c0016660>] (unwind_backtrace) from [<c0013524>] (show_stack+0x20/0x24)
[<c0013524>] (show_stack) from [<c05263c4>] (dump_stack+0x20/0x28)
[<c05263c4>] (dump_stack) from [<c0010ae4>] (show_regs+0x1c/0x20)
[<c0010ae4>] (show_regs) from [<c00939f0>] (kdb_main_loop+0x33c/0x740)
[<c00939f0>] (kdb_main_loop) from [<c00963e8>] (kdb_stub+0x18c/0x3cc)
[<c00963e8>] (kdb_stub) from [<c008ccfc>] (kgdb_handle_exception+0x27c/0x7c0)
more> 


U-Boot 2015.10 (Jan 02 2016 - 10:49:06 +0800)


DRAM:  128 MiB
RPI Model B rev2
MMC:   bcm2835_sdhci: 0
reading uboot.env
In:    serial
Out:   lcd
Err:   lcd
Net:   Net Initialization Skipped
No ethernet found.
Hit any key to stop autoboot:  0 


可以看到在触发了panic以后的15s后,系统自动重启,重新加载u-boot并引导Linux系统启动,至此树莓派的看门狗功能添加完成。

最后,本文中仅作为一种简单的实现方式,亦可根据需求实现看门狗在用户主进程中或硬件中断处理程序中执行喂狗策略(中断中可监测业务进程的执行状态并判断是否需要喂狗),实现方法和种类很多,原理一样。

你可能感兴趣的:(linux,kernel,嵌入式,内核,树莓派,看门狗)