You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

libuv管道实现父子进程通信:多子进程时父进程收不到数据

问题描述

编写程序实现父进程通过管道接收子进程发送的数据,运行时出现异常:

  • 仅启动1个子进程时,父进程可正常接收子进程数据
  • 启动2个子进程时,父进程无法接收到子进程发送的数据

子进程代码(test.cpp)

#include <stdio.h>
#include <unistd.h>
#include <string>

using namespace std;

int main(int argc, char **argv) {
    if (argc <= 1) {
        printf("params error !\n");
        return -1;
    }
    string index = argv[1];
    if (index.empty()) {
        printf("params error !\n");
        return -1;
    }
    int num = 0;
    string s1 = "fourth stdio! " + index + "\n";
    while (num < 100) {
        num++;
        ssize_t r;

        do
            r = write(3, s1.c_str(), sizeof(s1) - 1);
        while (r == -1);

        fsync(3);
        sleep(1);
    }
    return 0;
}

父进程代码(main.cpp,基于libuv实现)

#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string>
#include <uv.h>

using namespace std;
uv_loop_t *loop;


void on_exit(uv_process_t *req, int64_t exit_status, int term_signal) {
    fprintf(stderr, "Process exited with status %" PRId64 ", signal %d\n", exit_status, term_signal);
    uv_close((uv_handle_t *) req, NULL);
}

void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
    *buf = uv_buf_init((char *) malloc(suggested_size), suggested_size);
}

void on_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
    if (nread < 0) {
        if (nread != UV_EOF)
            fprintf(stderr, "Read error %s\n", uv_err_name(nread));
        uv_close((uv_handle_t *) client, NULL);
        free(buf->base);
        return;
    }

    char *data = (char *) malloc(sizeof(char) * (nread + 1));
    data[nread] = '\0';
    strncpy(data, buf->base, nread);

    fprintf(stdout, "%s", data);
    free(data);
    free(buf->base);
}


int main() {
    loop = uv_default_loop();

    for (int i = 0; i < 2; i++) {// The variable i controls the number of child processes
        uv_process_t child_req;
        string path = "/home/kanong/code/test_project/test";

        char *args[3];
        args[0] = (char *) path.c_str();
        args[1] = (char *) to_string(i).c_str();
        args[2] = NULL;

        /* ... */
        uv_pipe_t pipe;
        uv_pipe_init(loop, &pipe, 0);
        uv_stdio_container_t child_stdio[4];
        uv_process_options_t options = {0};
        options.stdio_count = 4;
        options.stdio = child_stdio;


        child_stdio[0].flags = UV_IGNORE;
        child_stdio[1].flags = UV_INHERIT_FD;
        child_stdio[1].data.fd = 1;
        child_stdio[2].flags = UV_INHERIT_FD;
        child_stdio[2].data.fd = 2;
        child_stdio[3].flags = (uv_stdio_flags) (UV_CREATE_PIPE | UV_WRITABLE_PIPE);
        child_stdio[3].data.stream = (uv_stream_t *) &pipe;

        options.exit_cb = on_exit;
        options.file = args[0];
        options.args = args;

        int r;
        if ((r = uv_spawn(loop, &child_req, &options))) {
            fprintf(stderr, "%s\n", uv_strerror(r));
            return 1;
        }
        if ((r = uv_read_start((uv_stream_t *) &pipe, alloc_buffer, on_read))) {
            fprintf(stderr, "%s\n", uv_strerror(r));
            return 1;
        }

    }

    return uv_run(loop, UV_RUN_DEFAULT);
}
问题根因
  1. 栈变量生命周期失效:父进程for循环内定义的uv_pipe_t pipe、uv_process_t child_req、uv_stdio_container_t child_stdio[4]都是栈上局部变量,单次循环结束后变量就出作用域,内存会被后续循环操作覆盖。单进程场景下循环只执行1次,这些变量在uv_run运行时还未被覆盖,所以能正常工作;多进程场景下第一次循环创建的句柄内存被覆写,libuv访问到非法内存,自然无法正常读取数据。
  2. 子进程写入逻辑错误:子进程中write调用传入的长度是sizeof(s1)-1,sizeof计算的是std::string对象本身的栈内存大小(通常为24/32字节),不是实际字符串长度,会导致写入内容截断或乱码;另外管道是内存中的进程间通信机制,没有持久化存储,fsync(3)对管道fd完全无效,属于多余调用。
  3. 临时指针悬空:父进程中args[1] = (char *) to_string(i).c_str();写法存在问题,to_string(i)返回的是临时string对象,该行执行结束后临时对象就会被析构,c_str()获取的指针会变成野指针,导致子进程启动参数异常。
  4. 写入重试逻辑不严谨:子进程中write返回-1时无差别重试,若管道已经关闭会触发死循环。
