在嵌入式开发、服务器运维这些场景里,最头疼的事儿莫过于系统“卡死”——CPU负载飙到满格、进程僵死、甚至整个系统失去响应,没人手动干预的话,设备就彻底“趴窝”了。这时候,“看门狗守护进程(watchdogd)”就成了系统的“贴身保镖”:它盯着系统状态,定期给硬件看门狗“喂饭”,一旦系统出问题没法喂饭,看门狗就会触发硬件重启,把系统从死机状态拉回来。今天我们就拆解这款轻量的watchdogd实现,聊聊它是怎么守护系统稳定的。

一、先搞懂:看门狗到底是个啥?

先说白话版定义:
硬件层面有个“看门狗芯片”,你可以把它理解成一个“倒计时炸弹”——设定好20秒超时,只要系统每隔10秒给它发个“我还活着”的信号(行业里叫“喂狗”),炸弹就重置倒计时;要是系统卡死了,没法发信号,倒计时结束,芯片就会触发硬件重启,让系统重新运行。

而我们今天聊的watchdogd,是运行在用户态的守护进程,核心作用就是“替人干活”:自动定期给硬件看门狗发“喂狗”信号,还能监控系统负载、支持外部程序管控,是连接用户态和硬件看门狗的“桥梁”。

二、核心实现:代码到底在干啥?

这款watchdogd的代码不算复杂,但把看门狗的核心逻辑都覆盖了,整体流程就像“按剧本走流程的保安”,先看简易流程(文字版原理图):

启动程序
  ↓
解析命令行参数(比如是否后台跑、喂狗间隔、是否交给外部管控)
  ↓
后台运行?→ 是:变成守护进程(脱离终端,默认用syslog打日志);否:前台运行
  ↓
创建PID文件(/var/run/watchdogd.pid)→ 方便外部程序找进程ID
  ↓
打开看门狗设备/dev/watchdog(打不开直接退出,没硬件看门狗玩不了)
  ↓
设置硬件超时(比如设20秒)→ 读实际超时值(怕硬件不支持自定义)→ 确定喂狗间隔(默认超时的一半)
  ↓
注册信号处理(比如按Ctrl+C退出、收到SIGPWR强制重启、外部发SIGUSR1喂狗)
  ↓
记录上次重启原因(比如是看门狗触发、CPU过热还是断电)→ 写进/var/run/watchdogd.status
  ↓
进入主循环(核心环节):
  ├─ 没开外部管控:主动调用wdt_kick“喂狗”(给内核发IOCTL指令)
  ├─ 开了外部管控:等外部程序发SIGUSR1喂狗(启动时可设延迟,先自己喂几次)
  ├─ 检查系统负载:负载超阈值→触发重启
  ├─ 睡够喂狗间隔(被信号唤醒就补觉剩余时间)
  └─ 循环往复
  ↓
收到退出信号:
  ├─ 安全退出:给/dev/watchdog写"V"→关闭硬件看门狗→退出
  └─ 普通退出:直接关设备→退出(看门狗仍生效,没人喂就重启)

再拆解几个关键代码逻辑:

1. 代码实现

...
static int usage(int status)
{
	printf("Usage: %s [-f] [-w <sec>] [-k <sec>] [-s] [-h|--help]\n"
               "A simple watchdog deamon that kicks /dev/watchdog every %d sec, by default.\n"
               "Options:\n"
               "  --foreground, -f         Start in foreground (background is default)\n"
	       "  --external-kick, -x [N]  Force external watchdog kick using SIGUSR1\n"
	       "                           A 'N x <interval>' delay for startup is given\n"
	       "  --logfile, -l <file>     Log to <file> when backgrounding, otherwise silent\n"
	       "  --syslog, -L             Use syslog, even if in foreground\n"
               "  --timeout, -w <sec>      Set the HW watchdog timeout to <sec> seconds\n"
               "  --interval, -k <sec>     Set watchdog kick interval to <sec> seconds\n"
               "  --safe-exit, -s          Disable watchdog on exit from SIGINT/SIGTERM\n"
	       "  --load-average, -a <val> Adjust load average check, default: 0.7, reboot at 0.8\n"
	       "  --verbose, -V            Verbose operation, noisy output suitable for debugging\n"
	       "  --version, -v            Display version and exit\n"
               "  --help, -h               Display this help message and exit\n",
               __progname, WDT_TIMEOUT_DEFAULT);

	return status;
}

