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

C实现迷你Shell管道时的子进程等待异常问题

管道命令异常等待问题

我在用C语言开发迷你Shell时遇到管道功能异常:当管道中包含无stdout输出且需要等待的进程时,后续命令会被强制等待。比如执行sleep 5 | echo "Hello World",会先等待5秒才输出Hello World——这不符合预期,正常情况下echo应该立即执行输出,无需等待sleep结束。

问题根源分析

当前代码的核心问题并非"判断前序命令是否输出到stdout",而是进程等待时机错误:父进程每创建一个子进程就立即用waitpid等待它结束,导致管道中的命令被串行执行,而非并行。比如sleep进程被创建后,父进程直接阻塞等待其退出,echo进程要等sleep结束才会被创建,最终造成整体阻塞。

解决方案

  1. 先批量创建管道中所有子进程,让它们并行运行
  2. 所有子进程创建完成后,再统一等待所有子进程结束(非后台任务场景)
  3. 严格管理管道文件描述符,避免泄漏导致的额外阻塞

修改后的代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include "variante.h"
#include <sys/wait.h>
#include "readcmd.h"
#include <stdbool.h>

void execute_single_command(char **cmd){
    execvp(cmd[0], cmd);
    // 处理execvp执行失败的情况,避免子进程残留运行
    perror("execvp failed");
    exit(EXIT_FAILURE);
}

void execute_pipeline(struct cmdline *l){
    int previous_pipe_read = -1;
    int file_descriptor[2];
    // 保存所有子进程PID,用于后续统一等待
    pid_t *child_pids = NULL;
    size_t pid_count = 0;

    for(size_t i=0 ; l->seq[i] != NULL; i++){
        if (pipe(file_descriptor) == -1) {
            perror("pipe error");
            exit(EXIT_FAILURE);
        }

        pid_t child_pid = fork();
        char** cmd = l->seq[i];

        if (child_pid == -1) {
            perror("fork error");
            exit(EXIT_FAILURE);
        }

        if(child_pid == 0){
            // 子进程:配置管道重定向
            if (previous_pipe_read != -1) {
                dup2(previous_pipe_read, STDIN_FILENO);
                close(previous_pipe_read);
            }

            if (l->seq[i + 1] != NULL) {
                dup2(file_descriptor[1], STDOUT_FILENO);
            }

            // 关闭当前管道的两端,子进程无需保留这些描述符
            close(file_descriptor[0]);
            close(file_descriptor[1]);

            // 执行命令
            execute_single_command(cmd);
        } else {
            // 父进程:清理无用管道描述符,保存子进程PID
            close(file_descriptor[1]);
            if (previous_pipe_read != -1) {
                close(previous_pipe_read);
            }

            // 动态扩容保存PID的数组
            pid_t *temp = realloc(child_pids, (pid_count + 1) * sizeof(pid_t));
            if (!temp) {
                perror("realloc failed");
                exit(EXIT_FAILURE);
            }
            child_pids = temp;
            child_pids[pid_count++] = child_pid;

            previous_pipe_read = file_descriptor[0];
        }
    }

    // 关闭最后一个管道的读端,避免资源泄漏
    if (previous_pipe_read != -1) {
        close(previous_pipe_read);
    }

    // 非后台任务时,统一等待所有子进程结束
    if (!l->bg && child_pids != NULL) {
        for (size_t i = 0; i < pid_count; i++) {
            int status;
            waitpid(child_pids[i], &status, 0);
        }
        free(child_pids);
    }
}

int main()
{
//  unrelated code

    while (1)
    {
        struct cmdline *l;
        char *line = 0;
        char *prompt = "ensishell>";

        /* Readline use some internal memory structure that
           can not be cleaned at the end of the program. Thus
           one memory leak per command seems unavoidable yet */
        line = readline(prompt);
        if (line == 0 || !strncmp(line, "exit", 4))
        {
            terminate(line);
        }

#if USE_GNU_READLINE == 1
        add_history(line);
#endif

#if USE_GUILE == 1
        /* The line is a scheme command */
        if (line[0] == '(')
        {
            char catchligne[strlen(line) + 256];
            sprintf(catchligne, "(catch #t (lambda () %s) (lambda (key . parameters) (display \"mauvaise expression/bug en scheme\n\")))\n", line);
            scm_eval_string(scm_from_locale_string(catchligne));
            free(line);
            continue;
        }
#endif

        /* parsecmd free line and set it up to 0 */
        l = parsecmd(&line);

        /* If input stream closed, normal termination */
        if (!l)
        {
            terminate(0);
        }

        if (l->err)
        {
            /* Syntax error, read another command */
            printf("error: %s\n", l->err);
            continue;
        }

        if (l->in)
            printf("in: %s\n", l->in);
        if (l->out)
            printf("out: %s\n", l->out);
        if (l->bg)
            printf("background (&)\n");

        /* Display each command of the pipe */
        execute_pipeline(l);
    }
}

关键修改点

  • 新增child_pids数组保存所有子进程PID,确保管道中所有命令能并行创建
  • 父进程在所有子进程创建完成后再统一等待,而非创建一个就阻塞等待
  • 子进程中关闭管道两端的冗余描述符,避免资源泄漏
  • 修复execute_single_command的错误处理逻辑,避免execvp失败后子进程残留
  • 将循环条件l->seq[i] != 0改为l->seq[i] != NULL,符合C语言指针规范

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 11:01:26