欢迎光临
我们一直在努力

程序退出了怎么办?用 BusyBox init 让 ARM64 服务自动恢复

程序退出了怎么办?用 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 配置再次启动应用

最终检查四件事:

  • 开机后只有一个服务实例,由 PID 1 直接启动。
  • 使用 SIGKILL 终止实例后,不手动启动,也会出现新实例。
  • 使用 SIGTERM 让程序正常退出后,它同样会被重新启动。
  • 从启动配置中停用监督项并重启后,服务不再启动;恢复配置后可再次运行。
  • 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 尚未因此重新加载配置。接下来按顺序操作:

  • 在 QEMU 目标端的 # 提示符下执行:
  • 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 和后处理方式。
    赞(0)
    未经允许不得转载:171主机测评 » 程序退出了怎么办?用 BusyBox init 让 ARM64 服务自动恢复
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址