如何修复exec()重定向stdout到管道后父进程读取失败的问题?
C代码错误分析与cksum管道通信修复方案
代码中的核心错误
- 缓冲区未初始化且长度计算错误:
get_cksum中buffer初始化为NULL,read尝试向空指针写入,直接触发非法内存访问,导致返回-1;同时sizeof(buffer)计算的是指针的大小(32位系统4字节、64位8字节),而非实际需要的缓冲区大小,无法容纳cksum的完整输出。 - 子进程未关闭冗余管道描述符:子进程执行
dup2(pipe[1], STDOUT_FILENO)后,未关闭原pipe[1]描述符,虽然后台会自动回收,但显式关闭是良好编程习惯,也避免管道写端引用计数未及时归零导致的潜在阻塞问题。 - 错误的返回值类型:
create_cksum_child的返回值类型是pid_t(整数类型),但fork失败时返回NULL(指针类型),类型不匹配,正确做法是返回-1表示错误。 - 父进程未及时关闭管道写端:
get_cksum创建管道后,父进程不需要写入管道,但未关闭fd[1],可能导致父进程读取管道时无法正确识别EOF信号(尽管子进程退出后会关闭写端,主动关闭冗余描述符更严谨)。 - 父进程提前等待子进程:原代码中
create_cksum_child里父进程直接调用waitpid,虽然不会直接导致读取失败,但会让父进程阻塞到子进程结束后再读取,属于逻辑冗余,且不利于异步场景扩展。
正确实现代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <errno.h> #include <string.h> // 创建子进程执行cksum,将输出重定向到管道 pid_t create_cksum_child(int pipe_fd[], const char *filename) { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); return -1; } if (pid == 0) { // 子进程:关闭管道读端,重定向stdout到管道写端 close(pipe_fd[0]); if (dup2(pipe_fd[1], STDOUT_FILENO) == -1) { perror("dup2 failed"); exit(EXIT_FAILURE); } // 关闭冗余的管道写端 close(pipe_fd[1]); // 执行cksum命令 execlp("/usr/bin/cksum", "cksum", filename, NULL); // 如果execlp返回,说明执行失败 perror("execlp failed"); exit(EXIT_FAILURE); } // 父进程:关闭管道写端(不需要写入) close(pipe_fd[1]); return pid; } // 从管道读取cksum结果,返回动态分配的字符串(调用者需free) char *get_cksum(const char *filename) { int pipe_fd[2]; if (pipe(pipe_fd) < 0) { perror("pipe failed"); return NULL; } pid_t child_pid = create_cksum_child(pipe_fd, filename); if (child_pid == -1) { close(pipe_fd[0]); close(pipe_fd[1]); return NULL; } // 初始化动态缓冲区 char *buffer = NULL; size_t buffer_size = 256; // 初始大小足够容纳cksum输出 ssize_t bytes_read; size_t total_read = 0; buffer = malloc(buffer_size); if (!buffer) { perror("malloc failed"); close(pipe_fd[0]); waitpid(child_pid, NULL, 0); // 回收子进程,避免僵尸进程 return NULL; } // 循环读取直到管道EOF while ((bytes_read = read(pipe_fd[0], buffer + total_read, buffer_size - total_read - 1)) > 0) { total_read += bytes_read; // 缓冲区不足时扩容 if (total_read >= buffer_size - 1) { buffer_size *= 2; char *new_buffer = realloc(buffer, buffer_size); if (!new_buffer) { perror("realloc failed"); free(buffer); close(pipe_fd[0]); waitpid(child_pid, NULL, 0); return NULL; } buffer = new_buffer; } } close(pipe_fd[0]); // 处理读取错误 if (bytes_read < 0) { perror("read failed"); free(buffer); waitpid(child_pid, NULL, 0); return NULL; } // 等待子进程结束,回收资源 int status; waitpid(child_pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { fprintf(stderr, "child process exited with error\n"); free(buffer); return NULL; } // 添加字符串结束符,可选去除末尾换行 buffer[total_read] = '\0'; if (total_read > 0 && buffer[total_read - 1] == '\n') { buffer[total_read - 1] = '\0'; } return buffer; } // 测试示例 int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "usage: %s <filename>\n", argv[0]); exit(EXIT_FAILURE); } char *cksum_result = get_cksum(argv[1]); if (cksum_result) { printf("cksum result: %s\n", cksum_result); free(cksum_result); } else { fprintf(stderr, "failed to get cksum\n"); exit(EXIT_FAILURE); } return EXIT_SUCCESS; }
关键实现说明
管道重定向逻辑:
- 子进程先关闭管道读端,仅保留写端用于输出;
- 通过
dup2将标准输出绑定到管道写端后,立即关闭原管道写端,避免冗余文件描述符; - 父进程创建子进程后马上关闭管道写端,确保子进程退出后管道写端完全关闭,父进程读取时能正确触发EOF。
缓冲区处理:
- 使用动态扩容的缓冲区,确保能完整读取
cksum的输出内容; - 循环读取管道直到EOF,避免单次读取长度不足导致的内容截断。
- 使用动态扩容的缓冲区,确保能完整读取
资源与错误管理:
- 每个系统调用都添加错误检查,通过
perror输出清晰的错误信息; - 确保所有文件描述符被正确关闭,避免资源泄漏;
- 父进程通过
waitpid回收子进程资源,避免僵尸进程; - 检查子进程退出状态,确保
cksum命令执行成功。
- 每个系统调用都添加错误检查,通过
内容的提问来源于stack exchange,提问作者Samuel H Page
相关产品推荐
相关产品推荐

