让 ARM64 程序支持配置文件:修改日志周期,无需重新编译
上一篇,我们把 debug_demo 交给 BusyBox init 管理:程序退出后,respawn 会重新启动它。但程序每隔两秒写一条日志,这个周期仍写死在 C 代码里。
如果只是想把两秒改成五秒,是否也要修改源码、交叉编译、重新生成镜像?
这一篇给程序增加一个配置文件。完成第一次代码升级后,只修改目标系统中的配置,再重启服务进程,就能改变日志周期,不需要重新编译。
一、这次只解决一个问题
本文承接《程序退出了怎么办?用 BusyBox init 让 ARM64 服务自动恢复》。需要已经完成前文的监督配置,并确认开机后只有一个服务实例。
本篇只增加一个设置:
interval_sec=2
它表示每次写完一条业务日志后,等待多少秒再写下一条。我们将完成以下实验:
这里的“无需重新编译”有一个前提:第一次给程序增加读取配置的能力,仍需要编译并重新生成镜像。后面的配置实验才不需要编译。
本篇不实现热加载。运行中的进程不会自动重新读取配置,也不要向它发送 SIGHUP 尝试重载。配置只在每个新服务进程启动时读取一次。
二、环境与操作位置
| Ubuntu 主机 | Ubuntu 20.04 x86_64 虚拟机 |
| Buildroot | 2025.02.8,~/buildroot-2025.02.8 |
| 输出目录 | ~/buildroot-output-arm64-demo |
| 外部目录 | ~/buildroot-external-demo,标识 ARM64_DEMO |
| 目标系统 | QEMU ARM64,BusyBox init |
| 服务入口 | /usr/bin/debug_demo –service |
| 配置文件 | /etc/debug-demo.conf |
| 日志文件 | /var/log/debug-demo.log |
全文区分两个终端环境:
- Ubuntu 主机:编辑源码、运行 Buildroot、启动 QEMU。
- QEMU 目标端:以 root 登录后修改 /etc 中的配置、查看日志、给服务发信号。
不要在 Windows PowerShell 执行本文的 Bash 命令,也不要在 Ubuntu 主机修改 /etc/debug-demo.conf 来代替目标端操作。
监督关系保持不变:
BusyBox init
-> /usr/bin/debug_demo –service
-> 启动时读取 /etc/debug-demo.conf
-> 按生效周期写日志
-> 原进程退出后,再启动一个新进程
/etc/inittab 中仍然只有一条本服务的监督配置:
::respawn:/usr/bin/debug_demo –service
不新增第二条启动路径,不手工运行额外的 –service 实例,也不恢复旧脚本的后台启动逻辑。
三、先确定配置规则和错误策略
为了把重点放在完整流程上,本篇不引入 INI、JSON 或 YAML 库,也不把这段代码当作通用配置解析器。文件只支持一个赋值项,使用 C 标准库读取和转换数值。
| interval_sec=2 | 接受,使用两秒 |
| interval_sec=5 | 接受,使用五秒 |
| 数值范围 | 只接受十进制数字组成的整数,范围 1~60 |
| 文件开头、末尾的空白 | 允许,包括末尾换行 |
| 等号两侧额外空格 | 不支持,例如 interval_sec = 5 无效 |
| 注释、其他键、重复赋值 | 不支持,整个文件视为无效 |
| 空文件、内嵌 NUL、超过 255 字节 | 无效 |
| 文件不存在 | 告警,使用默认两秒 |
| 格式错误、数值越界 | 告警,使用默认两秒 |
| 读取或关闭配置文件失败 | 告警,使用默认两秒 |
这里有意选择“告警并回退”,而不是“错误就退出”。因为当前服务由 respawn 管理,如果每次启动都因同一个错误配置退出,init 会反复启动它,但配置并不会自行修好。
这只是日志周期的实验策略,不是所有配置都应该静默降级。 涉及安全、设备动作或数据一致性的参数,可能需要拒绝启动,并配合明确的维护模式和重试限制。本篇的回退一定会写日志,不会把错误伪装成配置已成功加载。
四、关机并备份当前状态
如果上一轮实验的 QEMU 仍在运行,先在 QEMU 目标端执行:
poweroff
等待目标系统关机。如果 QEMU 仍未退出,再按 Ctrl+A,松开后按 X。
确认 QEMU 已经释放镜像后,再进行下面的主机构建。不要一边运行同一个可写镜像,一边重建或挂载它。
回到 Ubuntu 主机,执行完整代码块:
export DEMO_OUT="$HOME/buildroot-output-arm64-demo"
export CONFIG_BACKUP="$HOME/debug-demo-before-config-$(date +%Y%m%d-%H%M%S)"
(
set -eu
test -f "$DEMO_OUT/.config"
test -f "$HOME/buildroot-external-demo/src/debug-demo/debug_demo.c"
mkdir "$CONFIG_BACKUP"
cp -a "$HOME/buildroot-external-demo" "$CONFIG_BACKUP/"
cp -p "$DEMO_OUT/.config" "$CONFIG_BACKUP/buildroot.config"
printf 'Backup: %s\\n' "$CONFIG_BACKUP"
)
echo $?
返回值应为 0。保存输出的备份目录路径;失败就先检查,不要继续覆盖文件。子 shell 内的 set -eu 只影响这一块,不会关闭外层终端。
再核对现有的监督配置和兼容脚本:
grep -nFx '::respawn:/usr/bin/debug_demo –service' \\
"$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittab"
cat "$HOME/buildroot-external-demo/package/debug-demo/S99debug-demo"
grep -E '^BR2_(INIT_BUSYBOX|PACKAGE_DEBUG_DEMO)=' "$DEMO_OUT/.config"
grep '^BR2_ROOTFS_OVERLAY=' "$DEMO_OUT/.config"
监督行应只有一条;两个 Buildroot 开关应为 y,overlay 设置应保留上一篇的路径。脚本的 start 分支应只创建 /var/log,stop 分支不操作服务。如果脚本仍包含 start-stop-daemon,先完成上一篇的迁移,不要混用两种管理方式。
本次检查确认:外部 overlay 中只有一条 debug_demo 的 respawn 配置;兼容脚本的 start 分支仅创建日志目录,stop 分支直接返回;BR2_INIT_BUSYBOX 和 BR2_PACKAGE_DEBUG_DEMO 均为 y,原有及新增的 overlay 路径均已保留,可以继续修改程序。
五、替换完整 C 程序
在 Ubuntu 主机执行。下面是完整源码,不需要把多个片段自行拼接:
cd ~/buildroot-external-demo || exit 1
cat > src/debug-demo/debug_demo.c <<'EOF'
#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define CONFIG_PATH "/etc/debug-demo.conf"
#define DEFAULT_INTERVAL_SEC 2U
#define MAX_INTERVAL_SEC 60L
static volatile sig_atomic_t running = 1;
static void handle_signal(int signal_number)
{
(void)signal_number;
running = 0;
}
int compute_total(const int *values, int count)
{
int total = 0;
for (int index = 0; index < count; ++index) {
total += values[index];
}
return total;
}
static unsigned int load_interval(const char **status)
{
char buffer[256];
const char prefix[] = "interval_sec=";
FILE *config_file = fopen(CONFIG_PATH, "r");
if (config_file == NULL) {
*status = errno == ENOENT ? "missing-default" : "read-error-default";
return DEFAULT_INTERVAL_SEC;
}
size_t length = fread(buffer, 1, sizeof(buffer), config_file);
int read_failed = ferror(config_file);
int close_failed = fclose(config_file);
if (read_failed || close_failed != 0) {
*status = "read-error-default";
return DEFAULT_INTERVAL_SEC;
}
*status = "invalid-default";
if (length == sizeof(buffer) || memchr(buffer, '\\0', length) != NULL) {
return DEFAULT_INTERVAL_SEC;
}
buffer[length] = '\\0';
char *begin = buffer;
while (isspace((unsigned char)*begin)) {
++begin;
}
char *end = buffer + length;
while (end > begin && isspace((unsigned char)end[-1])) {
–end;
}
*end = '\\0';
if (strncmp(begin, prefix, sizeof(prefix) – 1) != 0) {
return DEFAULT_INTERVAL_SEC;
}
char *number = begin + sizeof(prefix) – 1;
if (*number == '\\0') {
return DEFAULT_INTERVAL_SEC;
}
for (const char *cursor = number; *cursor != '\\0'; ++cursor) {
if (*cursor < '0' || *cursor > '9') {
return DEFAULT_INTERVAL_SEC;
}
}
errno = 0;
char *number_end = NULL;
long parsed = strtol(number, &number_end, 10);
if (errno != 0 || *number_end != '\\0' ||
parsed < 1 || parsed > MAX_INTERVAL_SEC) {
return DEFAULT_INTERVAL_SEC;
}
*status = "loaded";
return (unsigned int)parsed;
}
int main(int argc, char *argv[])
{
const int values[] = {10, 20, 30, 40};
int result = compute_total(values, 4);
if (argc == 1) {
printf("debug-demo package v2: total = %d\\n", result);
return 0;
}
if (argc != 2 || strcmp(argv[1], "–service") != 0) {
fprintf(stderr, "Usage: %s [–service]\\n", argv[0]);
return 1;
}
struct sigaction action = {0};
action.sa_handler = handle_signal;
sigemptyset(&action.sa_mask);
if (sigaction(SIGTERM, &action, NULL) != 0 ||
sigaction(SIGINT, &action, NULL) != 0) {
perror("sigaction");
return 1;
}
FILE *log_file = fopen("/var/log/debug-demo.log", "a");
if (log_file == NULL) {
perror("open log");
return 1;
}
const char *config_status = NULL;
unsigned int interval_sec = load_interval(&config_status);
const char *level = strcmp(config_status, "loaded") == 0 ? "INFO" : "WARN";
if (fprintf(log_file,
"debug-demo config: pid=%ld level=%s path=%s status=%s interval_sec=%u\\n",
(long)getpid(), level, CONFIG_PATH, config_status, interval_sec) < 0 ||
fflush(log_file) != 0) {
fclose(log_file);
return 1;
}
unsigned long tick = 0;
while (running) {
struct timespec timestamp;
if (clock_gettime(CLOCK_MONOTONIC, ×tamp) != 0) {
perror("clock_gettime");
fclose(log_file);
return 1;
}
if (fprintf(log_file,
"debug-demo service: pid=%ld tick=%lu mono=%lld.%09ld interval_sec=%u total=%d\\n",
(long)getpid(), tick++, (long long)timestamp.tv_sec,
timestamp.tv_nsec, interval_sec, result) < 0 ||
fflush(log_file) != 0) {
fclose(log_file);
return 1;
}
for (unsigned int elapsed = 0; running && elapsed < interval_sec; ++elapsed) {
struct timespec remaining = {1, 0};
while (running && nanosleep(&remaining, &remaining) != 0) {
if (errno != EINTR) {
perror("nanosleep");
fclose(log_file);
return 1;
}
}
}
}
if (fprintf(log_file, "debug-demo service: pid=%ld stopped\\n",
(long)getpid()) < 0) {
fclose(log_file);
return 1;
}
return fclose(log_file) == 0 ? 0 : 1;
}
EOF
1. 无参数行为保持不变
无参数模式保持原有行为:输出 debug-demo package v2: total = 100 后退出;只有 –service 模式读取配置并写服务日志。这里仅说明代码逻辑,暂不执行;完成后续构建后,在第八节的 QEMU 目标端统一验证。
2. 不用 atoi 判断配置是否有效
atoi 不方便区分转换失败、零和溢出。这里先检查是否全部为数字,再用 strtol 检查转换错误和范围,拒绝 5abc、-1、0、61 等值。
长度判断也有明确边界:最多接受 255 字节;读满 256 字节就判为无效,不继续解析一个可能被截断的文件。重复赋值不会采用“最后一个覆盖前一个”,而是整体回退。
3. 日志里同时记录配置状态和时间
每个新进程先写一条配置日志。status=loaded 表示成功加载;带 default 的状态表示发生回退。仅看到 interval_sec=2 不足以判断是正常配置还是错误回退,还要看 status 和 level。
业务日志中的 mono 来自 CLOCK_MONOTONIC,用于比较同一次启动中的时间间隔,不是年月日时间,也不能拿不同系统启动之间的数值直接比较。
睡眠按一秒分段,并在中断后处理剩余等待时间,方便收到终止信号后退出。这不是精确定时任务:日志 I/O 和调度都会带来延迟,预期相邻时间差接近配置值,不要求严格等于整数秒。
信号处理函数仍然只修改退出标志。fflush 便于立即观察日志,但不代表掉电持久化保证。
六、让软件包安装默认配置
配置属于应用,所以这次由 debug-demo 包安装默认文件。上一篇的 overlay 继续负责 inittab,不要再在 overlay 中放第二份同名配置。
在 Ubuntu 主机执行:
cd ~/buildroot-external-demo || exit 1
cat > package/debug-demo/debug-demo.conf <<'EOF'
interval_sec=2
EOF
cat > package/debug-demo/debug-demo.mk <<'EOF'
DEBUG_DEMO_VERSION = 1.0
DEBUG_DEMO_SITE = $(BR2_EXTERNAL_ARM64_DEMO_PATH)/src/debug-demo
DEBUG_DEMO_SITE_METHOD = local
define DEBUG_DEMO_BUILD_CMDS
$(TARGET_CC) $(TARGET_CFLAGS) -Wall -Wextra -O0 -g -gdwarf-4 \\
-fno-omit-frame-pointer $(@D)/debug_demo.c \\
-o $(@D)/debug_demo $(TARGET_LDFLAGS)
endef
define DEBUG_DEMO_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/debug_demo \\
$(TARGET_DIR)/usr/bin/debug_demo
$(INSTALL) -D -m 0644 $(DEBUG_DEMO_PKGDIR)/debug-demo.conf \\
$(TARGET_DIR)/etc/debug-demo.conf
endef
define DEBUG_DEMO_INSTALL_INIT_SYSV
$(INSTALL) -D -m 0755 $(DEBUG_DEMO_PKGDIR)/S99debug-demo \\
$(TARGET_DIR)/etc/init.d/S99debug-demo
endef
$(eval $(generic-package))
EOF
define 内的命令保持 Tab 缩进。默认配置安装为 0644,不是可执行脚本;DEBUG_DEMO_PKGDIR 指向包规则目录,$(@D) 指向已同步的应用构建目录。
这里没有重写 S99debug-demo。 它必须保留上一篇的兼容版本:只准备日志目录,不负责启动和停止应用。
本篇继续沿用 -O0 -g -gdwarf-4 等调试参数。如果出现 _FORTIFY_SOURCE requires compiling with optimization (-O),含义仍是该加固在当前优化级别下未生效,不应仅凭这条告警判断构建失败,也不应把调试参数当作生产推荐。
本次没有改变 Buildroot 的配置选择,不必为此重新保存一个 defconfig。仍可沿用 arm64_demo_respawn_defconfig,但要一并保存更新后的外部源码、包规则和默认配置。defconfig 不包含这些文件的内容。
七、首次升级必须重建程序和镜像
确认没有 QEMU 使用该镜像。在 Ubuntu 主机执行:
export DEMO_OUT="$HOME/buildroot-output-arm64-demo"
(
set -eu
cd "$HOME/buildroot-2025.02.8"
make O="${DEMO_OUT😕请先设置 DEMO_OUT}" \\
BR2_EXTERNAL="$HOME/buildroot-external-demo" debug-demo-rebuild V=1
make O="${DEMO_OUT😕请先设置 DEMO_OUT}" \\
BR2_EXTERNAL="$HOME/buildroot-external-demo" -j2
)
echo $?
应返回 0,否则先处理构建错误。第一条 make 重新同步本地源码并编译、安装软件包;第二条生成最终文件系统镜像。只完成第一条不能说明启动镜像已更新,也不能只改源码就期待普通 make 自动识别所有本地包变化。
这里沿用一个 BR2_EXTERNAL 目录;配置了多个外部目录的读者必须保留完整列表。不要重新加载官方 defconfig,也不需要为了这次修改执行 make clean。
构建成功后,在 Ubuntu 主机检查:
cat "$DEMO_OUT/target/etc/debug-demo.conf"
cmp "$HOME/buildroot-external-demo/package/debug-demo/debug-demo.conf" \\
"$DEMO_OUT/target/etc/debug-demo.conf"
echo $?
grep -nFx '::respawn:/usr/bin/debug_demo –service' "$DEMO_OUT/target/etc/inittab"
cat "$DEMO_OUT/target/etc/init.d/S99debug-demo"
file "$DEMO_OUT/target/usr/bin/debug_demo"
ls -l "$DEMO_OUT/images/rootfs.ext2"
默认配置应是 interval_sec=2,cmp 返回 0;程序应识别为 AArch64 ELF。若配置不同,检查是否有其他 overlay 或 post-build 脚本覆盖了同一路径,不要忽略差异继续实验。
本次主机端检查还确认:目标目录中的兼容脚本仍只负责准备日志目录;debug_demo 被识别为 AArch64 64 位动态链接 ELF;rootfs.ext2 镜像已生成,大小为 60 MiB。这些结果确认了部分构建产物,不代表配置读取和服务运行已在 QEMU 中验证。
这些是主机端检查。接下来还要从实际启动的镜像中确认文件和程序行为,避免把更新后的 target/ 误当作已经更新的运行系统。
八、启动 QEMU 并确认默认值
在 Ubuntu 主机启动一个 QEMU 实例:
export DEMO_OUT="$HOME/buildroot-output-arm64-demo"
/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 \\
-device virtio-net-device,netdev=net0
本篇不连接 GDB,所以省略了早期调试文章中的端口转发;不影响串口中的配置实验。
出现登录提示后输入 root。从下面开始,命令全部在 QEMU 目标端执行,直到明确提示返回 Ubuntu 主机。
cat /etc/debug-demo.conf
grep -nFx '::respawn:/usr/bin/debug_demo –service' /etc/inittab
pidof debug_demo
ps | grep '[d]ebug_demo'
/usr/bin/debug_demo
echo $?
tail -n 8 /var/log/debug-demo.log
本次 QEMU 目标端实测如下:
# cat /etc/debug-demo.conf
interval_sec=2
# grep -nFx '::respawn:/usr/bin/debug_demo –service' /etc/inittab
42:::respawn:/usr/bin/debug_demo –service
# pidof debug_demo
120
# ps | grep '[d]ebug_demo'
120 root /usr/bin/debug_demo –service
# /usr/bin/debug_demo
debug-demo package v2: total = 100
# echo $?
0
# tail -n 8 /var/log/debug-demo.log
debug-demo service: pid=120 tick=5 mono=18.015093552 interval_sec=2 total=100
debug-demo service: pid=120 tick=6 mono=20.017694336 interval_sec=2 total=100
debug-demo service: pid=120 tick=7 mono=22.021376736 interval_sec=2 total=100
debug-demo service: pid=120 tick=8 mono=24.024203552 interval_sec=2 total=100
debug-demo service: pid=120 tick=9 mono=26.027881920 interval_sec=2 total=100
debug-demo service: pid=120 tick=10 mono=28.030749072 interval_sec=2 total=100
debug-demo service: pid=120 tick=11 mono=30.033222320 interval_sec=2 total=100
debug-demo service: pid=120 tick=12 mono=32.036797632 interval_sec=2 total=100
结果显示配置文件为两秒,inittab 有服务监督项,且只有 PID 120 的一个服务实例。无参数调用打印总和并返回 0,没有另起服务;日志中同一 PID 的周期约为两秒,与配置一致。最初的 tail 没有显示进程启动时的配置状态行,所以下面再按当前 PID 筛选该记录。
登录较慢时,启动记录可能已不在最后八行。可以按当前进程筛选配置记录:
current_pid=$(pidof debug_demo)
grep -F "debug-demo config: pid=$current_pid " /var/log/debug-demo.log
sleep 6
tail -n 5 /var/log/debug-demo.log
本次筛选与等待后的实际输出如下:
# current_pid=$(pidof debug_demo)
# grep -F "debug-demo config: pid=$current_pid " /var/log/debug-demo.log
debug-demo config: pid=120 level=INFO path=/etc/debug-demo.conf status=loaded interval_sec=2
# sleep 6
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=120 tick=51 mono=110.993277696 interval_sec=2 total=100
debug-demo service: pid=120 tick=52 mono=113.024472640 interval_sec=2 total=100
debug-demo service: pid=120 tick=53 mono=115.043104160 interval_sec=2 total=100
debug-demo service: pid=120 tick=54 mono=117.046812976 interval_sec=2 total=100
debug-demo service: pid=120 tick=55 mono=119.052642512 interval_sec=2 total=100
由此确认 PID 120 成功加载了配置(status=loaded),并采用两秒周期;五条业务日志的单调时钟间隔约为两秒。若其他环境中没有配置记录或日志缺少 mono 字段,应先检查是否运行了新版程序和镜像。
九、准备一个受控的服务重启命令
旧的 /etc/init.d/S99debug-demo restart 已不适用于当前方案。这里采用上一篇已验证的机制:给当前实例发送 SIGTERM,它退出后,由 init 启动新实例;新实例再读取配置。
在 QEMU 目标端定义下面的函数,完整执行一次即可:
restart_demo() {
old_pid=$(pidof debug_demo) || return 1
case "$old_pid" in
''|*[!0-9]*)
echo 'Expected exactly one debug_demo instance' >&2
return 1
;;
esac
[ "$old_pid" -gt 1 ] || return 1
[ "$(readlink "/proc/$old_pid/exe")" = /usr/bin/debug_demo ] || return 1
kill -TERM "$old_pid" || return 1
attempt=0
while [ "$attempt" -lt 10 ]; do
sleep 1
new_pid=$(pidof debug_demo) || new_pid=
case "$new_pid" in
'') ;;
*[!0-9]*)
echo 'Expected exactly one debug_demo instance' >&2
return 1
;;
*)
if [ "$new_pid" -gt 1 ] && [ "$new_pid" != "$old_pid" ] &&
[ "$(readlink "/proc/$new_pid/exe")" = /usr/bin/debug_demo ]; then
if grep -F "debug-demo config: pid=$new_pid " \\
/var/log/debug-demo.log; then
printf 'Restarted: old_pid=%s new_pid=%s\\n' "$old_pid" "$new_pid"
return 0
fi
fi
;;
esac
attempt=$((attempt + 1))
done
echo 'No new configured instance observed within 10 seconds' >&2
return 1
}
这个函数不直接启动程序,只发送一次信号,然后有限等待新进程和配置日志出现。函数返回 0 不表示配置有效:回退到默认值同样可以正常启动,仍须核对输出中的配置状态。
它仅适用于单人、顺序进行的实验,不提供并发控制,不能彻底消除 PID 复用竞争,也不是通用服务管理器。如果超时或发现多个 PID,停止当前实验并检查进程和日志,不要反复调用它或直接 killall。
函数只保存在当前 shell 中。重新登录或重启系统后,需要重新定义;不要把它写进 inittab。
十、改成五秒,不编译、不重建镜像
先在 QEMU 目标端写入配置,再替换正式文件:
(
set -eu
umask 022
printf 'interval_sec=5\\n' > /etc/debug-demo.conf.new
chmod 0644 /etc/debug-demo.conf.new
mv -f /etc/debug-demo.conf.new /etc/debug-demo.conf
)
echo $?
cat /etc/debug-demo.conf
写入应返回 0。同目录临时文件再重命名,可避免新启动的服务读到写了一半的配置;这是单人实验约定,不是可抵御恶意并发写入的部署工具,也不保证掉电持久化。
先不要重启,观察原进程:
pidof debug_demo
sleep 6
tail -n 5 /var/log/debug-demo.log
本次修改前的实际观察结果如下:
# pidof debug_demo
120
# sleep 6
# tail -n 5 /var/log/debug-demo.log
debug-demo service: pid=120 tick=130 mono=270.227472704 interval_sec=2 total=100
debug-demo service: pid=120 tick=131 mono=272.243346032 interval_sec=2 total=100
debug-demo service: pid=120 tick=132 mono=274.246367072 interval_sec=2 total=100
debug-demo service: pid=120 tick=133 mono=276.257045952 interval_sec=2 total=100
PID 120 保持运行,日志仍按约两秒的周期写入。这符合程序只在启动时读取配置的设计;修改配置本身不会改变当前进程已加载的周期。
现在重启服务进程:
restart_demo
echo $?
sleep 11
pidof debug_demo
ps | grep '[d]ebug_demo'
tail -n 8 /var/log/debug-demo.log
本次目标端的实际输出如下:
# restart_demo
debug-demo config: pid=143 level=INFO path=/etc/debug-demo.conf status=loaded interval_sec=5
Restarted: old_pid=120 new_pid=143
# echo $?
0
# sleep 11
# pidof debug_demo
143
# ps | grep '[d]ebug_demo'
143 root /usr/bin/debug_demo –service
# tail -n 8 /var/log/debug-demo.log
debug-demo service: pid=120 tick=150 mono=310.681282560 interval_sec=2 total=100
debug-demo service: pid=120 tick=151 mono=312.734786784 interval_sec=2 total=100
debug-demo service: pid=120 tick=152 mono=314.796729504 interval_sec=2 total=100
debug-demo service: pid=120 stopped
debug-demo config: pid=143 level=INFO path=/etc/debug-demo.conf status=loaded interval_sec=5
debug-demo service: pid=143 tick=0 mono=317.835807968 interval_sec=5 total=100
debug-demo service: pid=143 tick=1 mono=322.861145872 interval_sec=5 total=100
debug-demo service: pid=143 tick=2 mono=327.881431440 interval_sec=5 total=100
结果确认 restart_demo 返回 0,PID 从 120 变为 143,且只看到一个新服务实例。旧进程记录了 stopped;新进程报告 status=loaded interval_sec=5,后续日志的单调时钟间隔约为五秒。该修改只涉及目标端配置和服务重启,没有重新编译或重建镜像。
这一节没有运行编译器,没有调用 Buildroot,也没有重新生成镜像。改变的是目标端的配置,以及读取它的新服务进程。
十一、验证错误配置,不把回退当成成功加载
下面继续在 QEMU 目标端顺序执行。先确认当前 shell 已定义第九节的 restart_demo 函数;如果重新登录、重启或换了终端,需要重新执行函数定义。每一项都要先确认配置操作成功,再调用 restart_demo;函数失败时停止检查,不要直接进行下一项。
1. 配置文件不存在
先安全地移走有效配置。首次运行时会把五秒配置保存到 .saved;如果上次已完成移动,则核对备份内容后沿用,不会覆盖它:
(
set -eu
config_file=/etc/debug-demo.conf
backup_file=/etc/debug-demo.conf.saved
if [ -f "$config_file" ] && [ ! -e "$backup_file" ]; then
mv "$config_file" "$backup_file"
elif [ ! -e "$config_file" ] && [ -f "$backup_file" ] &&
grep -qx 'interval_sec=5' "$backup_file"; then
printf 'Config is already absent; verified backup: %s\\n' "$backup_file"
else
echo 'Unexpected config/backup state; inspect both files before continuing.' >&2
exit 1
fi
)
echo $?
只有返回 0 才继续。若配置文件和备份文件同时存在,或备份内容不是 interval_sec=5,代码会停止,避免覆盖文件;先检查实际状态并人工处理,不要直接删除备份。
然后执行:
restart_demo
echo $?
sleep 5
tail -n 6 /var/log/debug-demo.log
本次 QEMU 目标端的实测结果如下:
# restart_demo
debug-demo config: pid=165 level=WARN path=/etc/debug-demo.conf status=missing-default interval_sec=2
Restarted: old_pid=143 new_pid=165
# echo $?
0
# sleep 5
# tail -n 6 /var/log/debug-demo.log
debug-demo service: pid=143 stopped
debug-demo config: pid=165 level=WARN path=/etc/debug-demo.conf status=missing-default interval_sec=2
debug-demo service: pid=165 tick=0 mono=861.590173248 interval_sec=2 total=100
debug-demo service: pid=165 tick=1 mono=863.599384384 interval_sec=2 total=100
debug-demo service: pid=165 tick=2 mono=865.602162592 interval_sec=2 total=100
结果确认旧进程 PID 143 正常停止,新进程 PID 165 因配置文件缺失而记录 status=missing-default,回退到默认两秒周期并继续运行。五秒配置仍保存在 .saved 文件中;程序不会自行重建配置文件。
2. 格式错误
写入一个不能转换为整数的值:
(
set -eu
printf 'interval_sec=abc\\n' > /etc/debug-demo.conf.new
chmod 0644 /etc/debug-demo.conf.new
mv -f /etc/debug-demo.conf.new /etc/debug-demo.conf
)
echo $?
确认返回 0 后:
restart_demo
echo $?
sleep 5
tail -n 6 /var/log/debug-demo.log
本次 QEMU 目标端的实测结果如下:
# restart_demo
debug-demo config: pid=176 level=WARN path=/etc/debug-demo.conf status=invalid-default interval_sec=2
Restarted: old_pid=165 new_pid=176
# echo $?
0
# sleep 5
# tail -n 6 /var/log/debug-demo.log
debug-demo service: pid=165 stopped
debug-demo config: pid=176 level=WARN path=/etc/debug-demo.conf status=invalid-default interval_sec=2
debug-demo service: pid=176 tick=0 mono=1148.671731744 interval_sec=2 total=100
debug-demo service: pid=176 tick=1 mono=1150.678101840 interval_sec=2 total=100
debug-demo service: pid=176 tick=2 mono=1152.687769152 interval_sec=2 total=100
debug-demo service: pid=176 tick=3 mono=1154.690495232 interval_sec=2 total=100
新进程 PID 176 将无效配置标记为 invalid-default,回退到默认两秒后继续运行;旧进程 PID 165 正常记录 stopped。程序不会改写错误配置,仍需由使用者修正。
3. 数字有效,但超过允许范围
接着测试 61:
(
set -eu
printf 'interval_sec=61\\n' > /etc/debug-demo.conf.new
chmod 0644 /etc/debug-demo.conf.new
mv -f /etc/debug-demo.conf.new /etc/debug-demo.conf
)
echo $?
确认返回 0 后:
restart_demo
echo $?
sleep 5
tail -n 6 /var/log/debug-demo.log
本次测试 interval_sec=61 的实测结果如下:
# restart_demo
debug-demo config: pid=187 level=WARN path=/etc/debug-demo.conf status=invalid-default interval_sec=2
Restarted: old_pid=176 new_pid=187
# echo $?
0
# sleep 5
# tail -n 6 /var/log/debug-demo.log
debug-demo service: pid=176 stopped
debug-demo config: pid=187 level=WARN path=/etc/debug-demo.conf status=invalid-default interval_sec=2
debug-demo service: pid=187 tick=0 mono=1218.720501904 interval_sec=2 total=100
debug-demo service: pid=187 tick=1 mono=1220.726634112 interval_sec=2 total=100
debug-demo service: pid=187 tick=2 mono=1222.730689536 interval_sec=2 total=100
debug-demo service: pid=187 tick=3 mono=1224.733233104 interval_sec=2 total=100
新进程 PID 187 将超出 1~60 范围的数值标记为 invalid-default,回退到两秒并继续运行;旧进程 PID 176 正常停止。这验证了程序不仅检查数字格式,也执行范围校验。
其他输入的预期策略如下,属于扩展验证项,不表示已经完成实验:
| interval_sec=1、interval_sec=60 | loaded,采用对应数值 |
| interval_sec=0、interval_sec=-1 | invalid-default,两秒 |
| interval_sec=5abc、interval_sec=5.0 | invalid-default,两秒 |
| 极长且超出 long 范围的数字 | invalid-default,两秒 |
| interval_sec = 5、未知键、注释 | invalid-default,两秒 |
| 空文件、两行重复赋值、256 字节文件 | invalid-default,两秒 |
| 配置读取失败 | read-error-default,两秒 |
目标进程以 root 运行,不能简单用 chmod 000 来断言必定发生权限读取失败。本篇不以这种方式构造“已验证”的读失败案例。
4. 检查没有因坏配置反复退出
在最后一个错误配置仍然存在时执行:
pidof debug_demo
sleep 6
pidof debug_demo
ps | grep '[d]ebug_demo'
tail -n 6 /var/log/debug-demo.log
本次在无效配置仍存在时观察到:
# pidof debug_demo
187
# sleep 6
# pidof debug_demo
187
# ps | grep '[d]ebug_demo'
187 root /usr/bin/debug_demo –service
# tail -n 6 /var/log/debug-demo.log
debug-demo service: pid=187 tick=36 mono=1291.102217184 interval_sec=2 total=100
debug-demo service: pid=187 tick=37 mono=1293.113423360 interval_sec=2 total=100
debug-demo service: pid=187 tick=38 mono=1295.120605056 interval_sec=2 total=100
debug-demo service: pid=187 tick=39 mono=1297.136505936 interval_sec=2 total=100
debug-demo service: pid=187 tick=40 mono=1299.144985424 interval_sec=2 total=100
debug-demo service: pid=187 tick=41 mono=1301.148042560 interval_sec=2 total=100
等待前后 PID 都是 187,进程只有一个,tick 持续增加且周期约两秒。这说明该无效配置被记录并回退后,服务仍持续运行,没有因配置错误退出并触发反复重启。每次主动调用 restart_demo 都会产生新 PID,这是实验操作,不应与重启风暴混淆。
如果 PID 持续变化,检查是否有日志打开失败、写入失败或其他退出原因。本篇只避免“配置错误导致退出”这一种循环,不保证其他启动错误不会被 respawn 反复重试。
十二、恢复有效配置并收尾
先恢复第十一节保存的五秒配置。在 QEMU 目标端执行:
(
set -eu
test -f /etc/debug-demo.conf.saved
mv -f /etc/debug-demo.conf.saved /etc/debug-demo.conf
)
echo $?
cat /etc/debug-demo.conf
确认返回 0,且文件为 interval_sec=5,再执行:
restart_demo
echo $?
sleep 11
pidof debug_demo
ps | grep '[d]ebug_demo'
tail -n 6 /var/log/debug-demo.log
sync
预期重新出现 status=loaded interval_sec=5,只有一个服务实例,相邻新日志约相隔五秒。以上恢复实验同样不需要编译。
如果还想验证重启后的读取行为,可在 QEMU 目标端执行:
reboot
若 QEMU 继续运行,就等待新的登录提示后以 root 登录;只有 QEMU 确实退出时,才回到 Ubuntu 主机重新执行第八节启动命令。不要另开一个使用相同镜像的实例。
重新登录后检查:
cat /etc/debug-demo.conf
pidof debug_demo
ps | grep '[d]ebug_demo'
current_pid=$(pidof debug_demo)
grep -F "debug-demo config: pid=$current_pid " /var/log/debug-demo.log
本篇使用可写的 rootfs.ext2、未启用 QEMU snapshot,且期间没有重建镜像,因此预期配置仍为五秒,服务启动时重新加载。日志是否跨重启保留取决于 /var/log 的挂载方式,不要把旧日志保留作为本实验成立的前提。
结束实验时,在 QEMU 目标端执行 poweroff,等待关机;如果 QEMU 未退出,再按 Ctrl+A、X。
十三、目标端配置与构建默认值不是同一份文件
本篇涉及三处位置,需要区分各自职责:
| 外部目录 package/debug-demo/debug-demo.conf | 主机上维护的镜像默认配置 |
| $DEMO_OUT/target/etc/debug-demo.conf | Buildroot 构建过程中的目标文件 |
| QEMU 内 /etc/debug-demo.conf | 当前运行系统实际读取的配置 |
在 QEMU 中改为五秒,不会反向更新主机上的包文件。后续重新生成根文件系统镜像,不会自动合并运行中设备的配置修改。 按本篇规则重新安装包并构建镜像,会再次带入包中的两秒默认值;其他 overlay 或后处理脚本如有覆盖,还需另外排查。
如果希望以后构建出的镜像默认就是五秒,先正常关闭 QEMU,再在 Ubuntu 主机执行:
printf 'interval_sec=5\\n' > \\
"$HOME/buildroot-external-demo/package/debug-demo/debug-demo.conf"
随后按第七节执行软件包重建和完整构建,再启动并验证。这里复用重建包的完整流程,虽然只是修改默认配置,也会重新执行编译;它与前面的“目标端改配置无需编译”是两个不同场景。
不要把手工修改 output/target 当作长期保存配置的方法。需要随构建复现的内容应保存在外部目录及相应构建规则中。
真实设备如果需要刷写系统后仍保留用户配置,通常需要独立数据分区、默认值与用户配置分离、版本迁移和升级恢复策略。这些不属于本篇已经实现的功能。
十四、常见问题与本篇边界
| 文件改成五秒,日志仍是两秒 | 是否尚未重启进程;是否有回退告警 |
| 日志没有 config、mono 字段 | 是否运行旧程序或旧镜像,是否完成两步构建 |
| 新进程报告 missing-default | 配置是否位于 QEMU 内的 /etc/debug-demo.conf |
| 新进程报告 invalid-default | 等号、数值范围、空文件、注释或重复项 |
| 运行脚本 restart 报错 | 旧管理接口已停用,使用本文的受控 TERM 实验 |
| restart_demo 找不到新实例 | 监督项是否存在,程序是否启动失败,日志是否可写 |
| 配置写入提示只读文件系统 | 核对镜像及挂载状态,不要盲目修改主机的 /etc |
| 重建后又回到两秒 | 主机中的默认配置仍是两秒,运行时改动没有自动合并 |
仍需记住这些限制:
- 程序以 root 运行,配置是普通文本,不适合存放秘密信息。
- 配置格式很小,不支持多参数、注释、热加载或配置版本迁移。
- init 只处理进程退出,不判断业务健康,也没有本篇自定义的限速重试策略。
- 日志没有轮转和容量上限,只适合短时实验;fflush 不等于持久化。
- mono 记录的是目标系统的单调时钟;QEMU 暂停、主机负载等因素不适合用于证明实时性能。
- 原子替换减少半写文件被读取的机会,但不等于具备完整的掉电恢复或并发部署能力。
如果要退回上一篇的程序,先正常关闭 QEMU,再从第四节备份中恢复 src/debug-demo/debug_demo.c 和 package/debug-demo/debug-demo.mk,按第七节重建程序及镜像。旧程序不读取 /etc/debug-demo.conf,因此残留配置不影响旧逻辑;增量构建不会仅因删除安装规则就自动清掉已安装文件。如需完全移除,应在确认路径后清理外部目录和构建目标中仅由本篇新增的配置文件,再生成镜像。回退步骤也需要另行验证。
十五、总结
从这一篇开始,日志周期不再只能写在源码中:
程序代码:定义默认值、解析规则和错误策略
配置文件:保存这次运行希望采用的参数
init:在旧进程退出后启动新实例
日志:说明实际采用了什么值,以及是否发生回退
关键不只是“成功读到了一个数字”,还包括:错误输入是否被拒绝、回退是否可见、进程何时重新读取配置,以及重建系统时哪些修改会丢失。
第一次升级需要构建;之后把日志周期从两秒改为五秒,只需要修改目标端配置并重启服务进程。完成实测后,应以实际 PID、配置状态和时间差替换本文的示意输出,再对实验结论作最终确认。
下一篇预告
下一篇:《日志达到阈值后轮转,确认程序继续写入正确文件》。我们将继续使用 debug_demo,在日志达到设定阈值后执行轮转,并检查轮转前后的文件内容与服务进程,确认后续日志确实写入预期的活动日志文件。



