程序退出了怎么办?用 BusyBox init 让 ARM64 服务自动恢复
上一篇已经让 debug_demo 随 Linux 开机自动运行。但开机之后,如果程序意外退出,启动脚本会再调用一次吗?
不会。start-stop-daemon 完成启动后就退出了,它不是一直盯着应用的监督进程。设备虽然还在运行,应用却可能已经停止工作。
这一篇继续使用 Ubuntu、QEMU 和 Buildroot,不需要开发板,也暂不引入 systemd。我们把程序交给 BusyBox init,通过 respawn 在进程退出后重新启动它,再用故障注入观察实际效果。
一、从开机启动到退出后重新启动
本文承接《Buildroot 开机自启动:让你的 ARM64 程序随 Linux 启动运行》。请先完成上一篇,确认已有 –service 模式及可用的新输出目录。
本次只解决一个问题:程序进程退出后,由谁重新启动它?
上一篇:rcS -> S99debug-demo -> 后台启动应用 -> 启动工具退出
本篇:BusyBox init -> 直接启动前台应用
-> 回收退出的子进程
-> 按 respawn 配置再次启动应用
最终检查四件事:
respawn 不是只在“崩溃”时重启,而是在被监督进程退出后重新启动。它不能判断业务是否正常,也不能发现一个没有退出却已经卡死的程序。
二、实验环境与操作约定
| 开发主机 | Ubuntu 20.04 x86_64 虚拟机 |
| Buildroot | 2025.02.8,位于 ~/buildroot-2025.02.8 |
| 输出目录 | ~/buildroot-output-arm64-demo |
| 外部目录 | ~/buildroot-external-demo,标识为 ARM64_DEMO |
| 目标 init | BusyBox init;上一篇目标端为 BusyBox 1.37.0 |
| 服务程序 | /usr/bin/debug_demo –service |
| 日志 | /var/log/debug-demo.log |
默认命令在 Ubuntu 主机执行,只有标注“QEMU 目标端”的命令才在 ARM64 系统内执行。不要在 Windows PowerShell 直接执行本文的 Bash 命令。
本篇不修改 C 源码,也不改变工具链、内核或 init 类型。仍然使用上一篇的前台持续运行模式,以及收到 SIGTERM 后记录 stopped 并退出的逻辑。
三、先观察旧方案为什么不会自动恢复
如果上一篇的 QEMU 仍在运行,可直接在 QEMU 目标端完成本节。如果已关闭,可使用上一篇第八节的完整 QEMU 命令启动原镜像。暂时不要修改构建文件。
先确认原服务正常:
/etc/init.d/S99debug-demo status
cat /var/run/debug-demo.pid
ps | grep '[d]ebug_demo'
tail -n 5 /var/log/debug-demo.log
接下来直接终止应用进程,观察旧方案是否会自动拉起它。在 QEMU 目标端执行下面的命令。命令会读取 PID 文件,并核对 /proc 中的可执行文件;检查不通过就不会发送信号:
service_pid=$(cat /var/run/debug-demo.pid) || exit 1
case "$service_pid" in
''|*[!0-9]*) echo 'Invalid PID file; stop and investigate.' >&2; exit 1 ;;
esac
[ "$service_pid" -gt 1 ] || exit 1
[ "$(readlink "/proc/$service_pid/exe")" = /usr/bin/debug_demo ] || {
echo 'PID does not belong to debug_demo; stop and investigate.' >&2
exit 1
}
printf 'Sending SIGKILL to debug_demo pid=%s\\n' "$service_pid"
kill -KILL "$service_pid"
echo $?
sleep 5
/etc/init.d/S99debug-demo status
echo $?
ps | grep '[d]ebug_demo'
预期 kill 返回 0;等待五秒后仍没有服务实例,status 显示未运行并返回上一篇约定的 1,进程查询没有输出。这说明旧方案没有持续监督应用。由于 SIGKILL 不能被程序捕获,日志不会写入 stopped;PID 文件可能暂时残留,但状态脚本会根据进程检查报告未运行。这里是受控的非正常终止实验,不代表真实崩溃原因分析。后面会用同样的信号验证 respawn 能否重新启动进程。
结束后在 QEMU 目标端执行:
poweroff
等系统关机后,如果 QEMU 没有退出,再按 Ctrl+A,松开后按 X。后续构建和备份期间,不要让 QEMU 使用同一可写镜像,也不要从主机挂载它。
四、备份配置,确认迁移前提
回到 Ubuntu 主机:
export DEMO_OUT="$HOME/buildroot-output-arm64-demo"
cd ~/buildroot-2025.02.8
test -f "$DEMO_OUT/.config"
echo $?
grep -E '^BR2_(INIT_BUSYBOX|PACKAGE_BUSYBOX|PACKAGE_DEBUG_DEMO)=' "$DEMO_OUT/.config"
grep '^BR2_ROOTFS_OVERLAY=' "$DEMO_OUT/.config"
grep -E '^CONFIG_(INIT|FEATURE_USE_INITTAB|PIDOF)=y$' \\
"$DEMO_OUT"/build/busybox-*/.config
文件检查应返回 0,三个 Buildroot 选项均应为 y;BusyBox 应启用 INIT、FEATURE_USE_INITTAB 和用于后续检查的 PIDOF。如果缺少选项,先补齐并保存自定义 BusyBox 配置、重建验证,再继续。不要根据 Ubuntu 主机工具推断目标端功能。
本篇只使用已有的一个外部目录;如果你另外配置了其他 BR2_EXTERNAL 目录,后续命令必须保留完整列表。
备份外部文件、完整配置和当前已生成的 inittab:
BACKUP_DIR="$HOME/debug-demo-before-respawn-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
cp -a "$HOME/buildroot-external-demo" "$BACKUP_DIR/"
cp -p "$DEMO_OUT/.config" "$BACKUP_DIR/buildroot.config"
cp -p "$DEMO_OUT/target/etc/inittab" "$BACKUP_DIR/inittab.generated"
printf 'Backup: %s\\n' "$BACKUP_DIR"
确认每条复制命令成功,记下备份目录,再进行修改。本篇只新增自己的 overlay,不移动、覆盖已有 overlay;仍需检查已有 overlay 或后处理脚本是否会覆盖同名文件。
五、理解 respawn:监督的必须是应用本身
BusyBox inittab 的格式是:
<id>:<runlevels>:<action>:<process>
本篇新增的条目为:
::respawn:/usr/bin/debug_demo –service
各字段含义如下:
| 空 id | 使用默认控制台关联,不是给服务起名字 |
| 空 runlevels | BusyBox init 不按 SysV init 的运行级别管理服务 |
| respawn | 进程退出后再次启动 |
| 最后一段 | 要直接执行的程序及参数 |
不要把这一行写成 /etc/init.d/S99debug-demo start,也不要添加 & 或 start-stop-daemon -b。这些命令会把应用放到后台后很快返回,init 监督到的就不是持续运行的应用,可能出现反复启动或重复实例。
上一篇程序恰好已经满足要求:–service 模式持续运行、不自行 fork、能够处理终止信号。我们改变管理方式,而不是再给它套一个无限重启的 shell 循环。
BusyBox init 会在启动阶段的 sysinit 等动作完成后处理 respawn。这不表示网络或外设一定已经就绪,本例仍不依赖它们。
六、关闭旧启动入口,保留构建一致性
如果只添加 respawn,而保留旧 S99debug-demo 的后台启动,两条路径就可能各启动一个实例。
本篇保留软件包中的 DEBUG_DEMO_INSTALL_INIT_SYSV 安装接口,但把它安装的脚本改成兼容入口:start 只准备日志目录,stop 不再操作服务,其他参数明确报错。这样增量构建会覆盖旧脚本,不会因为只删除安装规则而留下旧文件。
在 Ubuntu 主机执行:
cd ~/buildroot-external-demo
cat > package/debug-demo/S99debug-demo <<'EOF'
#!/bin/sh
case "$1" in
start)
mkdir -p /var/log || exit 1
;;
stop)
exit 0
;;
*)
echo "debug-demo is managed by BusyBox init; inspect /etc/inittab and processes" >&2
exit 1
;;
esac
EOF
chmod 0755 package/debug-demo/S99debug-demo
sh -n package/debug-demo/S99debug-demo
echo $?
语法检查应返回 0。文件使用 LF 换行。
从这里开始,旧脚本不再提供服务的 start/stop/restart/status 管理语义。 rcS 和 rcK 仍可调用这个兼容入口,但它不创建 PID 文件,也不向应用发信号。正常关机时,进程终止由 init 的关机流程处理,须在本次实验中观察验证。
本篇保留的只是一个迁移用的兼容脚本,不是一套新的服务管理器。不要继续用 /var/run/debug-demo.pid 判断服务状态,也不要绕过 init 手动运行额外的 –service 实例。
七、用 overlay 保存新的 inittab
不要用只含一行 respawn 的文件覆盖整个 inittab,否则可能丢掉挂载、系统初始化、登录终端及关机动作。
本例从上一篇已经可启动的目标 inittab 复制一份,再只追加应用条目。这样也保留了当前构建生成的串口配置。复制后的文件将作为自定义配置维护,不会自动跟随上游版本变化。
在 Ubuntu 主机执行完整代码块;子 shell 中的检查失败会中止本块,不关闭当前终端:
(
set -eu
INIT_OVERLAY="$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay"
INIT_SOURCE="${DEMO_OUT😕请先设置 DEMO_OUT}/target/etc/inittab"
test -f "$INIT_SOURCE"
test ! -e "$INIT_OVERLAY/etc/inittab"
if grep -q 'debug_demo' "$INIT_SOURCE"; then
printf 'Existing debug_demo entry found; review it before proceeding.\\n' >&2
exit 1
fi
mkdir -p "$INIT_OVERLAY/etc"
cp -p "$INIT_SOURCE" "$INIT_OVERLAY/etc/inittab"
printf '\\n::respawn:/usr/bin/debug_demo –service\\n' >> "$INIT_OVERLAY/etc/inittab"
chmod 0644 "$INIT_OVERLAY/etc/inittab"
)
echo $?
返回 0 后继续。若文件已存在,不要删除后重试;先比较已有内容并合并,避免覆盖之前的实验。
检查差异:
diff -u "$DEMO_OUT/target/etc/inittab" \\
"$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittab"
预期只多出空行和一条 respawn。这里 diff 返回 1 表示存在差异,不是构建失败;若还改动了其他启动项,先查清原因。
本次检查的实际输出如下:
user@qemu-host:~/buildroot-external-demo$ diff -u "$DEMO_OUT/target/etc/inittab" \\
> "$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittab"
— /home/user/buildroot-output-arm64-demo/target/etc/inittab 2026-10-17 04:48:02.276000000 +0000
+++ /home/user/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittab 2026-10-22 07:05:08.404000000 +0000
@@ -38,3 +38,5 @@
::shutdown:/etc/init.d/rcK
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
+
+::respawn:/usr/bin/debug_demo –service
输出显示 overlay 文件保留了原有的关机配置,并只新增空行和 respawn 项;命令返回 1 是 diff 表示文件不同的常规结果,符合预期。
1. 在 Buildroot 中追加 overlay
cd ~/buildroot-2025.02.8
make O="${DEMO_OUT😕请先设置 DEMO_OUT}" \\
BR2_EXTERNAL="$HOME/buildroot-external-demo" menuconfig
进入 System configuration,找到 Root filesystem overlay directories。保留现有路径,在末尾用空格追加:
$(BR2_EXTERNAL_ARM64_DEMO_PATH)/board/qemu-arm64-demo/rootfs-overlay
这里是在配置菜单中输入字面文本,不是在 shell 中执行。若原值为空,只填写这一项;若原值是前文的 /home/user/buildroot-2025.02.8/rootfs-overlay,就保留它再追加新项。
保存退出后查看:
grep '^BR2_ROOTFS_OVERLAY=' "$DEMO_OUT/.config"
本次检查的实际输出如下,原有 overlay 与新增 overlay 均保留在配置中:
user@qemu-host:~/buildroot-2025.02.8$ grep '^BR2_ROOTFS_OVERLAY=' "$DEMO_OUT/.config"
BR2_ROOTFS_OVERLAY="/home/user/buildroot-2025.02.8/rootfs-overlay $(BR2_EXTERNAL_ARM64_DEMO_PATH)/board/qemu-arm64-demo/rootfs-overlay"
后面的 overlay 可覆盖前面目录中的同名文件。仍需确认已有 overlay 没有旧的 debug_demo 或 S99debug-demo,后处理脚本也没有改写这些文件。本篇后续的产物比较会再次检查结果。
2. 保存独立的 defconfig
这次改变了 Buildroot 配置,应保存新的 defconfig。下面使用新名称,不覆盖第四篇的配置;如果该名称已存在,先备份:
make O="${DEMO_OUT😕请先设置 DEMO_OUT}" \\
BR2_EXTERNAL="$HOME/buildroot-external-demo" \\
BR2_DEFCONFIG="$HOME/buildroot-external-demo/configs/arm64_demo_respawn_defconfig" \\
savedefconfig
echo $?
预期返回 0。需要一起保留外部源码、包规则、兼容脚本及 overlay,不能只保存 defconfig。旧 defconfig 也不是旧源码的快照:外部脚本已经修改,要恢复上一篇行为必须同时恢复文件。
八、重建软件包和镜像
仍在 Ubuntu 主机的 Buildroot 目录:
make O="${DEMO_OUT😕请先设置 DEMO_OUT}" \\
BR2_EXTERNAL="$HOME/buildroot-external-demo" debug-demo-rebuild V=1
echo $?
确认返回 0,再生成镜像:
make O="${DEMO_OUT😕请先设置 DEMO_OUT}" \\
BR2_EXTERNAL="$HOME/buildroot-external-demo" -j2
echo $?
保留两次返回码,有错误就停止。不要重新加载官方 defconfig,也不需要执行 make clean。沿用的 -O0 调试编译参数及可能出现的 _FORTIFY_SOURCE 告警,含义与上一篇相同,不代表已达到生产加固要求。
这两条构建命令必须在最后一次修改源码、包规则、overlay 或 Buildroot 配置之后执行;如果之后又改了其中任何一项,就重新执行完整 make。target/ 中的检查不能证明现有镜像已包含最新文件,镜像时间戳只能作为线索,最终仍要启动 QEMU 检查实际生效的配置和服务。
构建完成后检查:
cmp "$HOME/buildroot-external-demo/package/debug-demo/S99debug-demo" \\
"$DEMO_OUT/target/etc/init.d/S99debug-demo"
echo $?
grep -n '^::respawn:/usr/bin/debug_demo –service$' \\
"$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittab"
echo $?
sh -n "$DEMO_OUT/target/etc/init.d/S99debug-demo"
echo $?
ls -l "$DEMO_OUT/target/etc/init.d/S99debug-demo"
ls -lh "$DEMO_OUT/images/Image" "$DEMO_OUT/images/rootfs.ext2"
启动脚本与包规则中的源文件比较、overlay 中 respawn 条目的检查,以及脚本语法检查均应返回 0;启动脚本仍有执行权限。不要仅凭 overlay 文件与 $DEMO_OUT/target/etc/inittab 的 cmp 判断最终镜像:本次检查时,output/target 中没有 overlay 新增的条目,但启动后的 QEMU 目标端确认该条目存在。Buildroot 文档说明 overlay 会覆盖构建后的目标文件系统;若本地 output/target 与 overlay 不一致,应结合构建过程排查,并以目标端检查确认实际启动的镜像内容。
本次曾执行 overlay 与 output/target 的 cmp,结果如下:
cmp: EOF on /home/user/buildroot-output-arm64-demo/target/etc/inittab after byte 1281, line 40
user@qemu-host:~/buildroot-2025.02.8$ echo $?
1
user@qemu-host:~/buildroot-2025.02.8$ grep -n 'debug_demo' "$DEMO_OUT/target/etc/inittab"
这表示 output/target/etc/inittab 比 overlay 文件短,目标目录中没有匹配项;在本次构建中这不是镜像验证失败,但也不能据此认定 overlay 已进入最终镜像。第九节启动 QEMU 后,再检查目标端 /etc/inittab 中的实际条目。
target/ 内容只能证明构建目录的状态,实际是否生效还要进入新镜像检查。
九、开机检查:应用是否真的由 init 监督
确认没有其他 QEMU 使用同一镜像或占用本机端口 5000。在 Ubuntu 主机执行:
/usr/bin/qemu-system-aarch64 \\
-M virt \\
-cpu cortex-a53 \\
-smp 2 \\
-m 1G \\
-nographic \\
-kernel "${DEMO_OUT😕请先设置 DEMO_OUT}/images/Image" \\
-append "rootwait root=/dev/vda console=ttyAMA0" \\
-drive file="$DEMO_OUT/images/rootfs.ext2",if=none,format=raw,id=hd0 \\
-device virtio-blk-device,drive=hd0 \\
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:5000-:2345 \\
-device virtio-net-device,netdev=net0
端口转发只是沿用前文,不参与本次监督。QEMU 启动后等待 buildroot login:,输入 root 登录,确认提示符变为目标端的 # 后再执行下面的命令。这些检查不能在 Ubuntu 主机的 embed@…$ 提示符下运行;也不要手动启动应用。
在 QEMU 目标端执行:
uname -m
readlink /proc/1/exe
grep -n 'debug_demo' /etc/inittab
cat /etc/init.d/S99debug-demo
command -v pidof
pidof debug_demo
ps | grep '[d]ebug_demo'
tail -n 5 /var/log/debug-demo.log
预期架构为 aarch64,PID 1 为 BusyBox,目标配置与新脚本一致,pidof 返回一个 PID,进程参数为 /usr/bin/debug_demo –service。
本次新镜像启动后的实际检查结果如下:
buildroot login: root
# grep -n '^::respawn:/usr/bin/debug_demo –service$' /etc/inittab
42:::respawn:/usr/bin/debug_demo –service
# cat /etc/init.d/S99debug-demo
#!/bin/sh
case "$1" in
start)
mkdir -p /var/log || exit 1
;;
stop)
exit 0
;;
*)
echo "debug-demo is managed by BusyBox init; inspect /etc/inittab and processes" >&2
exit 1
;;
esac
# command -v pidof
/bin/pidof
# pidof debug_demo
120
# ps | grep '[d]ebug_demo'
120 root /usr/bin/debug_demo –service
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=120 tick=1 total=100
debug-demo service: pid=120 tick=2 total=100
debug-demo service: pid=120 tick=3 total=100
debug-demo service: pid=120 tick=4 total=100
debug-demo service: pid=120 tick=5 total=100
这份目标端记录显示,启动后的 /etc/inittab 中存在 respawn 条目,旧启动脚本也已换成不会在 start 时启动应用的兼容版本;当前只有一个服务实例,日志持续增长。因此可以确认新镜像包含预期配置且服务正在运行。这还不能单独证明 init 会在进程退出后重新启动它,自动恢复行为仍需按下一节终止进程并观察新实例。
再核对可执行文件和父进程:
service_pid=$(pidof debug_demo)
case "$service_pid" in
''|*[!0-9]*) echo 'Expected exactly one service PID; stop and investigate.' ;;
*)
readlink "/proc/$service_pid/exe"
grep -E '^(Name|Pid|PPid):' "/proc/$service_pid/status"
;;
esac
预期可执行文件为 /usr/bin/debug_demo,PPid 为 1。单凭父 PID 为 1 还不足以证明监督成立,因为后台孤儿进程也可能被 PID 1 接管;要结合新配置、旧入口已停用和下一节的恢复实验判断。
本次检查的实际输出如下:
# readlink "/proc/$service_pid/exe"
/usr/bin/debug_demo
# grep -E '^(Name|Pid|PPid):' "/proc/$service_pid/status"
Name: debug_demo
Pid: 120
PPid: 1
间隔检查日志:
tail -n 5 /var/log/debug-demo.log
sleep 4
tail -n 5 /var/log/debug-demo.log
预期当前 PID 的 tick 增长,total 为 100。PID 和计数值以本次输出为准,不预设固定值。
本次间隔检查的实际输出如下:
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=120 tick=109 total=100
debug-demo service: pid=120 tick=110 total=100
debug-demo service: pid=120 tick=111 total=100
debug-demo service: pid=120 tick=112 total=100
debug-demo service: pid=120 tick=113 total=100
# sleep 4
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=120 tick=112 total=100
debug-demo service: pid=120 tick=113 total=100
debug-demo service: pid=120 tick=114 total=100
debug-demo service: pid=120 tick=115 total=100
debug-demo service: pid=120 tick=116 total=100
两次查看之间,最新 tick 从 113 增长到 116,PID 保持为 120,total 为 100,说明当前实例仍在持续写日志。两次输出有部分行重叠是正常的。
十、故障注入:终止进程,观察是否重新启动
以下信号命令只在 QEMU 实验目标端执行。不要对 PID 1 发终止信号,不要把 PID 写死,也不要用 killall 代替实例检查。
先在当前目标 shell 定义一个辅助函数。它只接受单个数字 PID,并核对可执行文件;检查失败就不发送信号:
signal_demo() {
service_pid=$(pidof debug_demo) || return 1
case "$service_pid" in
''|*[!0-9]*)
echo 'Expected exactly one debug_demo instance' >&2
return 1
;;
esac
[ "$service_pid" -gt 1 ] || return 1
[ "$(readlink "/proc/$service_pid/exe")" = /usr/bin/debug_demo ] || return 1
printf 'Sending %s to debug_demo pid=%s\\n' "$1" "$service_pid"
kill "-$1" "$service_pid"
}
这个函数适合单人顺序实验,不能消除检查与发信号之间所有 PID 复用竞争。若程序已经反复退出,不要继续注入故障,先处理启动失败。
1. 用 SIGKILL 模拟非正常终止
signal_demo KILL
echo $?
sleep 3
pidof debug_demo
ps | grep '[d]ebug_demo'
tail -n 10 /var/log/debug-demo.log
预期信号命令返回 0。随后不手动启动,出现新实例,且仍只有一个服务进程。短时间内的新 PID 通常不同于函数打印的旧 PID,日志中的新实例从 tick=0 开始记录。
本次 SIGKILL 故障注入的实际结果如下:
# signal_demo KILL
Sending KILL to debug-demo pid=120
# echo $?
0
# sleep 3
# pidof debug_demo
138
# ps | grep '[d]ebug_demo'
138 root /usr/bin/debug_demo –service
# tail -n 10 /var/log/debug-demo.log
debug-demo service: pid=120 tick=144 total=100
debug-demo service: pid=120 tick=145 total=100
debug-demo service: pid=120 tick=146 total=100
debug-demo service: pid=120 tick=147 total=100
debug-demo service: pid=120 tick=148 total=100
debug-demo service: pid=120 tick=149 total=100
debug-demo service: pid=120 tick=150 total=100
debug-demo service: pid=120 tick=151 total=100
debug-demo service: pid=138 tick=0 total=100
信号发送成功后,旧 PID 120 不再出现在进程列表中,新实例 PID 为 138,且从 tick=0 开始写日志。前几行 PID 120 的记录是文件中保留的历史日志,不代表旧进程仍在运行;本次结果验证了 SIGKILL 后 init 自动启动新实例。
SIGKILL 不能被应用捕获,所以不应要求旧实例留下 stopped。这只是非正常终止实验,不是制造段错误,也不是查明崩溃原因。
若未及时看到实例,不要手动启动后记作成功。保留输出,继续观察并检查配置和日志;本文不承诺固定的恢复时延。
2. 用 SIGTERM 验证正常退出也会重新启动
signal_demo TERM
echo $?
sleep 3
pidof debug_demo
ps | grep '[d]ebug_demo'
tail -n 10 /var/log/debug-demo.log
预期旧实例处理信号,写入 stopped 并退出;init 随后启动新实例。若操作间隔较长,旧记录可能已不在最后十行中,应按旧 PID 查找日志,不要仅凭一次 tail 判断退出逻辑没有执行。
本次 SIGTERM 验证的实际结果如下:
# signal_demo TERM
Sending TERM to debug-demo pid=138
# echo $?
0
# sleep 3
# pidof debug_demo
147
# ps | grep '[d]ebug_demo'
147 root /usr/bin/debug_demo –service
# tail -n 10 /var/log/debug-demo.log
debug-demo service: pid=138 tick=23 total=100
debug-demo service: pid=138 tick=24 total=100
debug-demo service: pid=138 tick=25 total=100
debug-demo service: pid=138 tick=26 total=100
debug-demo service: pid=138 tick=27 total=100
debug-demo service: pid=138 tick=28 total=100
debug-demo service: pid=138 stopped
debug-demo service: pid=147 tick=0 total=100
debug-demo service: pid=147 tick=1 total=100
debug-demo service: pid=147 tick=2 total=100
信号发送返回 0 后,旧进程 PID 138 写入 stopped 并退出;当前进程列表中只有新 PID 147,其日志从 tick=0 开始。这验证了正常处理 SIGTERM 后,BusyBox init 也会重新启动服务。
这说明:发送正常终止信号,并不等于停用监督。 对启用了 respawn 的进程反复执行 kill,并不能实现持续停止。
最后再次间隔查看日志,确认新实例仍在运行,而不只是曾经成功启动一次:
tail -n 5 /var/log/debug-demo.log
sleep 4
tail -n 5 /var/log/debug-demo.log
本次对新实例的间隔检查结果如下:
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=147 tick=24 total=100
debug-demo service: pid=147 tick=25 total=100
debug-demo service: pid=147 tick=26 total=100
debug-demo service: pid=147 tick=27 total=100
debug-demo service: pid=147 tick=28 total=100
# sleep 4
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=147 tick=26 total=100
debug-demo service: pid=147 tick=27 total=100
debug-demo service: pid=147 tick=28 total=100
debug-demo service: pid=147 tick=29 total=100
debug-demo service: pid=147 tick=30 total=100
两次查看之间,最新 tick 从 28 增长到 30,PID 保持为 147,total 为 100,说明新实例仍在持续运行。重复出现的 tick=26 至 28 是两次 tail 输出重叠的正常现象。
十一、维护时如何停用,以及怎样恢复
本文先使用最容易核对的流程:修改目标启动配置,再正常重启,使新的监督配置生效。 代价是需要重启整个实验系统,不是在线服务控制方案。
BusyBox init 的在线重读涉及 SIGHUP,删除条目后的进程处理还与版本和 CONFIG_FEATURE_KILL_REMOVED 等选项有关。本稿不把未实测的在线重载当作停止流程,也不要套用其他 init 的 telinit 命令。
1. 在当前镜像中暂时停用
在 QEMU 目标端先检查,确保只有一条完全匹配的监督配置:
grep -n '^::respawn:/usr/bin/debug_demo –service$' /etc/inittab
下面代码块会备份并移除这一条配置;如果备份已存在,会中止,先检查已有实验状态,不要直接覆盖:
(
set -eu
test ! -e /etc/inittab.with-debug-demo
test "$(grep -c '^::respawn:/usr/bin/debug_demo –service$' /etc/inittab)" -eq 1
cp -p /etc/inittab /etc/inittab.with-debug-demo
sed '/^::respawn:\\/usr\\/bin\\/debug_demo –service$/d' \\
/etc/inittab.with-debug-demo > /etc/inittab
)
echo $?
返回 0 后核对其他条目仍然保留:
cat /etc/inittab
grep -n 'debug_demo' /etc/inittab
本例最后一条 grep 应无输出,返回 1 表示没有匹配,不是文件检查失败。修改文件此刻不代表已加载的新配置,原进程仍可能运行。
确认文件正确后,仅在 QEMU 目标端执行:
sync
reboot
若出现新一轮启动信息和 login:,直接登录;若 QEMU 已退出并回到 Ubuntu 提示符,才使用第九节完整命令启动同一镜像,不要重新构建镜像。若既没有登录提示也没有退出,保留输出排查,不要另开第二个 QEMU。
登录后检查:
grep -n 'debug_demo' /etc/inittab
pidof debug_demo
echo $?
ps | grep '[d]ebug_demo'
sleep 4
pidof debug_demo
echo $?
预期没有有效监督条目,连续检查都没有服务进程,pidof 返回非零。这里不用旧脚本的 status,也不能仅根据日志文件是否存在判断运行状态。
本次停用并重启后的实际检查结果如下:
buildroot login: root
# grep -n 'debug_demo' /etc/inittab
# pidof debug_demo
# echo $?
1
# ps | grep '[d]ebug_demo'
# sleep 4
# pidof debug_demo
# echo $?
1
目标端配置中没有 debug_demo 条目,进程列表无输出;重启登录后及间隔四秒再次检查,pidof 均返回 1,符合已停用监督且服务未运行的预期。
2. 恢复当前镜像中的监督配置
仍在 QEMU 目标端执行:
cp -p /etc/inittab.with-debug-demo /etc/inittab
echo $?
grep -n '^::respawn:/usr/bin/debug_demo –service$' /etc/inittab
复制返回 0,确认配置已恢复后执行 sync、reboot。按照上面的判断方式等待登录或重新启动已退出的 QEMU,不重建镜像。
本次恢复配置的实际输出如下:
# cp -p /etc/inittab.with-debug-demo /etc/inittab
# echo $?
0
# grep -n '^::respawn:/usr/bin/debug_demo –service$' /etc/inittab
42:::respawn:/usr/bin/debug_demo –service
这确认 /etc/inittab 文件已恢复;当前运行中的 init 尚未因此重新加载配置。接下来按顺序操作:
sync
reboot
如果原 QEMU 窗口继续显示启动信息,等 buildroot login: 出现后输入 root。如果 QEMU 退出并回到 Ubuntu 主机的 embed@…$ 提示符,使用第九节的完整命令重新启动同一镜像,再登录 root。不要重新构建,也不要同时启动第二个使用该镜像的 QEMU。
登录后确认提示符是目标端的 #,不要手动启动服务。在目标端执行:
grep -n '^::respawn:/usr/bin/debug_demo –service$' /etc/inittab
pidof debug_demo
ps | grep '[d]ebug_demo'
tail -n 5 /var/log/debug-demo.log
sleep 4
tail -n 5 /var/log/debug-demo.log
预期能再次找到 respawn 条目,pidof 只返回一个 PID,进程列表中只有一个服务实例,间隔查看时该 PID 的 tick 继续增长。重启前后的 PID 可以相同,不能用 PID 是否变化证明系统重启。
本次恢复配置并重启后的实际检查结果如下:
# grep -n '^::respawn:/usr/bin/debug_demo –service$' /etc/inittab
42:::respawn:/usr/bin/debug_demo –service
# pidof debug_demo
119
# ps | grep '[d]ebug_demo'
119 root /usr/bin/debug_demo –service
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=119 tick=20 total=100
debug-demo service: pid=119 tick=21 total=100
debug-demo service: pid=119 tick=22 total=100
debug-demo service: pid=119 tick=23 total=100
debug-demo service: pid=119 tick=24 total=100
# sleep 4
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=119 tick=22 total=100
debug-demo service: pid=119 tick=23 total=100
debug-demo service: pid=119 tick=24 total=100
debug-demo service: pid=119 tick=25 total=100
debug-demo service: pid=119 tick=26 total=100
重启登录后未手动启动服务;配置中恢复了 respawn 条目,进程列表只有 PID 119,间隔查看时最新 tick 从 24 增长到 26。这验证了恢复监督配置后服务再次随系统启动并持续运行。
3. 区分镜像内实验和构建源配置
本节修改的是当前可写 rootfs 镜像中的 /etc/inittab。它可能在该镜像重启后保留,但下一次 Buildroot 生成镜像时,仍会使用主机 overlay 中的配置。
如果要让后续生成的镜像也停用服务,应在 Ubuntu 主机修改外部 overlay,移除相应 respawn 行,保留其他条目,然后在 QEMU 关闭的情况下重新生成镜像。此时不要重新启用旧脚本,否则只是换了一种方式启动服务。
十二、自动拉起不是长期可靠性的全部
| 开机有两个服务实例 | 旧脚本是否仍在后台启动;是否还有其他启动项或手动实例 |
| PID 反复变化,日志不增长 | 应用是否启动后立即失败;日志目录、权限、空间及参数是否正确 |
| 杀掉后没有新实例 | 当前镜像的 inittab 是否正确;是否确实使用 BusyBox init;应用是否可执行 |
| TERM 后又出现进程 | 这是 respawn 的预期行为,不等于信号没有生效 |
| 程序还在但不工作 | respawn 只处理退出,不能代替健康检查 |
| 改了目标文件,重新构建后恢复旧行为 | 是否只改了镜像,没有修改主机 overlay |
| 停用后仍有历史日志 | 日志存在不表示进程存活,检查进程和新日志增长 |
| 没有登录提示 | 是否误删了原来的控制台或初始化条目;先检查 inittab 差异 |
不要把其他 init 的“短时间重启若干次后暂停”等策略直接套到 BusyBox。它的重启节奏和相关行为需要按具体版本核对并实测;本篇没有实现可配置退避、重试次数上限或失败告警。
本例还没有解决以下问题:
- 程序持续启动失败时,如何阻止反复重启和重复报错。
- 服务没有退出但已经卡住时,如何识别故障。
- 重新启动后,如何恢复应用状态,避免重复处理业务。
- 日志怎样限额、轮转,以及如何以更小权限运行。
另外,本文的应用仍按 root 运行,日志没有大小限制,只适合短时间实验。进程重新启动不等于业务恢复,也不等于故障原因已经解决。
十三、回退到上一篇方案
若希望恢复旧的脚本管理方式,先正常关闭 QEMU。在 Ubuntu 主机确认 BACKUP_DIR 仍指向第四节创建的备份;如果换过终端,需要先手动设置为记录的实际路径。
移除 overlay 引用不会自动清理 target/ 中残留的文件。 因为本篇曾用 overlay 覆盖 inittab,下面在恢复配置和旧脚本的同时,也恢复第四节保存的旧目标配置。若中途还有其他配置修改,先备份当前文件并比较合并,不要直接覆盖。
(
set -eu
test -f "${BACKUP_DIR😕请先设置实际备份目录}/buildroot.config"
test -f "$BACKUP_DIR/buildroot-external-demo/package/debug-demo/S99debug-demo"
test -f "$BACKUP_DIR/inittab.generated"
cp -p "$BACKUP_DIR/buildroot.config" "${DEMO_OUT😕请先设置 DEMO_OUT}/.config"
cp -p "$BACKUP_DIR/buildroot-external-demo/package/debug-demo/S99debug-demo" \\
"$HOME/buildroot-external-demo/package/debug-demo/S99debug-demo"
cp -p "$BACKUP_DIR/inittab.generated" "$DEMO_OUT/target/etc/inittab"
)
echo $?
返回 0 后,在 Buildroot 目录先按第八节重建 debug-demo,检查成功,再执行完整 make。新 overlay 目录可以保留,但旧配置不应再引用它。
这里恢复目标 inittab 是同版本、同配置下的增量回退修复,不是日常直接修改 target/ 的配置管理方式;用于干净重建的配置仍来自原软件包和原 overlay。
构建后确认 target/etc/inittab 没有本篇的 debug_demo 条目,目标启动脚本与备份一致,再启动镜像,按上一篇检查 status、stop 和 start。
十四、小结
开机自启动解决的是“系统起来时谁启动应用”,respawn 解决的是“应用退出后谁重新启动它”。两者不能简单叠加,否则可能重复启动。
这一篇的关键是把管理权交给 BusyBox init:应用保持前台运行,旧后台启动入口停用,新的 inittab 通过 Buildroot overlay 保存。验证时不仅看进程是否存在,还要观察退出后是否出现新实例,以及新实例是否继续写日志。
完成这些实验后,再讨论日志限额、运行权限和故障诊断,才能逐步从“能自动启动”走向“更容易维护”。
参考资料
- BusyBox 1.37.0 init 实现及配置说明:respawn、启动动作和信号处理的版本核对入口。
- BusyBox 1.37.0 inittab 示例:BusyBox 的字段解释与动作类型。
- Buildroot 2025.02.8 默认 BusyBox inittab:系统初始化、登录与关机配置。
- Buildroot 2025.02.8 根文件系统定制说明:overlay 和后处理方式。



