一、项目背景与目标
在边缘计算场景中(如 RK3588、树莓派),AI 推理程序经常因为内存泄漏、硬件过热等原因半夜闪退。程序一旦挂掉,整个设备就变成了"砖头"——没有告警、没有日志、没人知道它什么时候死的。
本文记录了我使用 Shell 脚本从零打造一个带日志分类、二次验证、自动拉起功能的"看门狗"监控程序,并对比了"常驻循环"与"系统定时器"两种核心架构的实现。
为了测试看门狗,我先写了一个模拟 AI 推理的 Python 脚本(ai_infer.py),让它一直死循环运行:
#!/usr/bin/env python3
import time
# 模拟 AI 推理主循环
while True:
print('AI 推理中…')
time.sleep(2) # 模拟每2秒进行一次推理
二、看门狗架构一:常驻内存(while true + sleep)
这是最经典的守护进程写法,脚本作为一个独立进程常驻后台,每 60 秒巡逻一次。
1. 核心代码(watchdog.sh)
#!/bin/bash
while true
do
ai_infer="/home/panxuqian/watchdog/ai_infer.py"
# 1. 统计进程存活数(核心:必须使用 grep -v grep 排除自身)
process=$(ps -ef | grep ai_infer.py | grep -v grep | wc -l)
# 2. 按日期定义日志文件路径
date_day=$(date "+%Y-%m-%d")
true_logs="/home/panxuqian/watchdog/watchdog_logs/${date_day}_true.log"
false_logs="/home/panxuqian/watchdog/watchdog_logs/${date_day}_false.log"
# 3. 核心状态判断
if [ $process -eq 0 ]; then
echo "$(date '+%H:%M:%S') 进程死亡" >> "$false_logs"
echo "$(date '+%H:%M:%S') 正在拉起进程" >> "$false_logs"
# 后台静默拉起
nohup python3 "$ai_infer" > /dev/null 2>&1 &
sleep 3 # 关键:给新进程留出启动时间
# 二次复查
process_1=$(ps -ef | grep ai_infer.py | grep -v grep | wc -l)
if [ $process_1 -eq 0 ]; then
echo "$(date '+%H:%M:%S') 拉起失败" >> "$false_logs"
else
echo "$(date '+%H:%M:%S') 拉起成功" >> "$false_logs"
fi
else
echo "$(date '+%H:%M:%S') –进程存活" >> "$true_logs"
fi
sleep 60
done
2. 日志分类设计
这里有个小细节值得单独说:日志按 true / false 分成两个文件,而不是一股脑写进同一个文件。
- *_true.log:只记录"进程存活"的心跳,用于确认看门狗本身在正常工作;
- *_false.log:只记录"进程死亡 / 拉起成功 / 拉起失败"等异常事件。
这样排查问题时,直接 tail -f *_false.log 就能看到所有异常,不用在成千上万条心跳里翻找。文件名带上日期,也方便按天归档和清理。
三、看门狗架构二:系统定时器(crontab 强力兜底)
如果你担心 while true 脚本本身哪天也崩溃了(或者被误杀),此时常驻进程就彻底失效了——看门狗自己也需要被看护。更稳妥的方案是:让脚本只执行一次检查,然后交给 Linux 系统的 crontab 定时器去每分钟唤醒它。
1. 单次执行脚本(watchdog_cron.sh)
去掉 while 和 sleep,脚本跑完立刻退出,完全零内存占用:
#!/bin/bash
ai_infer="/home/panxuqian/watchdog/ai_infer.py"
process=$(ps -ef | grep ai_infer.py | grep -v grep | wc -l)
date_day=$(date "+%Y-%m-%d")
true_logs="/home/panxuqian/watchdog/watchdog_logs/${date_day}_true.log"
false_logs="/home/panxuqian/watchdog/watchdog_logs/${date_day}_false.log"
if [ $process -eq 0 ]; then
echo "$(date '+%H:%M:%S') 进程死亡" >> "$false_logs"
nohup python3 "$ai_infer" > /dev/null 2>&1 &
sleep 3
process_1=$(ps -ef | grep ai_infer.py | grep -v grep | wc -l)
if [ $process_1 -eq 0 ]; then
echo "$(date '+%H:%M:%S') 拉起失败" >> "$false_logs"
else
echo "$(date '+%H:%M:%S') 拉起成功" >> "$false_logs"
fi
else
echo "$(date '+%H:%M:%S') –进程存活" >> "$true_logs"
fi
2. 配置系统级计时器(Crontab)
敲 crontab -e,输入以下魔法规则(注意必须写绝对路径):
# 每隔 1 分钟执行一次看门狗脚本
*/1 * * * * /home/panxuqian/watchdog/watchdog_cron.sh
注:crontab 最小单位是分钟。如果真需要秒级响应,通常就用方案一的 while 循环。

