云计算百科
云计算领域专业知识百科平台

让 Buildroot 服务先完成 TCP 收发,再处理消息边界、断线与重连

让 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 方式监督

本次实际验证三条主线:

  • QEMU 中的程序能连接主机并完成请求/响应。
  • 主机服务停止后,客户端持续重试;主机服务恢复后,客户端自动重连并恢复收发。
  • 客户端进程退出后,BusyBox init 自动启动新进程,新进程降权后恢复收发。
  • 换行分帧、短写处理和 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 拉起新进程后的身份和收发,不代表客户端已具备生产级可靠性。构建退出码未记录;日志错误分支及其他未测项目按上表保留为实验边界。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 让 Buildroot 服务先完成 TCP 收发,再处理消息边界、断线与重连
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!