int main(int argc, char *argv[])
{
...
	struct option long_options[] = {
		{"foreground",    0, 0, 'f'},
		{"external-kick", 2, 0, 'x'},
		{"interval",      1, 0, 'k'},
		{"load-average",  1, 0, 'a'},
		{"logfile",       1, 0, 'l'},
		{"safe-exit",     0, 0, 's'},
		{"syslog",        0, 0, 'L'},
		{"timeout",       1, 0, 'w'},
		{"verbose",       0, 0, 'V'},
		{"version",       0, 0, 'v'},
		{"help",          0, 0, 'h'},
		{NULL, 0, 0, 0}
	};

	while ((c = getopt_long(argc, argv, "a:fx::l:Lw:k:sVvh?", long_options, NULL)) != EOF) {
		switch (c) {
		case 'a':
			load = strtod(optarg, NULL);
			if (load <= 0) {
				ERROR("Load average argument must be greater than zero.");
				return usage(1);
			}
			load_warn   = load;
			load_reboot = load + 0.1;
			break;

		case 'f':	
			background = 0;
			break;

		case 'x':
			if (!optarg)
				extdelay = 1; 
			else
				extdelay = atoi(optarg);
			break;

		case 'l':	
			if (!optarg) {
				ERROR("Missing logfile argument.");
				return usage(1);
			}
			logfile = strdup(optarg);
			break;

		case 'L':	
			sys_log = 1;
			break;

		case 'w':	
			if (!optarg) {
				ERROR("Missing timeout argument.");
				return usage(1);
			}
			timeout = atoi(optarg);
			break;

		case 'k':	
			if (!optarg) {
				ERROR("Missing interval argument.");
				return usage(1);
			}
			period = atoi(optarg);
			break;

		case 's':	
			magic = 1;
			break;

		case 'v':
			printf("v%s\n", VERSION);
			return 0;

		case 'V':
			verbose = 1;
			break;

		case 'h':
			return usage(0);

		default:
			printf("Unrecognized option \"-%c\".\n", c);
			return usage(1);
		}
	}

	if (background) {
		pid_t pid;


		if (!logfile)
			sys_log = 1;

		pid = daemonize(logfile);
		if (pid)
			return pid < 0 ? 1 : 0;
	}

	INFO("Userspace watchdog daemon v%s starting ...", VERSION);

	setup_signals();

	if (pidfile(NULL))
		PERROR("Cannot create pidfile");

	fd = open(WDT_DEVNODE, O_WRONLY);
	if (fd == -1) {
		PERROR("Failed opening watchdog device, %s", WDT_DEVNODE);
		return 1;
	}

	wdt_set_timeout(timeout);

	real_timeout = wdt_get_timeout();
	if (real_timeout < 0) {
		PERROR("Failed reading current watchdog timeout");
	} else {
		if (real_timeout <= period) {
			ERROR("Warning, watchdog timeout <= kick interval: %d <= %d",
			      real_timeout, period);
		}
	}


	if (-1 == period) {
		if (real_timeout < 0)
			period = WDT_KICK_DEFAULT;
		else
			period = real_timeout / 2;

		if (!period)
			period = 1;
	}
	DEBUG("Watchdog kick interval set to %d sec.", period);

	create_bootstatus(real_timeout, period);

	while (1) {
		int rem;

		if (!extkick)
			wdt_kick("Kicking watchdog.");

		if (extdelay) {
			DEBUG("Pending external kick in %d sec ...", extdelay * period);
			if (!--extdelay)
				extkick = 1;
		}

		load = check_loadavg();
		if (load > load_warn && load < load_reboot) {
			WARN("System load average very high!");
		} else if (load > load_reboot) {
			ERROR("System load too high, rebooting system ...");
			wdt_reboot(0);
		}

		rem = period;
		do {
			rem = sleep(rem);
		} while (rem > 0);
	}
}

...

If you need the complete source code, please add the WeChat number (c17865354792)

逐个选项测试示例 & 效果验证