修复方案
  • 延长libuv相关句柄的生命周期,不要在循环内用栈变量存储需要在事件循环中长期使用的对象,可以用数组按子进程序号存储uv_pipe_t、uv_process_t、参数字符串等对象,保证整个事件循环运行期间内存有效;也可以选择动态堆分配,在句柄关闭回调中释放内存。
  • 修正子进程写入长度,将sizeof(s1)-1替换为s1.size(),删除多余的fsync调用,仅对EINTR中断错误做写入重试,其他错误直接退出避免死循环。
  • 不要直接取临时string对象的c_str()指针存入参数数组,需保证字符串对象的生命周期覆盖子进程启动的全流程。

修复后父进程核心代码示例

#define CHILD_COUNT 2
int main() {
    loop = uv_default_loop();
    // 数组存储所有跨循环生命周期的对象,避免栈内存覆写
    uv_pipe_t pipes[CHILD_COUNT];
    uv_process_t child_reqs[CHILD_COUNT];
    string arg_strs[CHILD_COUNT];
    char* args[CHILD_COUNT][3];
    uv_stdio_container_t child_stdio[CHILD_COUNT][4];
    string path = "/home/kanong/code/test_project/test";

    for (int i = 0; i < CHILD_COUNT; i++) {
        arg_strs[i] = to_string(i);
        args[i][0] = (char*)path.c_str();
        args[i][1] = (char*)arg_strs[i].c_str();
        args[i][2] = NULL;

        uv_pipe_init(loop, &pipes[i], 0);
        uv_process_options_t options = {0};
        options.stdio_count = 4;
        options.stdio = child_stdio[i];

        child_stdio[i][0].flags = UV_IGNORE;
        child_stdio[i][1].flags = UV_INHERIT_FD;
        child_stdio[i][1].data.fd = 1;
        child_stdio[i][2].flags = UV_INHERIT_FD;
        child_stdio[i][2].data.fd = 2;
        child_stdio[i][3].flags = (uv_stdio_flags)(UV_CREATE_PIPE | UV_WRITABLE_PIPE);
        child_stdio[i][3].data.stream = (uv_stream_t*)&pipes[i];

        options.exit_cb = on_exit;
        options.file = args[i][0];
        options.args = args[i];

        int r;
        if ((r = uv_spawn(loop, &child_reqs[i], &options))) {
            fprintf(stderr, "%s\n", uv_strerror(r));
            return 1;
        }
        if ((r = uv_read_start((uv_stream_t*)&pipes[i], alloc_buffer, on_read))) {
            fprintf(stderr, "%s\n", uv_strerror(r));
            return 1;
        }
    }

    return uv_run(loop, UV_RUN_DEFAULT);
}

修复后子进程核心写入代码

while (num < 100) {
    num++;
    ssize_t r;
    do {
        r = write(3, s1.c_str(), s1.size());
    } while (r == -1 && errno == EINTR); // 仅对系统中断错误重试
    if (r == -1) break; // 管道异常直接退出循环
    sleep(1);
}

内容的提问来源于stack exchange,提问作者kanong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 23:12:20