在嵌入式开发、服务器运维这些场景里,最头疼的事儿莫过于系统“卡死”——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. 代码实现
...staticintusage(intstatus){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);returnstatus;}intmain(intargc,char*argv[]){...structoptionlong_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.");returnusage(1);}load_warn=load;load_reboot=load+0.1;break;case'f':background=0;break;case'x':if(!optarg)extdelay=1;elseextdelay=atoi(optarg);break;case'l':if(!optarg){ERROR("Missing logfile argument.");returnusage(1);}logfile=strdup(optarg);break;case'L':sys_log=1;break;case'w':if(!optarg){ERROR("Missing timeout argument.");returnusage(1);}timeout=atoi(optarg);break;case'k':if(!optarg){ERROR("Missing interval argument.");returnusage(1);}period=atoi(optarg);break;case's':magic=1;break;case'v':printf("v%s\n",VERSION);return0;case'V':verbose=1;break;case'h':returnusage(0);default:printf("Unrecognized option \"-%c\".\n",c);returnusage(1);}}if(background){pid_tpid;if(!logfile)sys_log=1;pid=daemonize(logfile);if(pid)returnpid<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);return1;}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;elseperiod=real_timeout/2;if(!period)period=1;}DEBUG("Watchdog kick interval set to %d sec.",period);create_bootstatus(real_timeout,period);while(1){intrem;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!");}elseif(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 -w30-k10# 等价长选项: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 -x2# 等价: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:
psaux|grepwatchdogd|grep-vgrep|awk'{print$2}' - 给程序发
SIGUSR1信号(触发外部喂狗):sudokill-USR1<watchdogd的PID>
预期效果:
- 原终端日志打印
External kick.,表示喂狗成功; - 若不发信号,超过看门狗超时时间(默认20秒),系统会触发重启(测试时注意保存数据!)。
步骤3:恢复自主喂狗(发SIGUSR2)
sudokill-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/Ubuntutail-f /var/log/syslog|grepwatchdogd# CentOS/RHELtail-f /var/log/messages|grepwatchdogd
负载平均值调整(-a/--load-average)
默认负载超过 0.7 警告、0.8 重启,自定义阈值:
# 设置负载警告阈值为 1.0,重启阈值为 1.1(1.0+0.1)sudo./watchdogd -f -V -a1.0测试负载触发:
- 用
stress工具压测CPU(先安装:sudo apt install stress):stress --cpu4--timeout 60s - 若系统负载超过 1.0,日志打印
System load average very high!; - 超过 1.1 则打印
System load too high, rebooting system ...,并触发看门狗重启。
强制重启测试(SIGPWR 信号)
# 先启动程序sudo./watchdogd -f -V# 另开终端发 SIGPWR 信号sudokill-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【程序猿编码】