使用exec()、fork()、pipe()实现Bash管道时的进程冻结问题排查
修复C语言匿名管道实现Bash管道的冻结问题
咱们直接定位你遇到的进程冻结问题——根源在于管道的文件描述符没有被正确关闭,再加上子进程的启动与等待逻辑顺序有误,导致读进程一直阻塞等待数据。
核心问题拆解
- 主进程未关闭管道两端:你在
main里创建管道后,自己一直攥着pipeFileDesc[READ]和pipeFileDesc[WRITE]这两个文件描述符。当wc读完ls的输出后,它会一直等管道的所有写端都关闭才会结束,但主进程还没关写端,导致wc卡在读取操作上,进程直接冻住。 - 子进程等待时机错误:你先启动
ls并等待它跑完,再启动wc。这不仅不符合管道命令并发执行的逻辑,还会让主进程长时间持有管道描述符,加重阻塞问题。 - 子进程管道处理的小疏漏:比如
LEFT_PIPE模式下,子进程关了读端,但主进程还拿着这个描述符;另外,子进程完成dup2重定向后,必须关闭原管道描述符,避免资源泄漏。
修复后的完整代码实现
修改后的Main.c
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include "routines.h" #define READ 0 // 管道读端索引 #define WRITE 1 // 管道写端索引 typedef enum eMode{ LEFT_PIPE=0, // 管道左端命令(如ls -l | ...) CENTER_PIPE=1, // 管道中间命令(如... | cmd | ...) RIGHT_PIPE=2, // 管道右端命令(如... | wc) NORMAL_COMMAND=3 // 无管道的普通命令 } Mode; int main() { char * commandTest1[]={"ls","-l",NULL}; char * commandTest2[]={"wc",NULL}; int pipeFileDesc[2]; // 管道创建失败的容错处理 if (pipe(pipeFileDesc) == -1) { perror("pipe creation failed"); exit(EXIT_FAILURE); } // 先启动左端命令,父进程不等待,继续启动右端命令 pid_t ls_pid = executeCommand(commandTest1, pipeFileDesc[READ], pipeFileDesc[WRITE], LEFT_PIPE); pid_t wc_pid = executeCommand(commandTest2, pipeFileDesc[READ], pipeFileDesc[WRITE], RIGHT_PIPE); // 主进程必须关闭管道两端!这是解决冻结的关键! close(pipeFileDesc[READ]); close(pipeFileDesc[WRITE]); // 统一等待两个子进程结束 waitpid(ls_pid, NULL, 0); waitpid(wc_pid, NULL, 0); return 0; }
修改后的executeCommand(Routines.c)
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <errno.h> int executeCommand(char** args, int inputDataFD, int outputDataFD, int mode){ pid_t pid = fork(); if(pid == -1){ perror("Fork Error"); return -1; } if(pid == 0){ // 子进程逻辑 printf("Child process: pid %d, father pid %d\n", getpid(), getppid()); switch(mode){ case LEFT_PIPE: // 左端命令:标准输出重定向到管道写端,不需要读管道 close(inputDataFD); // 关闭无用的读端 dup2(outputDataFD, STDOUT_FILENO); close(outputDataFD); // dup2后原描述符可关闭 break; case CENTER_PIPE: // 中间命令:标准输入从管道读端来,标准输出到管道写端 dup2(inputDataFD, STDIN_FILENO); close(inputDataFD); dup2(outputDataFD, STDOUT_FILENO); close(outputDataFD); break; case RIGHT_PIPE: // 右端命令:标准输入从管道读端来,不需要写管道 close(outputDataFD); // 关闭无用的写端 dup2(inputDataFD, STDIN_FILENO); close(inputDataFD); break; case NORMAL_COMMAND: // 普通命令,无需重定向 break; } // 执行命令,若返回则说明执行失败 execvp(args[0], args); perror("Exec error"); exit(EXIT_FAILURE); } else { // 父进程直接返回子进程PID,不在这里等待 printf("Parent process: pid %d, child pid %d\n", getpid(), pid); return pid; } }
关键修复点说明
- 主进程关闭管道两端:主进程只负责把管道描述符传给子进程,自己不需要使用管道,必须关闭两端。这样当所有子进程的写端都关闭时,读进程(比如
wc)才会知道没有更多数据,正常结束读取操作。 - 调整等待时机:父进程先启动所有子进程,再统一等待它们结束,符合管道命令并发执行的逻辑,避免主进程长时间持有管道描述符。
- 子进程清理管道描述符:子进程完成
dup2重定向后立即关闭原管道描述符,避免资源占用,同时保证管道的引用计数正确。 - 使用标准宏替代魔法数字:用
STDIN_FILENO/STDOUT_FILENO代替0和1,代码更易读且符合规范。
扩展支持多管道串联
如果要实现cmd1 | cmd2 | cmd3这类多管道场景,可以用循环创建管道:
- 第一个命令用
LEFT_PIPE模式,连接到第一个管道的写端; - 中间命令用
CENTER_PIPE模式,前一个管道的读端作为输入,当前管道的写端作为输出; - 最后一个命令用
RIGHT_PIPE模式,连接到最后一个管道的读端; - 每个管道创建后,主进程在传递给子进程后及时关闭不需要的描述符。
内容的提问来源于stack exchange,提问作者Jhonathan Asimov
相关产品推荐
相关产品推荐

