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

C语言实现简易Bash时如何获取子Shell命令输出供管道使用

C实现简易Bash子Shell管道死循环问题修复

你遇到的wc/grep/head类命令触发死循环,本质是管道写端文件描述符(fd)泄漏+子Shell输出重定向逻辑错误,和输入处理逻辑本身无关。
ls/ps/echo这类命令执行完会直接退出,内核会自动回收进程持有的所有fd,因此不会触发问题;而wc/grep这类需要持续读取标准输入直到EOF的命令,只要管道写端还有任意一个进程持有打开状态,就永远收不到结束标识,会一直阻塞读取,最终表现为死循环。

现有代码的3个明确缺陷

  • 子Shell分支重复持有管道写端:子进程中已经通过dup2(pipes[i + 1], STDOUT_FILENO)将标准输出重定向到管道写端,又额外执行shell->fd = pipes[i + 1]给子Shell传递fd,等于子Shell进程额外持有了一份管道写端引用。就算子Shell内的命令执行完成,这份未关闭的fd会导致管道永远不会触发EOF,下游读端命令会持续阻塞。
  • 子进程回收逻辑错误:waitpid循环仅传入最后一次fork返回的pid,前面所有fork出的子进程没有被回收,这些僵尸进程仍然持有管道读写端的fd引用,同样会导致管道无法正常关闭触发EOF。同时循环体漏写大括号,退出码赋值逻辑不在循环内,永远只能拿到最后一个子进程的退出状态。
  • 子进程执行流泄漏:ft_subshell执行完成后,子进程没有调用exit()终止,会跑回父进程的执行逻辑继续fork、执行后续命令,直接打乱整个管道的进程拓扑,导致fd引用计数完全混乱。

可直接落地的修复方案

核心原则:所有进程(父进程+所有子进程)在完成dup2重定向后,必须立刻关闭管道数组中的所有原始fd,不能保留任何额外的管道fd引用——只有当管道写端的引用计数降到0时,读端才能收到EOF。
修复后的核心管道执行代码如下:

void ft_exec_pipe(t_shell *shell, int nb_pipe)
{
    int     i;
    int     j;
    int     pipes[2 * nb_pipe];
    int     status;
    pid_t   pid;
    pid_t   child_pids[nb_pipe + 1]; // 存储所有子进程pid用于回收
    int     cmd_idx = 0;
    t_token *current_token = shell->token; // 暂存指针初始位置,避免偏移错误
    t_op    *current_op = shell->operator;
    
    i = 0;
    j = nb_pipe + 1;
    open_pipe(pipes, nb_pipe);
    while (current_token && j-- > 0)
    {
        pid = fork();
        child_pids[cmd_idx] = pid;
        if (pid == 0)
        {
            // 管道重定向
            if (*(current_token + 1) != NULL)
                dup2(pipes[i + 1], STDOUT_FILENO);
            if (i != 0)
                dup2(pipes[i - 2], STDIN_FILENO);
            // 重定向完成后立刻关闭所有管道原始fd,不保留多余引用
            close_pipe(pipes, nb_pipe);
            if (ft_is_subshell(current_token))
            {
                // 移除shell->fd赋值逻辑,子Shell直接使用已重定向的STDOUT_FILENO即可
                ft_subshell(shell, current_token);
                // 子Shell执行完立刻退出子进程,避免流回父进程逻辑
                exit(WEXITSTATUS(shell->exitstatus));
            }
            else
                ft_exec_cmd_fork(shell);
            exit(127); // 命令执行失败兜底退出
        }
        i += 2;
        current_token++;
        cmd_idx++;
        if (j)
            current_op++;
    }
    // 父进程fork完所有子进程后,立刻关闭自己持有的所有管道fd
    close_pipe(pipes, nb_pipe);
    // 逐个回收所有子进程,取管道最后一个命令的退出码作为整条管道的退出码(与Bash原生逻辑一致)
    for (int k = 0; k < cmd_idx; k++)
    {
        waitpid(child_pids[k], &status, 0);
        if (k == cmd_idx - 1)
        {
            shell->exitstatus = WEXITSTATUS(status);
            shell->prev_exitstatus = shell->exitstatus;
        }
    }
    // 恢复shell结构体原指针位置
    shell->token = current_token;
    shell->operator = current_op;
}

关键修改点说明

  • 移除子Shell分支多余的fd传参:子进程已经完成标准输出重定向,子Shell内执行的所有命令默认输出到stdout就会自动写入管道,不需要额外操作fd。
  • 所有子进程执行完对应逻辑后必须调用exit()终止,绝对不能让子进程执行流回到父进程的代码路径。
  • 父进程fork完所有子进程后第一时间关闭自身持有的管道fd:这是最容易遗漏的点,父进程作为打开管道的进程,同样持有管道的读写端引用,如果不关闭,就算所有子进程都释放了写端,父进程持有的引用仍然会导致下游读命令收不到EOF。
  • 存储所有子进程的pid逐个回收,避免僵尸进程残留持有fd引用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 21:27:22