【问题标题】:Implementing shell in C - pipelined input has correct output but exits loop在 C 中实现 shell - 流水线输入具有正确的输出但退出循环
【发布时间】:2018-11-19 07:23:51
【问题描述】:

我正在尝试在 C 中实现一个处理多个管道的基本 shell。它等待输入并在 for 循环中执行命令。当它收到EOF时,它停止等待输入并退出。

现在,当我输入流水线命令时,我的 shell 会输出正确的输出,例如ls | wc | grep ...它停止等待输入并退出外部 while 循环而不是等待下一行输入。

我发现发生这种情况是因为我的 while 循环中的 fgets 返回 null(stdin 以某种方式获取 EOF?)。在创建分叉、创建管道或执行时,我没有收到任何错误。

但是,如果我一次输入一个命令而没有任何管道,例如ls,它成功打印出正确的输出等待下一行输入,因为它应该。

我的程序在尝试执行每个命令之前将每一行输入解析为struct(以下省略)。 struct 的设计目的是让我可以轻松地将解析后的参数传递给execvp,这里不再赘述。

这是我的代码的高度简化版本,省略了大部分错误检查:

FILE* input;
char line[MAX_LINE];

input = stdin;
printf("> ");
fflush(stdout);

while (fgets(line, sizeof(line), input)) {
    int i;
    struct cmdLine;
    /* struct defined elsewhere
    ** commands = # of commands in parsed input
    ** start = index where a command and its args start
    ** args[] = array holding each command/arg
    */

    /* parse input line into cmdLine */
    ...

    /* exec all commands in pipeline except the last */
    for (i = 0; i < cmdLine.commands-1; ++i) {
        int pd[2];
        pipe(pd);

        if (fork() == 0) {
            dup2(pd[1], 1);
            execvp(cmdLine.args[cmdLine.start[i]], &(cmdLine.args[cmdLine.start[i]]));
        } else {
            wait(NULL);
        }

        dup2(pd[0], 0);
        close(pd[1]);
    }    

    /* exec last command */
    if (fork() == 0) {
        execvp(cmdLine.args[cmdLine.start[i]], &(cmdLine.args[cmdLine.start[i]]));
    } else {
        wait(NULL);
    }

    if (stdin == input) {
        printf("> ");  /* print shell prompt */
        fflush(stdout);
    }
}

我几乎可以肯定我的欺骗行为在某个地方搞砸了,但我已经尝试了几个小时,但我不明白我做错了什么。 EOF 是否以某种方式发送到标准输入,所以封闭的fgets 返回NULL

【问题讨论】:

  • "这是我的代码的高度简化版本,省略了大部分错误检查" - 那么为什么要发布它呢?见minimal reproducible example
  • 你试过在你的程序上运行strace -f吗?
  • 因为如果我发布了我的整个代码,那么人们想要阅读的内容太多了?这已经足够简化,因此任何人都可以看到我的方法而不会省略重要的行(fork、pipe、dup2)。
  • 如果你不知道bug在哪里,你怎么知道哪些行重要哪些不重要?
  • 因为我得到了处理除这部分代码之外的所有内容的代码,我保证这是一个正确的解决方案。我知道错误在于这部分,它与我调用dup2fork 和/或pipe有关。

标签: c shell pipe fork dup2


【解决方案1】:

通过使用 0 (=stdin) 作为第二个参数调用 dup2,您将在 for 循环的每次迭代结束时关闭原始 stdin,因此您无法再与您的程序通过原始标准输入。

您的代码中的问题是您试图将所有管道连接在一起交给其他人;那是行不通的。以下是应该起作用的:

  • 对于 n 个程序,您至少需要 (n-1) 个管道。
  • 在数组中记录所有管道 FD:一个用于管道的输入端(写入),一个用于输出端(读取)。
  • 对于您要分叉的每个进程,将前一个管道的输出(如果有)连接到它的stdin,并将下一个管道的输入连接到它的stdout(或者如果您正在处理最后一个,则连接到您的主程序的stdout在您的管道链中处理)。
  • 一旦你分叉了所有东西:在一个循环中,poll() 在你的管道的输出 FD 上,从任何有活动的管道中读取,并写入下一个管道的输入(最后是你自己的 stdout )。如果您在其中一个管道上获得 EOF,请关闭下一个管道的输入(并从输出数组中删除 EOF 的管道输出)。关闭所有 FD 后,退出循环。

编辑:我只是想到了另一种更简单的方法,它需要更少的代码更改,但我还没有完全考虑清楚。 :) 问题是您正在破坏自己的标准输入。如果您在分叉的子进程中执行所有这些操作(即整个“处理一行命令),则在进程之间替换标准输入根本不会影响父进程。不过,这将需要在内核中进行大量缓冲,因此它可能无法扩展。

【讨论】:

  • 非常感谢您实际回答我的问题 - 这更有意义。我在这里看到了一个例子,这个人只使用了 2 个管道来遍历每个命令,所以我认为这样做会更“干净”。
  • 我确信有一些方法可以用两个管道来完成。 :) - 但实际上你正在为每个进程创建一个新管道,你只是没有记住所有这些。但是,如果没有大量数据通过管道,您完全可以在没有 poll 的情况下完成此操作。在这种情况下,您可以从每个进程的输出中读取所有内容并将其写入输入,然后再分叉下一个。不过,在现实世界中,您会遇到缓冲区限制。
  • @itspiuy 实际上我只是想到了一种可能使它更容易的方法:在一个分叉的孩子中完成上述所有工作(即围绕处理一行命令的整个代码部分的另一个分叉)。这样原始标准输入就不会在父进程中消失。
  • 刚刚尝试了“简单”的修复方法——效果很好。谢谢。
  • 另外,你是对的,从技术上讲,我每次遍历 for 循环时都会创建一个新管道;我的意思是说制作一个巨大的 fd 数组,这有点“丑陋”。我从早些时候阅读其他一些帖子中意识到,更好的方法是不要等待每个过程完成 - 您使用 poll 的建议可能会更加现实。幸运的是,这是一个简单的练习题,所以我不必担心。再次感谢您的帮助!
猜你喜欢
  • 2015-02-01
  • 2014-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-11
  • 2021-03-02
  • 1970-01-01
相关资源
最近更新 更多