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); }
问题根因
- 栈变量生命周期失效:父进程for循环内定义的
uv_pipe_t pipe、uv_process_t child_req、uv_stdio_container_t child_stdio[4]都是栈上局部变量,单次循环结束后变量就出作用域,内存会被后续循环操作覆盖。单进程场景下循环只执行1次,这些变量在uv_run运行时还未被覆盖,所以能正常工作;多进程场景下第一次循环创建的句柄内存被覆写,libuv访问到非法内存,自然无法正常读取数据。 - 子进程写入逻辑错误:子进程中
write调用传入的长度是sizeof(s1)-1,sizeof计算的是std::string对象本身的栈内存大小(通常为24/32字节),不是实际字符串长度,会导致写入内容截断或乱码;另外管道是内存中的进程间通信机制,没有持久化存储,fsync(3)对管道fd完全无效,属于多余调用。 - 临时指针悬空:父进程中
args[1] = (char *) to_string(i).c_str();写法存在问题,to_string(i)返回的是临时string对象,该行执行结束后临时对象就会被析构,c_str()获取的指针会变成野指针,导致子进程启动参数异常。 - 写入重试逻辑不严谨:子进程中
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
相关产品推荐
相关产品推荐