基础帮助/版本查看(无风险,先测)
  • 查看帮助信息(对应 -h/--help):

    ./watchdogd -h
    # 或
    ./watchdogd --help
    

    预期效果:打印所有选项说明,和 usage 函数里的内容一致。

  • 查看版本(对应 -v/--version):

    ./watchdogd -v
    # 或
    ./watchdogd --version
    

    预期效果:输出程序版本号(需确保代码里定义了 VERSION 宏,比如加 #define VERSION "1.0" 在代码开头)。

前台运行+详细日志(核心调试选项,必测)
sudo ./watchdogd -f -V
# 等价长选项:sudo ./watchdogd --foreground --verbose

预期效果

  • 程序不后台运行,直接在终端输出日志;
  • 能看到 Kicking watchdog.(喂狗)、Watchdog kick interval set to X sec. 等详细调试信息;
  • 每隔默认 10 秒(超时 20 秒的一半)打印一次喂狗日志。
自定义超时+喂狗间隔(-w/--timeout + -k/--interval
# 设置硬件看门狗超时为 30 秒,喂狗间隔为 10 秒,前台+详细输出
sudo ./watchdogd -f -V -w 30 -k 10
# 等价长选项:sudo ./watchdogd --foreground --verbose --timeout 30 --interval 10

预期效果

  • 日志里显示 Setting watchdog timeout to 30 sec.
  • 喂狗间隔固定为 10 秒(不再是超时的一半);
  • 若设置的间隔 ≥ 超时(比如 -w 10 -k 10),会打印警告:Warning, watchdog timeout <= kick interval
安全退出测试(-s/--safe-exit
# 启动时加安全退出,前台+详细输出
sudo ./watchdogd -f -V -s
# 另开一个终端,给程序发终止信号(或直接按 Ctrl+C)
# 方式1:按 Ctrl+C(触发 SIGINT)
# 方式2:先查PID:ps aux | grep watchdogd,再杀进程:sudo kill -TERM <PID>

预期效果

  • 退出时日志打印 Safe exit, disabling HW watchdog.
  • 看门狗被关闭,不会因为程序退出触发系统重启(对比不加 -s 的情况:退出日志是 Exiting, watchdog still active.,若没人喂狗,系统会在超时后重启)。
外部喂狗测试(-x/--external-kick

这个选项是让外部程序通过 SIGUSR1 信号控制喂狗,分两步测试:

步骤1:启动程序并设置外部喂狗延迟
# -x 2 表示:先自己喂2次(间隔默认10秒,共20秒),再交给外部管控
sudo ./watchdogd -f -V -x 2
# 等价:sudo ./watchdogd --foreground --verbose --external-kick=2

预期效果

  • 启动后日志打印 Pending external kick in 20 sec ...
  • 前2次喂狗是程序自己来(每隔10秒),之后打印 External supervisor now controls watchdog kick via SIGUSR1.
步骤2:外部发 SIGUSR1 喂狗
  • 另开终端,先查 watchdogd 的 PID:
    ps aux | grep watchdogd | grep -v grep | awk '{print $2}'
    
  • 给程序发 SIGUSR1 信号(触发外部喂狗):
    sudo kill -USR1 <watchdogd的PID>
    

预期效果

  • 原终端日志打印 External kick.,表示喂狗成功;
  • 若不发信号,超过看门狗超时时间(默认20秒),系统会触发重启(测试时注意保存数据!)。
步骤3:恢复自主喂狗(发 SIGUSR2
sudo kill -USR2 <watchdogd的PID>

预期效果

  • 日志打印 External supervisor requested safe exit. Reverting to built-in kick.
  • 程序恢复自己每隔10秒喂狗。
日志文件输出(-l/--logfile
# 后台运行,日志输出到 /tmp/watchdog.log,同时开详细输出
sudo ./watchdogd -V -l /tmp/watchdog.log
# 等价:sudo ./watchdogd --verbose --logfile /tmp/watchdog.log

预期效果

  • 程序后台运行,终端无输出;
  • 查看日志文件能看到所有调试信息:
    tail -f /tmp/watchdog.log
    
强制用 syslog 日志(-L/--syslog

即使前台运行,也把日志输出到系统日志(/var/log/syslog/var/log/messages):

sudo ./watchdogd -f -V -L
# 等价:sudo ./watchdogd --foreground --verbose --syslog

预期效果

  • 终端可能无输出(或少量),日志会写入系统日志;
  • 查看系统日志:
    # Debian/Ubuntu
    tail -f /var/log/syslog | grep watchdogd
    # CentOS/RHEL
    tail -f /var/log/messages | grep watchdogd
    
负载平均值调整(-a/--load-average

默认负载超过 0.7 警告、0.8 重启,自定义阈值:

# 设置负载警告阈值为 1.0,重启阈值为 1.1(1.0+0.1)
sudo ./watchdogd -f -V -a 1.0

测试负载触发

  • stress 工具压测CPU(先安装:sudo apt install stress):
    stress --cpu 4 --timeout 60s
    
  • 若系统负载超过 1.0,日志打印 System load average very high!
  • 超过 1.1 则打印 System load too high, rebooting system ...,并触发看门狗重启。
强制重启测试(SIGPWR 信号)
# 先启动程序
sudo ./watchdogd -f -V
# 另开终端发 SIGPWR 信号
sudo kill -PWR <watchdogd的PID>

预期效果

  • 日志打印 Forced watchdog reboot.
  • 程序将看门狗超时设为 1 秒,关闭设备后等待重启,系统会在 1 秒后重启(测试前务必保存数据!)。

2. 灵活的信号处理:不止能喂狗,还能控退出

程序注册了好几类信号,对应不同操作:

  • SIGINT/SIGTERM(比如Ctrl+C):触发wdt_close→退出程序,开了–safe-exit就先关看门狗;
  • SIGPWR:强制重启→把看门狗超时设为1秒,关设备后等重启;
  • SIGUSR1:外部喂狗→比如你写了个自定义监控程序,检测到业务正常就给watchdogd发这个信号,让它喂狗;
  • SIGUSR2:恢复自主喂狗→外部程序不想管控了,发这个信号,watchdogd就自己继续喂。

3. 负载监控:不止防“卡死”,还防“过载”

程序会定期调用check_loadavg读系统负载,默认负载超过0.8就触发重启——比如服务器扛不住高并发,CPU满了,系统没法处理请求,这时候主动重启比卡死强。

三、设计思路:为啥这么写?

这款watchdogd的设计核心是“默认能用,灵活扩展”,完全贴合实际使用场景:

1. 兼容性优先:适配不同硬件看门狗

有些硬件看门狗不支持自定义超时,程序会先读实际超时值(wdt_get_timeout),再用一半时间当喂狗间隔——比如硬件默认30秒超时,程序就15秒喂一次,既不频繁占用资源,又不会超时重启。

2. 兼顾“自主”和“外部管控”

默认自己喂狗,也支持–external-kick参数:比如系统启动时,外部监控程序还没起来,可设-x 3,让watchdogd先自己喂3次,等外部程序就绪后,再交给外部用SIGUSR1管控——嵌入式场景里,多进程协作时这个功能特别实用。

3. 运维友好:日志+状态文件

  • 日志:支持syslog、日志文件、前台输出,调试时开–verbose就能看详细信息;
  • 状态文件:/var/run/watchdogd.status记录重启原因、超时时间、喂狗间隔——系统重启后,查这个文件就知道上次为啥重启(是看门狗触发?还是CPU过热?)。

4. 安全退出:避免误操作

加了–safe-exit参数,退出时会给/dev/watchdog写"V"——这是内核看门狗驱动的“魔法指令”,告诉硬件“我要正常退出,别重启”,测试或维护时不会误触发重启。

四、必懂的相关知识点(新手也能懂)

想搞懂看门狗开发,这些知识点绕不开:

1. Linux看门狗驱动

/dev/watchdog是内核给用户态留的“接口”,所有和硬件看门狗的交互都通过这个文件,核心靠IOCTL指令(喂狗、设超时、查状态)。

2. 守护进程(Daemon)

程序默认后台运行,通过daemonize函数脱离终端——要是不做守护进程化,终端一关,程序就退出了,看门狗没人喂,系统直接重启,这就本末倒置了。

3. Linux信号机制

信号是Linux进程间通信的“简易方式”,watchdogd靠信号实现“外部管控”,比如SIGUSR1/SIGUSR2就是自定义信号,专门用来控喂狗。

4. PID文件

/var/run/watchdogd.pid存进程ID,外部程序想给watchdogd发信号,先读这个文件找PID就行——这是Linux守护进程的通用做法。

五、总结

这款watchdogd虽然代码精简,但把看门狗守护进程的核心需求都覆盖了:基础喂狗、外部管控、负载监控、安全退出,特别适合嵌入式Linux、边缘服务器这些需要高可用性的场景。

从设计角度看,它遵循了Linux系统编程的“最佳实践”:用守护进程保证后台运行,用信号实现灵活管控,用日志/状态文件方便运维,兼容不同硬件看门狗保证通用性。

对开发者来说,理解这个程序的思路,不光能搞懂看门狗怎么用,还能掌握“用户态与内核驱动交互”“守护进程开发”“系统监控”这些核心技能——不管是做嵌入式开发,还是服务器运维,这些都是必备的硬本事。

简单说,看门狗守护进程的核心就是“兜底”:系统能自己跑的时候,它默默喂狗;系统出问题的时候,它触发重启,让系统“起死回生”——这也是工业级设备、无人值守服务器里,看门狗必不可少的原因。

Welcome to follow WeChat official account【程序猿编码

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