四、架构对比与核心避坑点
| 常驻循环(while) | 响应极快,可精确到秒级 | 占用微小内存;自身死掉无人救 | 核心主业务、实时性要求高 |
| 定时器(crontab) | 系统级兜底,脚本跑完即退 | 响应较慢,仅支持分钟级 | 定时巡检、系统级脚本 |
🚨 核心避坑点(面试高频考点)
- grep 的自匹配问题:必须用 grep -v grep 排除自己,否则永远判定为存活。
- 变量静态快照:拉起后用 process_1 重新查询,不能用旧的 process。
- 启动时序竞争:拉起和二次复查之间,必须加 sleep 3,给进程启动留出时间。
- 中文路径与空格:在 nohup 执行时,尽量避开带空格的绝对路径,否则极易报"找不到文件"错误。
五、终极进阶:如何防范"假死"?
无论是 ps 还是 crontab,都只能检测进程"是否存在"。如果进程发生死锁(假死),检测会失效——进程还在,但已经不再干活了。
终极方案:让 AI 程序每次推理完,往一个文件里更新一次时间戳。看门狗检测时,如果发现时间戳超过 3 分钟没变,直接 kill -9 杀掉并重启。这就是商业级监控软件的底层逻辑。
先改造 ai_infer.py,让它在每次推理后刷新心跳文件:
#!/usr/bin/env python3
import time
import os
heartbeat = "/home/panxuqian/watchdog/heartbeat.txt"
# 模拟 AI 推理主循环
while True:
print('AI 推理中…')
time.sleep(2) # 模拟每2秒进行一次推理
# 每次推理完成后刷新心跳时间戳
with open(heartbeat, "w") as f:
f.write(str(int(time.time())))
看门狗侧则把心跳判断嵌进常驻循环里,和进程存活判断形成"双保险"——进程没了就拉起,进程还在但心跳停了就强杀重启:
#!/bin/bash
while true
do
ai_infer="/home/panxuqian/watchdog/ai_infer.py"
process=$(ps -ef | grep ai_infer.py | grep -v grep | wc -l)
date_day=$(date "+%Y-%m-%d")
true_logs="/home/panxuqian/watchdog/watchdog_logs/${date_day}_true.log"
false_logs="/home/panxuqian/watchdog/watchdog_logs/${date_day}_false.log"
heartbeat="/home/panxuqian/watchdog/heartbeat.txt"
if [ $process -eq 0 ]; then
echo "$(date '+%H:%M:%S') 进程死亡" >> "$false_logs"
echo "$(date '+%H:%M:%S') 正在拉起进程" >> "$false_logs"
nohup python3 "$ai_infer" > /dev/null 2>&1 &
sleep 3 # 给新进程留出启动时间
process_1=$(ps -ef | grep ai_infer.py | grep -v grep | wc -l)
if [ $process_1 -eq 0 ]; then
echo "$(date '+%H:%M:%S') 拉起失败" >> "$false_logs"
else
echo "$(date '+%H:%M:%S') 拉起成功" >> "$false_logs"
fi
else
# 进程还在,进一步判断是否假死
if [ -f "$heartbeat" ]; then
now=$(date "+%s")
last=$(stat -c %Y "$heartbeat")
diff_time=$((now – last))
if [ $diff_time -gt 180 ]; then # 心跳停止超过 3 分钟
echo "$(date '+%H:%M:%S') –检测到假死,强制重启" >> "$false_logs"
pkill -9 -f ai_infer.py
sleep 1 # 给系统清理进程的时间
nohup python3 "$ai_infer" > /dev/null 2>&1 &
sleep 3 # 给进程重启的时间
echo "$(date '+%H:%M:%S') 假死重启完成" >> "$false_logs"
else
echo "$(date '+%H:%M:%S') 心跳正常" >> "$true_logs"
fi
else
# 心跳文件不存在(刚启动或尚未生成)
echo "$(date '+%H:%M:%S') –等待心跳文件" >> "$true_logs"
fi
fi
sleep 10
done
这里有几个容易踩的细节:
- stat -c %Y 取的是文件修改时间,比读文件内容更省事,也不用担心写入格式问题;
- pkill -9 之后要 sleep 1,否则旧进程可能还没被回收,新进程起来后又被误判;
- 心跳文件不存在时不能直接判死,刚启动的进程还没来得及写第一次心跳,直接杀会陷入"启动即被杀"的死循环;
- 巡检间隔从 60 秒缩短到 10 秒,因为假死检测的响应精度取决于巡检频率,60 秒一轮意味着最坏要 3 分 60 秒才能发现异常。
从"进程是否存在"到"进程是否还活着",这一步跨越,正是玩具脚本和可用监控之间的分水岭。
六、进阶版完整运行展示







