让 Buildroot 服务先完成 TCP 收发,再处理消息边界、断线与重连
上一篇为 debug-demo 服务创建了专用账户,并在 QEMU 中验证了降权后的服务仍能写日志、同时无法读取 root-only 测试文件。这篇沿用同一套 Ubuntu、Buildroot 和 QEMU 环境,让这个前台服务再增加一项能力:连接 Ubuntu 主机上的测试服务,周期性发送一行请求并接收一行响应。
本文先让最小收发链路跑通,再说明为什么一次 send() 不等于对端一次 recv(),然后加入换行分帧、断线检测和带退避的重连。网络请求仍由 debug-demo 非特权账户发起;BusyBox init 继续负责监督进程。
一、这次要完成什么
本篇不实现一个通用网络协议栈,只建立一个容易观察的 TCP 客户端实验:
Ubuntu 主机上的 Python 测试服务
^ TCP:一行请求 / 一行响应
|
QEMU ARM64 中的 debug-demo 客户端
|
BusyBox init 以 respawn 方式监督
本次实际验证三条主线:
换行分帧、短写处理和 1、2、4、8 秒退避会结合代码说明;普通收发日志不等于这些机制的全部边界情况都已经过测试。具体已验证和未验证范围见文末。
本例的消息格式是教学用的 ASCII 文本行,例如 tick=3\\n。它不是生产环境协议,没有认证、加密、消息去重或业务级确认机制。
二、环境与操作约定
| 开发主机 | 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 |
| TCP 服务端 | Ubuntu 主机,TCP 5000 端口 |
| QEMU user-mode 主机地址 | 10.0.2.2 |
默认命令在 Ubuntu 主机执行;标注 QEMU 目标端的命令才在 ARM64 系统内执行。不要在 Windows PowerShell 执行本文的 Bash 命令。
本文依赖前文已经配置的服务账户、日志目录权限、启动准备脚本和 init 服务项,不是从空白 Buildroot 环境开始的独立教程。
代码与实测版本说明: 本次审校补充了 C 客户端的日志失败处理和 Python 服务的连接异常处理。第八、九节只记录本轮修订版实验;本轮服务先由 PID 121 运行,随后由 init 拉起 PID 133。构建退出码和日志异常分支状态见文末。
QEMU 的 -netdev user 为虚拟机提供 user-mode 网络。该配置下,guest 通常通过 10.0.2.2 访问宿主机。若你的 QEMU 网络配置不同,应按实际网关调整程序中的 SERVER_HOST。主机测试服务会绑定 0.0.0.0 以接受来自 QEMU 的连接;实验时只在可信环境中短时运行,并检查主机防火墙,不要把调试服务长期暴露到不可信网络。
三、先确认服务仍由 init 管理
如果 QEMU 仍在运行,先在 QEMU 目标端执行:
poweroff
确认 QEMU 进程退出后,再在 Ubuntu 主机检查现有配置和源码:
export DEMO_OUT="$HOME/buildroot-output-arm64-demo"
test -f "$DEMO_OUT/.config"
printf 'config check exit code: %s\\n' "$?"
test -f "$HOME/buildroot-external-demo/src/debug-demo/debug_demo.c"
printf 'source check exit code: %s\\n' "$?"
grep -nFx '::respawn:/usr/bin/debug_demo –service' \\
"$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittab"
grep -E '^BR2_(INIT_BUSYBOX|PACKAGE_DEBUG_DEMO)=' "$DEMO_OUT/.config"
两个文件检查的退出码都应为 0;非零时先检查路径,不要继续覆盖源码。还应确认 debug-demo 已启用,且 overlay 中仍只有预期的 respawn 服务项。本文不增加第二条启动路径,不手工在 shell 中启动另一个 –service 实例。
此前 Ubuntu 主机配置检查的实际输出节选如下(已移除提示符中的用户名和主机名,命令结果未改动):
$ grep -nFx '::respawn:/usr/bin/debug_demo –service' \\
> "$HOME/buildroot-external-demo/board/qemu-arm64-demo/rootfs-overlay/etc/inittab"
42:::respawn:/usr/bin/debug_demo –service
$ grep -E '^BR2_(INIT_BUSYBOX|PACKAGE_DEBUG_DEMO)=' "$DEMO_OUT/.config"
BR2_INIT_BUSYBOX=y
BR2_PACKAGE_DEBUG_DEMO=y
overlay 中的服务项位于第 42 行,BusyBox init 和 debug-demo 包均已启用。此前两个 test -f 的退出码未记录,不能仅凭没有输出判断成功;上面的操作命令已补充即时显示退出码。
在修改源码前做一份备份:
export TCP_BACKUP="$HOME/debug-demo-before-tcp-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$TCP_BACKUP"
cp -p "$HOME/buildroot-external-demo/src/debug-demo/debug_demo.c" \\
"$TCP_BACKUP/debug_demo.c"
cp -p "$DEMO_OUT/.config" "$TCP_BACKUP/buildroot.config"
printf 'Backup: %s\\n' "$TCP_BACKUP"
四、准备 Ubuntu 主机测试服务
测试服务只使用 Python 3 标准库,不需要安装第三方包。在 Ubuntu 主机终端 A 执行下面命令。这个终端专门运行测试服务,后续编译和启动 QEMU 使用另一个终端:
python3 – <<'PY'
import socket
HOST = "0.0.0.0"
PORT = 5000
MAX_LINE = 256
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen(4)
print(f"test server listening on {HOST}:{PORT}", flush=True)
while True:
connection, address = server.accept()
print(f"accepted {address}", flush=True)
try:
with connection:
pending = bytearray()
while True:
chunk = connection.recv(128)
if not chunk:
print(f"peer closed {address}", flush=True)
break
pending.extend(chunk)
if len(pending) > MAX_LINE and b"\\n" not in pending:
print("frame too long; closing connection", flush=True)
break
too_long = False
while b"\\n" in pending:
line, _, remainder = pending.partition(b"\\n")
pending = bytearray(remainder)
if len(line) > MAX_LINE:
print("frame too long; closing connection", flush=True)
too_long = True
break
response = b"ACK " + line + b"\\n"
connection.sendall(response)
print(f"received={line!r} sent={response!r}", flush=True)
if too_long:
break
except OSError as error:
print(f"connection error {address}: {error}; accepting next peer", flush=True)
PY
输入最后一行 PY 并回车后,本次实际看到的启动输出是:
test server listening on 0.0.0.0:5000
此时终端不再显示 shell 命令提示符、也暂时没有新输出,都是正常现象,不是程序卡死。 Python 服务正在前台运行,等待客户端连接;建立连接后,它还会等待客户端发送数据。不要在这个终端继续输入后续的 Bash 命令,也不要为了取回提示符而按 Ctrl+C,否则会停止测试服务。
保持终端 A 开着,另开一个 Ubuntu 主机终端 B,继续第六节的源码更新、第七节的构建和第八节的 QEMU 启动。新终端不自动继承终端 A 中设置的变量;第七、八节会重新设置所需的 DEMO_OUT。
上面的输出只证明主机服务已开始监听,不代表 QEMU 客户端已经连接或 TCP 收发已经验证成功。等新客户端启动并连接后,终端 A 才会出现 accepted …;收到完整请求并发送响应后,才会出现 received=… sent=…。本轮主机端和目标端收发实测见第九节。
服务按 \\n 分割收到的字节流,再为每一行返回 ACK 前缀。它把 recv() 收到的任意片段先放进缓冲区,所以请求即使分成多次到达,也会等到完整行再处理。sendall() 则负责把响应字节全部交给 socket 层发送。
连接处理中的 OSError(如连接重置、发送断管)会被记录,当前连接关闭后继续接受新连接;Ctrl+C 引发的 KeyboardInterrupt 不会被这一处理捕获,仍可停止整个服务。监听端口绑定等启动错误仍直接报错退出。该服务一次只处理一个连接,没有并发和连接空闲超时,空闲客户端可能阻塞其他客户端,因此仅用于本实验。
如果提示端口 5000 已被占用,先检查是否已在另一个终端运行了本节服务;若已运行,直接使用它,不要再启动第二份。若端口被其他需要保留的程序占用,再把本文主机端和客户端中的端口一并改成另一个未使用端口。仅在第九节主动测试断线或实验结束时,回到终端 A 按 Ctrl+C 停止服务。
五、先理解最小收发,再看边界
TCP 提供的是有序字节流,不提供应用层消息边界。下面这种代码只适合说明 socket 调用关系,不是最终实现:
send(socket_fd, request, request_length, 0);
received = recv(socket_fd, response, sizeof(response), 0);
send() 可能只发送一部分数据;recv() 可能只返回当前已到达的一部分,也可能一次拿到多条应用消息。返回值是字节数,不是“消息数”。因此代码必须检查返回值,并由应用协议定义如何知道一条消息在哪里结束。
本篇使用最简单的文本行协议:请求和响应都以换行符 \\n 结尾。稍后的 send_all() 循环处理短写,receive_line() 持续读取到换行符才返回一条完整响应。教学实现逐字节调用 recv(),便于看清边界;低延迟或高吞吐产品应使用缓冲区解析器,避免每个字节都触发一次系统调用。
六、更新服务客户端
客户端使用 QEMU user-mode 网络中的主机地址 10.0.2.2 和端口 5000。程序仍然先查询服务账户、清空附加组并切换 UID/GID,再打开 /var/log/debug-demo/debug-demo.log。网络错误和重连事件写入同一日志。
在 Ubuntu 主机终端 B 执行下面的完整命令,替换 src/debug-demo/debug_demo.c;终端 A 中的 Python 服务继续运行:
cd ~/buildroot-external-demo || exit 1
cat > src/debug-demo/debug_demo.c <<'EOF'
#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <grp.h>
#include <netdb.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#define CONFIG_PATH "/etc/debug-demo.conf"
#define LOG_PATH "/var/log/debug-demo/debug-demo.log"
#define SERVICE_USER "debug-demo"
#define SERVER_HOST "10.0.2.2"
#define SERVER_PORT "5000"
#define DEFAULT_INTERVAL_SEC 2U
#define MAX_INTERVAL_SEC 60L
#define MAX_LINE_BYTES 256U
#define MAX_RETRY_SECONDS 8U
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 (*begin == ' ' || *begin == '\\t' || *begin == '\\r' || *begin == '\\n') {
++begin;
}
char *end = buffer + length;
while (end > begin &&
(end[-1] == ' ' || end[-1] == '\\t' || end[-1] == '\\r' || end[-1] == '\\n')) {
–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;
}
static int drop_privileges(uid_t *service_uid, gid_t *service_gid)
{
struct passwd *account = getpwnam(SERVICE_USER);
if (account == NULL) {
fprintf(stderr, "Required service account '%s' is missing\\n", SERVICE_USER);
return -1;
}
uid_t uid = account->pw_uid;
gid_t gid = account->pw_gid;
if (uid == 0 || gid == 0 || geteuid() != 0) {
fprintf(stderr, "Service account or startup identity is invalid\\n");
return -1;
}
if (setgroups(0, NULL) != 0 || setgid(gid) != 0 || setuid(uid) != 0) {
perror("drop service privileges");
return -1;
}
if (getuid() != uid || geteuid() != uid ||
getgid() != gid || getegid() != gid) {
fprintf(stderr, "Privilege drop verification failed\\n");
return -1;
}
*service_uid = uid;
*service_gid = gid;
return 0;
}
static int rotate_log(FILE **log_file)
{
FILE *old_log = *log_file;
if (fflush(old_log) != 0) {
return -1;
}
*log_file = NULL;
if (fclose(old_log) != 0) {
return -1;
}
if (rename(LOG_PATH, LOG_PATH ".1") != 0) {
int rename_error = errno;
*log_file = fopen(LOG_PATH, "a");
if (*log_file == NULL) {
return -1;
}
errno = rename_error;
perror("rename active log; continuing with current file");
return 0;
}
*log_file = fopen(LOG_PATH, "a");
if (*log_file == NULL) {
int open_error = errno;
if (rename(LOG_PATH ".1", LOG_PATH) == 0) {
*log_file = fopen(LOG_PATH, "a");
}
if (*log_file == NULL) {
errno = open_error;
return -1;
}
errno = open_error;
perror("open new active log; restored previous path");
return 0;
}
if (fprintf(*log_file,
"debug-demo log: rotation complete; active file reopened pid=%ld\\n",
(long)getpid()) < 0 || fflush(*log_file) != 0) {
return -1;
}
return 1;
}
static int rotate_if_needed(FILE **log_file)
{
struct stat log_status;
if (fflush(*log_file) != 0 ||
fstat(fileno(*log_file), &log_status) != 0) {
return -1;
}
if (log_status.st_size < 1024) {
return 0;
}
return rotate_log(log_file) < 0 ? -1 : 0;
}
static int write_log(FILE **log_file, const char *format, …)
{
va_list arguments;
va_start(arguments, format);
int result = vfprintf(*log_file, format, arguments);
va_end(arguments);
if (result < 0 || fputc('\\n', *log_file) == EOF || fflush(*log_file) != 0) {
return -1;
}
return rotate_if_needed(log_file);
}
static int connect_server(void)
{
struct addrinfo hints = {0};
struct addrinfo *addresses = NULL;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
int lookup_result = getaddrinfo(SERVER_HOST, SERVER_PORT, &hints, &addresses);
if (lookup_result != 0) {
fprintf(stderr, "getaddrinfo: %s\\n", gai_strerror(lookup_result));
errno = EHOSTUNREACH;
return -1;
}
int connected_socket = -1;
for (struct addrinfo *address = addresses; address != NULL;
address = address->ai_next) {
int socket_fd = socket(address->ai_family, address->ai_socktype,
address->ai_protocol);
if (socket_fd < 0) {
continue;
}
if (connect(socket_fd, address->ai_addr, address->ai_addrlen) == 0) {
connected_socket = socket_fd;
break;
}
close(socket_fd);
}
freeaddrinfo(addresses);
if (connected_socket < 0) {
return -1;
}
struct timeval receive_timeout = {5, 0};
if (setsockopt(connected_socket, SOL_SOCKET, SO_RCVTIMEO,
&receive_timeout, sizeof(receive_timeout)) != 0) {
int saved_error = errno;
close(connected_socket);
errno = saved_error;
return -1;
}
return connected_socket;
}
static int send_all(int socket_fd, const char *data, size_t length)
{
size_t sent = 0;
while (sent < length) {
ssize_t result = send(socket_fd, data + sent, length – sent, MSG_NOSIGNAL);
if (result < 0 && errno == EINTR) {
if (!running) {
return -1;
}
continue;
}
if (result <= 0) {
if (result == 0) {
errno = EPIPE;
}
return -1;
}
sent += (size_t)result;
}
return 0;
}
static int receive_line(int socket_fd, char *line, size_t capacity)
{
size_t used = 0;
for (;;) {
char byte;
ssize_t result = recv(socket_fd, &byte, 1, 0);
if (result == 0) {
if (used == 0) {
return 0;
}
errno = EPROTO;
return -1;
}
if (result < 0) {
if (errno == EINTR && running) {
continue;
}
return -1;
}
if (byte == '\\n') {
line[used] = '\\0';
return 1;
}
if (used + 1 >= capacity) {
errno = EMSGSIZE;
return -1;
}
line[used++] = byte;
}
}
static void wait_seconds(unsigned int seconds)
{
struct timespec remaining = {(time_t)seconds, 0};
while (running && nanosleep(&remaining, &remaining) != 0) {
if (errno != EINTR) {
return;
}
}
}
static unsigned int increase_backoff(unsigned int seconds)
{
if (seconds >= MAX_RETRY_SECONDS / 2) {
return MAX_RETRY_SECONDS;
}
return seconds * 2;
}
static int retry_after_disconnect(FILE **log_file, unsigned int *backoff)
{
if (write_log(log_file, "tcp: retry in %u second(s)", *backoff) != 0) {
return -1;
}
wait_seconds(*backoff);
*backoff = increase_backoff(*backoff);
return 0;
}
static int run_client(FILE **log_file, unsigned int interval_sec)
{
unsigned long sequence = 0;
unsigned int backoff = 1;
while (running) {
int socket_fd = connect_server();
if (socket_fd < 0) {
int saved_error = errno;
if (write_log(log_file, "tcp: connect failed host=%s port=%s error=%s",
SERVER_HOST, SERVER_PORT, strerror(saved_error)) != 0 ||
retry_after_disconnect(log_file, &backoff) != 0) {
return -1;
}
continue;
}
if (write_log(log_file, "tcp: connected host=%s port=%s",
SERVER_HOST, SERVER_PORT) != 0) {
close(socket_fd);
return -1;
}
while (running) {
char payload[64];
char frame[80];
char response[MAX_LINE_BYTES];
int payload_length = snprintf(payload, sizeof(payload),
"tick=%lu", sequence);
int frame_length = snprintf(frame, sizeof(frame), "%s\\n", payload);
if (payload_length < 0 || (size_t)payload_length >= sizeof(payload) ||
frame_length < 0 || (size_t)frame_length >= sizeof(frame)) {
close(socket_fd);
errno = EMSGSIZE;
return -1;
}
if (send_all(socket_fd, frame, (size_t)frame_length) != 0) {
int saved_error = errno;
if (write_log(log_file, "tcp: send failed error=%s",
strerror(saved_error)) != 0) {
close(socket_fd);
return -1;
}
break;
}
int receive_result = receive_line(socket_fd, response, sizeof(response));
if (receive_result == 0) {
if (write_log(log_file, "tcp: peer closed connection") != 0) {
close(socket_fd);
return -1;
}
break;
}
if (receive_result < 0) {
int saved_error = errno;
if (write_log(log_file, "tcp: receive failed error=%s",
strerror(saved_error)) != 0) {
close(socket_fd);
return -1;
}
break;
}
if (write_log(log_file, "tcp: sent=%s received=%s", payload, response) != 0) {
close(socket_fd);
return -1;
}
++sequence;
backoff = 1;
wait_seconds(interval_sec);
}
close(socket_fd);
if (running && retry_after_disconnect(log_file, &backoff) != 0) {
return -1;
}
}
return 0;
}
int main(int argc, char *argv[])
{
const int values[] = {10, 20, 30, 40};
int total = compute_total(values, 4);
if (argc == 1) {
printf("debug-demo package v2: total = %d\\n", total);
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;
}
uid_t service_uid;
gid_t service_gid;
if (drop_privileges(&service_uid, &service_gid) != 0) {
return 1;
}
const char *config_status = NULL;
unsigned int interval_sec = load_interval(&config_status);
umask(027);
FILE *log_file = fopen(LOG_PATH, "a");
if (log_file == NULL) {
perror("open log as service user");
return 1;
}
if (write_log(&log_file,
"debug-demo start: pid=%ld uid=%ld gid=%ld config=%s interval_sec=%u",
(long)getpid(), (long)service_uid, (long)service_gid,
config_status, interval_sec) != 0 ||
run_client(&log_file, interval_sec) != 0) {
perror("debug-demo service");
if (log_file != NULL) {
fclose(log_file);
}
return 1;
}
if (write_log(&log_file, "debug-demo service: pid=%ld stopped",
(long)getpid()) != 0) {
perror("write final service log");
if (log_file != NULL) {
fclose(log_file);
}
return 1;
}
return fclose(log_file) == 0 ? 0 : 1;
}
EOF
代码保留了前文的服务账户降权、配置读取、日志目录和 1 KiB 日志轮转。新增的 send_all() 处理短写;receive_line() 读到换行符才返回完整响应,并对超长行和中途 EOF 报错。连接错误、接收超时或对端关闭后,当前连接会被关闭,随后按 1、2、4、8 秒退避重试;成功处理响应后,退避等待恢复为 1 秒。
SO_RCVTIMEO 设置的 5 秒针对单次阻塞接收等待,不是整条响应或整个请求的总时限。本例逐字节读取,若对端持续在每次超时前发来一个字节,完整响应可能耗时远超 5 秒。connect() 和 send() 仍是阻塞调用,未设置应用层超时;需要总时限的实现应使用单调时钟计算截止时间,并配合非阻塞 I/O 或 poll() 等机制。
所有日志写入都检查返回值;发生不可恢复的日志错误时,客户端结束服务,而不是继续使用可能已被轮转失败置空的日志指针。BusyBox init 可能随后再次启动服务,因此持续的存储故障仍需人工排查。
这份代码用于实验,不等同于生产网络客户端。它把收到的完整行作为响应记录,没有严格验证响应是否等于当前请求对应的 ACK tick=N;文中的对应关系由实测日志核对。重连后请求序号延续进程内状态,但请求超时后的业务结果可能不确定,不能据此推断请求“恰好执行一次”。实际协议还需定义响应校验、幂等键、认证、加密和业务超时策略。
七、关机重建镜像
确认 QEMU 已退出后,在 Ubuntu 主机终端 B 执行;不需要停止终端 A 中的 Python 服务:
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 $?
debug-demo-rebuild 编译并安装应用;完整 make 重新生成 rootfs 镜像。本文只使用 libc/POSIX socket 接口,不需要额外 Buildroot 库或改变工具链。不要在 QEMU 正使用 rootfs.ext2 时重建镜像。
构建命令应返回 0;非零时不要继续启动镜像。本轮已完成修订版 QEMU 收发、断线重连和 init 重启验证,但当时的构建退出码未记录。运行日志能证明系统启动并运行,不能补出历史构建退出码;如需发布构建状态,应如实注明未记录,或重新构建并明确标注新构建的结果,不必因此重复已完成的 QEMU 回归。
八、保持测试服务运行,启动 QEMU
先确认 Ubuntu 主机终端 A 中第四节的 Python 服务仍在运行。若它一直保持监听,直接继续,不要重复执行服务启动命令;只有服务已退出时,才在终端 A 重新执行第四节命令。
构建成功后,在 Ubuntu 主机终端 B 启动 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
执行上述命令后,终端 B 被 QEMU 占用,显示的是目标系统的串口控制台,不再是 Ubuntu 主机 shell。以 root 登录后,在这个 QEMU 目标端确认服务只有一个实例,并查看日志:
pidof debug_demo
service_pid=$(pidof debug_demo)
grep -E '^(Name|Uid|Gid|Groups):' "/proc/$service_pid/status"
tail -n 20 /var/log/debug-demo/debug-demo.log
此后,主机服务输出看终端 A,目标端命令在终端 B 执行。若还需要执行 Ubuntu 主机命令,请另开一个主机终端,不要把主机命令输入 QEMU 控制台。
本轮回归在 QEMU 目标端取得的实际输出如下:
# pidof debug_demo
121
# service_pid=$(pidof debug_demo)
# grep -E '^(Name|Uid|Gid|Groups):' "/proc/$service_pid/status"
Name: debug_demo
Uid: 100 100 100 100
Gid: 101 101 101 101
Groups:
# tail -n 20 /var/log/debug-demo/debug-demo.log
debug-demo start: pid=121 uid=100 gid=101 config=loaded interval_sec=2
tcp: connected host=10.0.2.2 port=5000
tcp: sent=tick=0 received=ACK tick=0
tcp: sent=tick=1 received=ACK tick=1
tcp: sent=tick=2 received=ACK tick=2
tcp: sent=tick=3 received=ACK tick=3
tcp: sent=tick=4 received=ACK tick=4
tcp: sent=tick=5 received=ACK tick=5
tcp: sent=tick=6 received=ACK tick=6
tcp: sent=tick=7 received=ACK tick=7
tcp: sent=tick=8 received=ACK tick=8
这份记录确认了以下结果:
- 检查时 pidof 只返回一个 PID,即 121;这是本轮运行值,不应在后续命令中写死。
- 该进程的真实、有效、保存和文件系统 UID 均为 100,GID 均为 101,附加组为空。登录控制台使用 root,不代表服务也以 root 身份运行。
- 配置读取成功,配置的发送间隔为 2 秒;这里没有时间戳,不能据此测量实际收发间隔。
- 客户端已连接 10.0.2.2:5000,记录了 tick=0 至 tick=8 共 9 次请求及对应的 ACK 响应。
因此,本轮非 root 客户端的基本 TCP 请求/响应已跑通。断线重连、退避和 init 重启的本轮结果见第九节;日志写入失败分支及短写、超长行、不完整帧等异常边界仍未专项测试。
如果日志反复显示 Connection refused,先检查主机 Python 服务是否仍运行、端口是否一致,以及主机防火墙是否允许该连接。若 QEMU user-mode 网关并非 10.0.2.2,按实际网络配置修改 SERVER_HOST 后重新编译。
九、验证收发、断线重连与进程恢复
记录归属: 本节只记录本轮修订版实验。本轮客户端先由 PID 121 运行,SIGTERM 后由 init 拉起 PID 133。若重启或重新登录后 PID 变化,以现场查询为准。
1. 验证一问一答
观察主机 Python 服务终端的 received=… sent=…,以及 QEMU 服务日志中的 sent=… received=…。两边的行内容应对应。每次循环发送一行 tick=N,主机按换行符解析,并回一行 ACK tick=N。
bytearray(b'tick=0') 是 Python 对字节数组的打印形式,不是额外的协议内容。请求末尾的换行符已被 partition(b"\\n") 分离,所以 received 中不显示它;sent=b'ACK tick=0\\n' 中的 \\n 表示响应确实带有换行符。
本轮 Ubuntu 主机终端 A 的实际输出如下:
test server listening on 0.0.0.0:5000
accepted ('127.0.0.1', 37286)
received=bytearray(b'tick=0') sent=b'ACK tick=0\\n'
received=bytearray(b'tick=1') sent=b'ACK tick=1\\n'
received=bytearray(b'tick=2') sent=b'ACK tick=2\\n'
received=bytearray(b'tick=3') sent=b'ACK tick=3\\n'
received=bytearray(b'tick=4') sent=b'ACK tick=4\\n'
received=bytearray(b'tick=5') sent=b'ACK tick=5\\n'
received=bytearray(b'tick=6') sent=b'ACK tick=6\\n'
received=bytearray(b'tick=7') sent=b'ACK tick=7\\n'
received=bytearray(b'tick=8') sent=b'ACK tick=8\\n'
received=bytearray(b'tick=9') sent=b'ACK tick=9\\n'
received=bytearray(b'tick=10') sent=b'ACK tick=10\\n'
received=bytearray(b'tick=11') sent=b'ACK tick=11\\n'
received=bytearray(b'tick=12') sent=b'ACK tick=12\\n'
received=bytearray(b'tick=13') sent=b'ACK tick=13\\n'
received=bytearray(b'tick=14') sent=b'ACK tick=14\\n'
received=bytearray(b'tick=15') sent=b'ACK tick=15\\n'
received=bytearray(b'tick=16') sent=b'ACK tick=16\\n'
received=bytearray(b'tick=17') sent=b'ACK tick=17\\n'
received=bytearray(b'tick=18') sent=b'ACK tick=18\\n'
received=bytearray(b'tick=19') sent=b'ACK tick=19\\n'
received=bytearray(b'tick=20') sent=b'ACK tick=20\\n'
received=bytearray(b'tick=21') sent=b'ACK tick=21\\n'
received=bytearray(b'tick=22') sent=b'ACK tick=22\\n'
received=bytearray(b'tick=23') sent=b'ACK tick=23\\n'
received=bytearray(b'tick=24') sent=b'ACK tick=24\\n'
received=bytearray(b'tick=25') sent=b'ACK tick=25\\n'
received=bytearray(b'tick=26') sent=b'ACK tick=26\\n'
主机端处理了 tick=0 至 tick=26 共 27 行请求并发送相应 ACK。QEMU 端本轮提供的输出只覆盖到 tick=8;由于两端记录截取时点不同,不据此声称客户端日志已确认读取 tick=9 之后的每一条响应。127.0.0.1 是 QEMU user-mode 网络转接后主机端看到的来源地址,37286 是临时源端口,不是监听端口。
2. 验证断线后重连
先在 QEMU 目标端(终端 B) 执行 pidof debug_demo,记录停服前的 PID。然后在 Ubuntu 主机终端 A 按 Ctrl+C 停止 Python 服务,不要在运行 QEMU 的终端 B 中操作。终端 A 返回主机 shell 提示符后,表示前台服务已退出;Python 可能同时打印 KeyboardInterrupt,这是本次主动中断产生的提示。客户端下一次收发应遇到 EOF、超时或 socket 错误,记录断开并进入退避重试。
代码中的重试等待依次为 1、2、4、8 秒,达到 8 秒后保持上限,以避免立即反复重试。连接调用自身的耗时还会影响两次尝试之间的实际间隔,不能仅凭等待参数断言精确的重试周期或 CPU 占用。
主机服务保持停止时,及时在 QEMU 目标端(终端 B) 查看重试日志:
tail -n 20 /var/log/debug-demo/debug-demo.log
grep -H -E 'tcp: (peer closed|send failed|receive failed|connect failed|retry in)' \\
/var/log/debug-demo/debug-demo.log*
在主动停服的前提下,Connection refused 是预期现象,不表示需要重建镜像。日志没有时间戳,因此只能根据日志中的等待参数判断退避状态,不能测量实际等待时长。
本轮修订版客户端(PID 121)的实际输出如下:
# tail -n 20 /var/log/debug-demo/debug-demo.log
debug-demo log: rotation complete; active file reopened pid=121
tcp: sent=tick=99 received=ACK tick=99
tcp: sent=tick=100 received=ACK tick=100
tcp: peer closed connection
tcp: retry in 1 second(s)
tcp: connect failed host=10.0.2.2 port=5000 error=Connection refused
tcp: retry in 2 second(s)
# grep -H -E 'tcp: (peer closed|send failed|receive failed|connect failed|retry in)' \\
> /var/log/debug-demo/debug-demo.log*
/var/log/debug-demo/debug-demo.log:tcp: peer closed connection
/var/log/debug-demo/debug-demo.log:tcp: retry in 1 second(s)
/var/log/debug-demo/debug-demo.log:tcp: connect failed host=10.0.2.2 port=5000 error=Connection refused
/var/log/debug-demo/debug-demo.log:tcp: retry in 2 second(s)
/var/log/debug-demo/debug-demo.log:tcp: connect failed host=10.0.2.2 port=5000 error=Connection refused
/var/log/debug-demo/debug-demo.log:tcp: retry in 4 second(s)
本轮记录确认连接关闭后,客户端先记录 1 秒等待;连接被拒绝后,等待参数递增到 2 秒,再递增到 4 秒。tail 与稍后的 grep 是不同时间点取得的输出,因此 tail 尚未显示的 4 秒记录出现在检索结果中并不矛盾。此处截取尚未显示 8 秒;后面的本轮恢复记录显示客户端达到 8 秒上限。所有输出都没有时间戳,因此只能确认退避参数序列,不能测量实际等待时长。
日志仅保留一份轮转备份,应及时记录输出。通配符检索按文件顺序输出,不是按时间合并;读取期间仍可能写入和轮转,不能仅凭缺少某一行判断请求丢失。
接下来在 Ubuntu 主机终端 A 重新执行第四节的完整 Python 命令,保持服务运行;无需重启 QEMU,也不要手工启动另一个客户端。在 QEMU 目标端(终端 B) 执行:
pidof debug_demo
tail -n 20 /var/log/debug-demo/debug-demo.log
若客户端仍处于退避等待,不一定立刻出现新连接;稍后再次查看。预期应重新出现 tcp: connected 和 sent=… received=…,并检查 PID 是否与停服前一致。恢复后及时记录输出,避免被轮转覆盖。
请求序号在断线后继续递增,符合客户端保留进程内计数的实现。日志未展示的中间序号不是丢包证据。该测试验证 TCP 连接恢复,不保证未确认请求是否已被执行,也不证明请求恰好执行一次。
本轮修订版客户端恢复主机服务后的实际输出如下:
# pidof debug_demo
121
# tail -n 20 /var/log/debug-demo/debug-demo.log
debug-demo log: rotation complete; active file reopened pid=121
tcp: retry in 8 second(s)
tcp: connect failed host=10.0.2.2 port=5000 error=Connection refused
tcp: retry in 8 second(s)
tcp: connect failed host=10.0.2.2 port=5000 error=Connection refused
tcp: retry in 8 second(s)
tcp: connect failed host=10.0.2.2 port=5000 error=Connection refused
tcp: retry in 8 second(s)
tcp: connect failed host=10.0.2.2 port=5000 error=Connection refused
tcp: retry in 8 second(s)
tcp: connected host=10.0.2.2 port=5000
tcp: sent=tick=101 received=ACK tick=101
tcp: sent=tick=102 received=ACK tick=102
tcp: sent=tick=103 received=ACK tick=103
tcp: sent=tick=104 received=ACK tick=104
tcp: sent=tick=105 received=ACK tick=105
tcp: sent=tick=106 received=ACK tick=106
本轮客户端在多次 Connection refused 后仍为 PID 121,记录了 8 秒上限等待,随后重新连接并完成 tick=101 至 tick=106 共 6 次请求/响应。因此,本轮“同一进程在主机服务恢复后自动重连并恢复收发”已验证。结合此前本轮日志中记录的 1、2、4 秒参数,可确认日志覆盖了退避增长至 8 秒上限;这些日志没有时间戳,不能据此测量实际等待时长。
3. 验证服务仍由 init 监督
本节验证“进程退出后由 init 启动新进程”,与上一节“同一进程重连 TCP”不同。保持 Ubuntu 主机终端 A 的 Python 服务运行;以下命令全部在 QEMU 目标端(终端 B) 的同一个 shell 中执行。不要重启 QEMU,也不要手工启动另一个客户端。
第一步:记录当前 PID。
old_pid=$(pidof debug_demo)
echo "$old_pid"
应只输出一个正整数。若没有输出或有多个 PID,先停止本节操作并检查服务状态,不要继续发送信号。将实际输出作为 old_pid,不要假设 PID 固定。
本轮第一步的实际输出为:
# old_pid=$(pidof debug_demo)
# echo "$old_pid"
121
当前只查询到一个服务 PID 121。这是 SIGTERM 前的基线;仍需执行后续步骤,才能验证 BusyBox init 是否启动了新进程。
第二步:发送 SIGTERM,让 init 自动重启服务。
kill -TERM "$old_pid"
成功发送信号时通常没有输出,这不等于新进程已经启动。等待几秒后查询:
pidof debug_demo
预期出现一个不同于旧 PID 的新 PID。若暂时没有输出,或仍是旧 PID,稍后再执行这条查询;不要重复发送信号,也不要手工启动服务。若持续没有新 PID,应先检查日志和 init 配置,不能继续按验证成功处理。
本轮第二步的实际输出为:
# kill -TERM "$old_pid"
# pidof debug_demo
133
查询返回的新 PID 133 与旧 PID 121 不同,符合 init 拉起新进程的预期。还需执行第三步,核验新进程的父进程、身份和收发状态,不能仅凭 PID 变化判断本节全部通过。
第三步:核验新进程身份和 TCP 收发。
确认查询结果只有一个新 PID 后,执行:
new_pid=$(pidof debug_demo)
printf 'old_pid=%s new_pid=%s\\n' "$old_pid" "$new_pid"
grep -E '^(Name|PPid|Uid|Gid|Groups):' "/proc/$new_pid/status"
tail -n 20 /var/log/debug-demo/debug-demo.log
本轮第三步的实际输出为:
# new_pid=$(pidof debug_demo)
# printf 'old_pid=%s new_pid=%s\\n' "$old_pid" "$new_pid"
old_pid=121 new_pid=133
# grep -E '^(Name|PPid|Uid|Gid|Groups):' "/proc/$new_pid/status"
Name: debug_demo
PPid: 1
Uid: 100 100 100 100
Gid: 101 101 101 101
Groups:
# tail -n 20 /var/log/debug-demo/debug-demo.log
debug-demo log: rotation complete; active file reopened pid=133
tcp: sent=tick=12 received=ACK tick=12
tcp: sent=tick=13 received=ACK tick=13
tcp: sent=tick=14 received=ACK tick=14
tcp: sent=tick=15 received=ACK tick=15
tcp: sent=tick=16 received=ACK tick=16
tcp: sent=tick=17 received=ACK tick=17
tcp: sent=tick=18 received=ACK tick=18
tcp: sent=tick=19 received=ACK tick=19
tcp: sent=tick=20 received=ACK tick=20
tcp: sent=tick=21 received=ACK tick=21
tcp: sent=tick=22 received=ACK tick=22
PID 从 121 变为 133;新进程由 PID 1 直接启动,四个 UID 均为 100、四个 GID 均为 101,附加组为空。上面的轮转记录标出 pid=133,并显示 tick=12 至 tick=22 的收发。
随后检索当前日志和轮转备份,找到新进程的启动、连接以及较早收发记录:
# grep -H -E 'debug-demo start:|tcp: connected|tcp: sent=' \\
> /var/log/debug-demo/debug-demo.log*
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=12 received=ACK tick=12
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=13 received=ACK tick=13
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=14 received=ACK tick=14
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=15 received=ACK tick=15
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=16 received=ACK tick=16
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=17 received=ACK tick=17
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=18 received=ACK tick=18
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=19 received=ACK tick=19
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=20 received=ACK tick=20
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=21 received=ACK tick=21
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=22 received=ACK tick=22
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=23 received=ACK tick=23
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=24 received=ACK tick=24
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=25 received=ACK tick=25
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=26 received=ACK tick=26
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=27 received=ACK tick=27
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=28 received=ACK tick=28
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=29 received=ACK tick=29
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=30 received=ACK tick=30
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=31 received=ACK tick=31
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=32 received=ACK tick=32
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=33 received=ACK tick=33
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=282 received=ACK tick=282
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=283 received=ACK tick=283
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=284 received=ACK tick=284
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=285 received=ACK tick=285
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=286 received=ACK tick=286
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=287 received=ACK tick=287
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=288 received=ACK tick=288
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=289 received=ACK tick=289
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=290 received=ACK tick=290
/var/log/debug-demo/debug-demo.log.1:debug-demo start: pid=133 uid=100 gid=101 config=loaded interval_sec=2
/var/log/debug-demo/debug-demo.log.1:tcp: connected host=10.0.2.2 port=5000
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=0 received=ACK tick=0
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=1 received=ACK tick=1
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=2 received=ACK tick=2
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=3 received=ACK tick=3
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=4 received=ACK tick=4
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=5 received=ACK tick=5
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=6 received=ACK tick=6
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=7 received=ACK tick=7
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=8 received=ACK tick=8
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=9 received=ACK tick=9
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=10 received=ACK tick=10
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=11 received=ACK tick=11
轮转备份确认 PID 133 启动并连接成功,记录了 tick=0 至 tick=11;当前日志继续记录 tick=12 至 tick=33。合计可确认新进程完成 tick=0 至 tick=33 共 34 次请求/响应。命令按文件展开顺序输出,先列当前日志再列 .1 备份,并非按时间排序;因此先看到 tick=12、再看到启动行和 tick=0 不代表序号倒退。结合新旧 PID、PPid=1 和身份信息,本轮 init respawn 及新进程恢复收发验证通过。
之后再次检索日志,看到当前文件有 tick=87 至 tick=98,轮转备份有 tick=62 至 tick=86;中间连续行省略如下:
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=87 received=ACK tick=87
…
/var/log/debug-demo/debug-demo.log:tcp: sent=tick=98 received=ACK tick=98
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=62 received=ACK tick=62
…
/var/log/debug-demo/debug-demo.log.1:tcp: sent=tick=86 received=ACK tick=86
这次检索没有找到 debug-demo start: 或 tcp: connected,说明这些行已不在当前两份保留日志中;此前捕获的 PID 133 启动与连接记录仍是本轮已有证据。此次 grep 未同时查询 PID,因此只据此说明保留日志中的 ACK 收发已到 tick=98,不单独用它推断查询当时的进程身份。文件参数顺序是当前日志在前、.1 在后,不能按输出顺序判断 tick 先后;轮转也会使更早行不再保留。
本节通过标准如下,均需以实际输出核对:
- 新旧 PID 不同,且查询时只有一个服务实例。
- 新进程的 PPid 为 1,符合本文由 BusyBox init 直接启动服务的配置。
- UID/GID 仍为非 root 服务账户,附加组为空;具体数值以目标系统账户配置为准。
- 新进程恢复 ACK 收发,并能通过对应新 PID 的启动或轮转记录关联到本轮进程,不能仅凭旧日志判定恢复。
新进程中的请求计数会从 tick=0 重新开始,这是进程内状态重新初始化的正常现象,不是 TCP 重连导致序号清零。若检查稍晚,当前日志可能只显示后续序号;启动和连接记录也可能已被轮转,可及时查看现存日志:
grep -H -E 'debug-demo start:|tcp: connected|tcp: sent=' \\
/var/log/debug-demo/debug-demo.log*
注意区分本轮新进程与轮转备份中的旧记录。若终端 A 的 Python 服务意外退出,先记录报错,再按第四节恢复主机服务;否则无法完成新进程收发验证。
本轮 init 重启及新进程恢复收发验证已通过。 实测支持新旧 PID、父进程、身份和 ACK 收发结论;日志轮转造成的未展示行不作为实测内容。
十、常见问题与实验边界
| Connection refused | 主机测试服务是否启动、端口是否一致、主机防火墙规则 |
| Network is unreachable | QEMU 是否使用 user-mode 网络设备、guest 路由和主机地址是否正确 |
| TCP 已连接但没有完整响应 | 服务端是否发送换行符;客户端是否遇到超时或不完整帧 |
| 每条响应前后内容错位 | 检查分帧规则、缓冲区长度和是否错误地把一次 recv() 当成一条消息 |
| 服务断线后不停快速重试 | 检查退避路径是否经过、错误分支是否遗漏等待 |
| 新服务端启动后客户端仍不恢复 | 检查客户端是否关闭了旧 socket、是否仍处于超时等待,以及服务器监听端口 |
本篇用 TCP 行协议演示嵌入式 Linux 客户端的收发、分帧、错误处理和重连。它不覆盖二进制协议、TLS、证书管理、DNS 配置、IPv6、并发、多路复用或实时性保证。主机 Python 程序是实验对端,不应直接作为产品服务部署。
验证范围
| 初始非 root 收发 | PID 121,UID/GID 为 100/101,附加组为空;初始运行日志节选记录 tick=0 至 tick=8 共 9 次请求/响应 |
| init 重启后收发 | PID 133,PPid=1,UID/GID 为 100/101,附加组为空;轮转日志记录 tick=0 至 tick=33 共 34 次请求/响应 |
| 主机端收发 | Python 服务记录接收 tick=0 至 tick=26 并发送 ACK;两端截取范围和时间点不同,不据此声称每条主机响应均有对应的客户端记录 |
| 停服后自动重连 | 本轮 PID 121 记录了 peer close、1、2、4、8 秒退避参数;主机服务恢复后重新连接并完成 tick=101 至 tick=106 的 6 次请求/响应 |
| 退避等待 | 本轮日志记录了 1、2、4、8 秒参数并达到上限;没有时间戳,未测量精确实际间隔 |
| Python 连接异常处理 | 修订后代码已通过本地模拟 socket 测试:接收重置、发送断管后接受下一连接,关闭出错连接,保持 Ctrl+C 退出行为 |
| Python 正常分帧 | 本地模拟测试覆盖了拆分输入和同一片段中的两行请求,均返回预期 ACK;不等于 ARM64 端到端测试 |
| C 日志错误处理 | 本轮运行记录覆盖正常日志路径;日志写入/轮转失败分支尚未故障注入验证,且未保存构建退出码 |
| 修订版验证 | 已记录本轮基本收发、主机端收发、退避重连及 init 重启;构建退出码未记录,日志错误分支和其他异常边界未专项测试 |
| 其他异常边界 | C 短写、超长行、不完整帧、慢速响应和总时限等尚未进行专项验证 |
十一、小结
本篇沿用 Buildroot、QEMU、BusyBox init 和 debug-demo 非特权服务,把本地日志服务扩展为 TCP 客户端。通过显式的换行分帧、完整发送循环、超时处理和有上限的重连退避,展示了从“能收发”到“能识别消息并处理断线”的基本演进。
本轮基本收发回归已记录 PID 121 以非 root 身份连接主机,QEMU 端完成并记录 tick=0 至 tick=8 的 9 次请求/响应;主机端记录处理 tick=0 至 tick=26 并发送对应 ACK。由于两端日志截取范围不同,不把主机端 tick=9 之后的 ACK 写成客户端已确认。主机停服后,本轮日志显示 peer closed 和 1、2、4、8 秒退避参数;服务恢复后,同一 PID 121 自动重连并完成 tick=101 至 tick=106 的 6 次请求/响应。随后 SIGTERM 使 PID 从 121 变为 133;新进程由 PID 1 启动,保持非 root 权限,并从 tick=0 连续收发至 tick=33。构建退出码未记录,日志错误分支及其他未测异常仍待验证。
这些记录支持本轮基本收发、主机停服后的退避、同一进程自动重连,以及 init 拉起新进程后的身份和收发,不代表客户端已具备生产级可靠性。构建退出码未记录;日志错误分支及其他未测项目按上表保留为实验边界。
网硕互联帮助中心






评论前必须登录!
注册