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

使用exec()、fork()、pipe()实现Bash管道时的进程冻结问题排查

修复C语言匿名管道实现Bash管道的冻结问题

咱们直接定位你遇到的进程冻结问题——根源在于管道的文件描述符没有被正确关闭,再加上子进程的启动与等待逻辑顺序有误,导致读进程一直阻塞等待数据。

核心问题拆解

  1. 主进程未关闭管道两端:你在main里创建管道后,自己一直攥着pipeFileDesc[READ]和pipeFileDesc[WRITE]这两个文件描述符。当wc读完ls的输出后,它会一直等管道的所有写端都关闭才会结束,但主进程还没关写端,导致wc卡在读取操作上,进程直接冻住。
  2. 子进程等待时机错误:你先启动ls并等待它跑完,再启动wc。这不仅不符合管道命令并发执行的逻辑,还会让主进程长时间持有管道描述符,加重阻塞问题。
  3. 子进程管道处理的小疏漏:比如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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 07:22:15