2

几天来我已经提出了一个关于这个的问题。我的解决方案与已接受答案中建议的内容一致。但是,我的一个朋友提出了以下解决方案:

请注意,代码已经更新了几次(检查编辑修订)以反映以下答案中的建议。如果你打算给出一个新的答案,请记住这个新代码,而不是有很多问题的旧代码。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    int fd[2], i, aux, std0, std1;

    do {
        std0 = dup(0); // backup stdin
        std1 = dup(1); // backup stdout

        // let's pretend I'm reading commands here in a shell prompt
        READ_COMMAND_FROM_PROMPT();

        for(i=1; i<argc; i++) {
            // do we have a previous command?
            if(i > 1) {
                dup2(aux, 0);
                close(aux);
            }

            // do we have a next command?
            if(i < argc-1) {
                pipe(fd);

                aux = fd[0];
                dup2(fd[1], 1);
                close(fd[1]);
            }

            // last command? restore stdout...
            if(i == argc-1) {
                dup2(std1, 1);
                close(std1);
            }

            if(!fork()) {
                // if not last command, close all pipe ends
                // (the child doesn't use them)
                if(i < argc-1) {
                    close(std0);
                    close(std1);
                    close(fd[0]);
                }

                execlp(argv[i], argv[i], NULL);
                exit(0);
            }
        }

        // restore stdin to be able to keep using the shell
        dup2(std0, 0);
        close(std0);
    }

    return 0;
}

这会通过 bash 中的管道模拟一系列命令,例如: cmd1 | 命令2 | ... | cmd_n。我说“模拟”,因为如您所见,命令实际上是从参数中读取的。只是为了节省时间编写一个简单的 shell 提示符......

当然,还有一些问题需要修复和添加,比如错误处理,但这不是重点。我想我有点得到代码,但它仍然让我很困惑这整个事情是如何工作的。

我是否遗漏了某些东西,或者这确实有效,并且它是解决问题的好而干净的解决方案?如果没有,谁能指出这段代码存在的关键问题?

4

5 回答 5

8

看起来很合理,尽管它确实需要在循环之后修复泄漏stdaux子级,并且父级的原始stdin文件永远丢失了。

这可能会更好的颜色...

./a.out foo bar baz <stdin >stdout
标准 = dup(标准输出) || |+===========================std
                      || || ||
管道(fd) || || 管道1[0] -- 管道0[1] ||
                      || || || || ||
辅助 = fd[0] || || 辅助 || ||
                      || XX || || ||
                      || /--------++---------+| ||
dup2(fd[1], 1) || // || || ||
                      || || || || ||
关闭(fd[1]) || || || XX ||
                      || || || ||
叉+执行(foo)|| || || ||
                      XX || || ||
                       /-----++-------+| ||
dup2(aux, 0) // || || ||
                      || || || ||
关闭(辅助)|| || XX ||
                      || || ||
管道(fd) || || 管道2[0] -- 管道2[1] ||
                      || || || || ||
辅助 = fd[0] || || 辅助 || ||
                      || XX || || ||
                      || /--------++---------+| ||
dup2(fd[1], 1) || // || || ||
                      || || || || ||
关闭(fd[1]) || || || XX ||
                      || || || ||
叉+执行(酒吧)|| || || ||
                      XX || || ||
                       /-----++-------+| ||
dup2(aux, 0) // || || ||
                      || || || ||
关闭(辅助)|| || XX ||
                      || || ||
管道(fd) || || 管道3[0] -- 管道3[1] ||
                      || || || || ||
辅助 = fd[0] || || 辅助 || ||
                      || XX || || ||
                      || /--------++---------+| ||
dup2(fd[1], 1) || // || || ||
                      || || || || ||
关闭(fd[1]) || || || XX ||
                      || XX || ||
                      || /--------++-----------------+|
dup2(std, 1) || // || ||
                      || || || ||
fork+exec(baz) || || || ||
  • foo得到stdin=stdinstdout=pipe1[1]
  • bar得到stdin=pipe1[0]stdout=pipe2[1]
  • baz得到stdin=pipe2[0]stdout=stdout

我的建议是不同的,因为它避免破坏父母的stdinand stdout,只在孩子内部操纵它们,并且从不泄漏任何 FD。不过,绘制图表有点困难。

for cmd in cmds
    if there is a next cmd
        pipe(new_fds)
    fork
    if child
        if there is a previous cmd
            dup2(old_fds[0], 0)
            close(old_fds[0])
            close(old_fds[1])
        if there is a next cmd
            close(new_fds[0])
            dup2(new_fds[1], 1)
            close(new_fds[1])
        exec cmd || die
    else
        if there is a previous cmd
            close(old_fds[0])
            close(old_fds[1])
        if there is a next cmd
            old_fds = new_fds
父母
    cmds = [foo, bar, baz]
    fds = {0:标准输入,1:标准输出}

cmd = cmds[0] {
    有一个下一个 cmd {
        管道(new_fds)
            new_fds = {3, 4}
            fds = {0:标准输入,1:标准输出,3:管道1 [0],4:管道1 [1]}
    }

    叉子 => 孩子
                        有一个下一个 cmd {
                            关闭(new_fds[0])
                                fds = {0:标准输入,1:标准输出,4:管道1 [1]}
                            dup2(new_fds[1], 1)
                                fds = {0:标准输入,1:管道1 [1],4:管道1 [1]}
                            关闭(new_fds[1])
                                fds = {0:标准输入,1:管道1 [1]}
                        }
                        执行(命令)

    有一个下一个 cmd {
        old_fds = new_fds
            old_fds = {3, 4}
    }
}

cmd = cmds[1] {
    有一个下一个 cmd {
        管道(new_fds)
            new_fds = {5, 6}
            fds = {0:标准输入,1:标准输出,3:管道1 [0],4:管道1 [1],
                                        5:管道2[0],6:管道2[1]}
    }

    叉子 => 孩子
                        有一个以前的 cmd {
                            dup2(old_fds[0], 0)
                                fds = {0:管道1 [0],1:标准输出,
                                       3:管道1[0],4:管道1[1],
                                       5:管道2[0],6:管道2[1]}
                            关闭(old_fds[0])
                                fds = {0:管道1 [0],1:标准输出,
                                                    4:管道1[1],
                                       5:管道2[0] 6:管道2[1]}
                            关闭(old_fds[1])
                                fds = {0:管道1 [0],1:标准输出,
                                       5:管道2[0],6:管道2[1]}
                        }
                        有一个下一个 cmd {
                            关闭(new_fds[0])
                                fds = {0: pipe1[0], 1: 标准输出, 6: pipe2[1]}
                            dup2(new_fds[1], 1)
                                fds = {0: pipe1[0], 1: pipe2[1], 6: pipe2[1]}
                            关闭(new_fds[1])
                                fds = {0: pipe1[0], 1: pipe1[1]}
                        }
                        执行(命令)

    有一个以前的 cmd {
        关闭(old_fds[0])
            fds = {0:标准输入,1:标准输出,4:管道1 [1],
                                        5:管道2[0],6:管道2[1]}
        关闭(old_fds[1])
            fds = {0:标准输入,1:标准输出,5:管道2 [0],6:管道2 [1]}
    }

    有一个下一个 cmd {
        old_fds = new_fds
            old_fds = {3, 4}
    }
}

cmd = cmds[2] {
    叉子 => 孩子
                        有一个以前的 cmd {
                            dup2(old_fds[0], 0)
                                fds = {0:管道2 [0],1:标准输出,
                                       5:管道2[0],6:管道2[1]}
                            关闭(old_fds[0])
                                fds = {0:管道2 [0],1:标准输出,
                                                    6:管道2[1]}
                            关闭(old_fds[1])
                                fds = {0: pipe2[0], 1: 标准输出}
                        }
                        执行(命令)

    有一个以前的 cmd {
        关闭(old_fds[0])
            fds = {0:标准输入,1:标准输出,6:管道2 [1]}
        关闭(old_fds[1])
            fds = {0:标准输入,1:标准输出}
    }
}

编辑

您更新的代码确实修复了以前的 FD 泄漏……但添加了一个:您现在正在泄漏std0给孩子们。正如 Jon 所说,这对大多数程序来说可能并不危险......但你仍然应该编写一个比这更好的 shell。

即使它是暂时的,我强烈建议不要修改你自己的 shell 的标准输入/输出/错误(0/1/2),只在 exec 之前的孩子中这样做。为什么?假设您printf在中间添加了一些调试,或者由于错误情况需要退出。如果您不首先清理混乱的标准文件描述符,您将遇到麻烦。请,为了让事情即使在意外情况下也能按预期运行,在需要之前不要乱搞。


编辑

正如我在其他评论中提到的,将其分成更小的部分使其更容易理解。这个小助手应该易于理解且没有错误:

/* cmd, argv: passed to exec
 * fd_in, fd_out: when not -1, replaces stdin and stdout
 * return: pid of fork+exec child
 */
int fork_and_exec_with_fds(char *cmd, char **argv, int fd_in, int fd_out) {
    pid_t child = fork();
    if (fork)
        return child;

    if (fd_in != -1 && fd_in != 0) {
        dup2(fd_in, 0);
        close(fd_in);
    }

    if (fd_out != -1 && fd_in != 1) {
        dup2(fd_out, 1);
        close(fd_out);
    }

    execvp(cmd, argv);
    exit(-1);
}

应该这样:

void run_pipeline(int num, char *cmds[], char **argvs[], int pids[]) {
    /* initially, don't change stdin */
    int fd_in = -1, fd_out;
    int i;

    for (i = 0; i < num; i++) {
        int fd_pipe[2];

        /* if there is a next command, set up a pipe for stdout */
        if (i + 1 < num) {
            pipe(fd_pipe);
            fd_out = fd_pipe[1];
        }
        /* otherwise, don't change stdout */
        else
            fd_out = -1;

        /* run child with given stdin/stdout */
        pids[i] = fork_and_exec_with_fds(cmds[i], argvs[i], fd_in, fd_out);

        /* nobody else needs to use these fds anymore
         * safe because close(-1) does nothing */
        close(fd_in);
        close(fd_out);

        /* set up stdin for next command */
        fd_in = fd_pipe[0];
    }
}

您可以看到Bashexecute_cmd.c#execute_disk_command调用 from execute_cmd.c#execute_pipelinexshprocess.c#process_run被调用 from ,甚至BusyBox各种小型最小外壳中的jobs.c#job_run每一个都将它们分开。

于 2009-06-04T02:50:01.450 回答
3

关键问题是您创建了一堆管道并且不确保所有末端都正确关闭。如果你创建一个管道,你会得到两个文件描述符;如果你分叉,那么你有四个文件描述符。如果您dup()dup2()管道的一端为标准描述符,则需要关闭管道的两端——至少其中一个关闭必须在 dup() 或 dup2() 操作之后。


考虑第一个命令可用的文件描述符(假设至少有两个 - 通常应该处理的东西(pipe()仅一个命令不需要或 I/O 重定向),但我认识到消除了错误处理以保持适用于SO的代码):

    std=dup(1);    // Likely: std = 3
    pipe(fd);      // Likely: fd[0] = 4, fd[1] = 5
    aux = fd[0];
    dup2(fd[1], 1);
    close(fd[1]);  // Closes 5

    if (fork() == 0) {
         // Need to close: fd[0] aka aux = 4
         // Need to close: std = 3
         close(fd[0]);
         close(std);
         execlp(argv[i], argv[i], NULL);
         exit(1);
    }

请注意,因为fd[0]在子进程中没有关闭,所以子进程永远不会在其标准输入上获得 EOF;这通常是有问题的。的不关闭std是不太重要的。


重新审视修改后的代码(截至 2009-06-03T20:52-07:00)...

假设进程以文件描述符 0、1、2(标准输入、输出、错误)仅打开开始。还假设我们正好有 3 个命令要处理。和以前一样,这段代码用注释写出循环。

std0 = dup(0); // backup stdin - 3
std1 = dup(1); // backup stdout - 4

// Iteration 1 (i == 1)
// We have another command
pipe(fd);   // fd[0] = 5; fd[1] = 6
aux = fd[0]; // aux = 5
dup2(fd[1], 1);
close(fd[1]);       // 6 closed
// Not last command
if (fork() == 0) {
    // Not last command
    close(std1);    // 4 closed
    close(fd[0]);   // 5 closed
    // Minor problemette: 3 still open
    execlp(argv[i], argv[i], NULL);
    }
// Parent has open 3, 4, 5 - no problem

// Iteration 2 (i == 2)
// There was a previous command
dup2(aux, 0);      // stdin now on read end of pipe
close(aux);        // 5 closed
// We have another command
pipe(fd);          // fd[0] = 5; fd[1] = 6
aux = fd[0];
dup2(fd[1], 1);
close(fd[1]);      // 6 closed
// Not last command
if (fork() == 0) {
    // Not last command
    close(std1);   // 4 closed
    close(fd[0]);  // 5 closed
    // As before, 3 is still open - not a major problem
    execlp(argv[i], argv[i], NULL);
    }
// Parent has open 3, 4, 5 - no problem

// Iteration 3 (i == 3)
// We have a previous command
dup2(aux, 0);      // stdin is now read end of pipe 
close(aux);        // 5 closed
// No more commands

// Last command - restore stdout...
dup2(std1, 1);     // stdin is back where it started
close(std1);       // 4 closed

if (fork() == 0) {
    // Last command
    // 3 still open
    execlp(argv[i], argv[i], NULL);
}
// Parent has closed 4 when it should not have done so!!!
// End of loop
// restore stdin to be able to keep using the shell
dup2(std0, 0);
// 3 still open - as desired

因此,所有孩子的原始标准输入都连接为文件描述符 3。这并不理想,尽管它不是可怕的创伤;我很难找到一个重要的情况。

在父级中关闭文件描述符 4 是一个错误 - '读取命令并处理它的下一次迭代将不起作用,因为std1未在循环内初始化。

一般来说,这接近正确 - 但并不完全正确。

于 2009-06-04T02:54:32.567 回答
1

它会给出一些意想不到的结果。这远非一个好的解决方案:它与父进程的标准描述符混淆,不恢复标准输入,描述符泄漏给子进程等。

如果你递归地思考,它可能更容易理解。下面是一个正确的解决方案,没有错误检查。考虑一个链表类型command,它是next指针和一个argv数组。

void run_pipeline(command *cmd, int input) {
  int pfds[2] = { -1, -1 };

  if (cmd->next != NULL) {
    pipe(pfds);
  }
  if (fork() == 0) { /* child */
    if (input != -1) {
      dup2(input, STDIN_FILENO);
      close(input);
    }
    if (pfds[1] != -1) {
      dup2(pfds[1], STDOUT_FILENO);
      close(pfds[1]);
    }
    if (pfds[0] != -1) {
      close(pfds[0]);
    }
    execvp(cmd->argv[0], cmd->argv);
    exit(1);
  }
  else { /* parent */
    if (input != -1) {
      close(input);
    }
    if (pfds[1] != -1) {
      close(pfds[1]);
    }
    if (cmd->next != NULL) {
      run_pipeline(cmd->next, pfds[0]);
    }
  }
}

使用链表中的第一个命令调用它,并且input= -1。它完成了其余的工作。

于 2009-06-04T03:54:28.247 回答
0

在这个问题和另一个问题中(如第一篇文章中所链接),ehemient建议我解决问题,而不会弄乱父文件描述符,正如这个问题中的可能解决方案所证明的那样。

我没有得到他的解决方案,我尝试并尝试理解,但我似乎无法理解。我也尝试在不理解的情况下对其进行编码,但它没有用。可能是因为我没有正确理解它并且无法对其进行编码,它应该已经编码。

无论如何,我尝试使用我从伪代码中理解的一些东西提出我自己的解决方案,并提出了这个:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
#include <readline/readline.h>
#include <readline/history.h>

#define NUMPIPES 5
#define NUMARGS 10

int main(int argc, char *argv[]) {
    char *bBuffer, *sPtr, *aPtr = NULL, *pipeComms[NUMPIPES], *cmdArgs[NUMARGS];
    int aPipe[2], bPipe[2], pCount, aCount, i, status;
    pid_t pid;

    using_history();

    while(1) {
        bBuffer = readline("\e[1;31mShell \e[1;32m# \e[0m");

        if(!strcasecmp(bBuffer, "exit")) {
            return 0;
        }

        if(strlen(bBuffer) > 0) {
            add_history(bBuffer);
        }

        sPtr = bBuffer;
        pCount =0;

        do {
            aPtr = strsep(&sPtr, "|");

            if(aPtr != NULL) {
                if(strlen(aPtr) > 0) {
                    pipeComms[pCount++] = aPtr;
                }
            }
        } while(aPtr);

        cmdArgs[pCount] = NULL;

        for(i = 0; i < pCount; i++) {
            aCount = 0;

            do {
                aPtr = strsep(&pipeComms[i], " ");

                if(aPtr != NULL) {
                    if(strlen(aPtr) > 0) {
                        cmdArgs[aCount++] = aPtr;
                    }
                }
            } while(aPtr);

            cmdArgs[aCount] = NULL;

            // Do we have a next command?
            if(i < pCount-1) {
                // Is this the first, third, fifth, etc... command?
                if(i%2 == 0) {
                    pipe(aPipe);
                }

                // Is this the second, fourth, sixth, etc... command?
                if(i%2 == 1) {
                    pipe(bPipe);
                }
            }

            pid = fork();

            if(pid == 0) {
                // Is this the first, third, fifth, etc... command?
                if(i%2 == 0) {
                    // Do we have a previous command?
                    if(i > 0) {
                        close(bPipe[1]);
                        dup2(bPipe[0], STDIN_FILENO);
                        close(bPipe[0]);
                    }

                    // Do we have a next command?
                    if(i < pCount-1) {
                        close(aPipe[0]);
                        dup2(aPipe[1], STDOUT_FILENO);
                        close(aPipe[1]);
                    }
                }

                // Is this the second, fourth, sixth, etc... command?
                if(i%2 == 1) {
                    // Do we have a previous command?
                    if(i > 0) {
                        close(aPipe[1]);
                        dup2(aPipe[0], STDIN_FILENO);
                        close(aPipe[0]);
                    }

                    // Do we have a next command?
                    if(i < pCount-1) {
                        close(bPipe[0]);
                        dup2(bPipe[1], STDOUT_FILENO);
                        close(bPipe[1]);
                    }
                }

                execvp(cmdArgs[0], cmdArgs);
                exit(1);
            } else {
                // Do we have a previous command?
                if(i > 0) {
                    // Is this the first, third, fifth, etc... command?
                    if(i%2 == 0) {
                        close(bPipe[0]);
                        close(bPipe[1]);
                    }

                    // Is this the second, fourth, sixth, etc... command?
                    if(i%2 == 1) {
                        close(aPipe[0]);
                        close(aPipe[1]);
                    }
                }

                // wait for the last command? all others will run in the background
                if(i == pCount-1) {
                    waitpid(pid, &status, 0);
                }

                // I know they will be left as zombies in the table
                // Not relevant for this...
            }
        }
    }

    return 0;
}

这可能不是最好和最干净的解决方案,但这是我能想到的,最重要的是,我能理解。有一些我不理解的工作,然后我被我的老师评估,我无法向他解释代码在做什么,这有什么好处?

不管怎样,你觉得这个怎么样?

于 2009-06-04T14:40:32.363 回答
0

这是我的“最终”代码,带有短暂的建议:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
#include <readline/readline.h>
#include <readline/history.h>

#define NUMPIPES 5
#define NUMARGS 10

int main(int argc, char *argv[]) {
    char *bBuffer, *sPtr, *aPtr = NULL, *pipeComms[NUMPIPES], *cmdArgs[NUMARGS];
    int newPipe[2], oldPipe[2], pCount, aCount, i, status;
    pid_t pid;

    using_history();

    while(1) {
        bBuffer = readline("\e[1;31mShell \e[1;32m# \e[0m");

        if(!strcasecmp(bBuffer, "exit")) {
            return 0;
        }

        if(strlen(bBuffer) > 0) {
            add_history(bBuffer);
        }

        sPtr = bBuffer;
        pCount = -1;

        do {
            aPtr = strsep(&sPtr, "|");

            if(aPtr != NULL) {
                if(strlen(aPtr) > 0) {
                    pipeComms[++pCount] = aPtr;
                }
            }
        } while(aPtr);

        cmdArgs[++pCount] = NULL;

        for(i = 0; i < pCount; i++) {
            aCount = -1;

            do {
                aPtr = strsep(&pipeComms[i], " ");

                if(aPtr != NULL) {
                    if(strlen(aPtr) > 0) {
                        cmdArgs[++aCount] = aPtr;
                    }
                }
            } while(aPtr);

            cmdArgs[++aCount] = NULL;

            // do we have a next command?
            if(i < pCount-1) {
                pipe(newPipe);
            }

            pid = fork();

            if(pid == 0) {
                // do we have a previous command?
                if(i > 0) {
                    close(oldPipe[1]);
                    dup2(oldPipe[0], 0);
                    close(oldPipe[0]);
                }

                // do we have a next command?
                if(i < pCount-1) {
                    close(newPipe[0]);
                    dup2(newPipe[1], 1);
                    close(newPipe[1]);
                }

                // execute command...
                execvp(cmdArgs[0], cmdArgs);
                exit(1);
            } else {
                // do we have a previous command?
                if(i > 0) {
                    close(oldPipe[0]);
                    close(oldPipe[1]);
                }

                // do we have a next command?
                if(i < pCount-1) {
                    oldPipe[0] = newPipe[0];
                    oldPipe[1] = newPipe[1];
                }

                // wait for last command process?
                if(i == pCount-1) {
                    waitpid(pid, &status, 0);
                }
            }
        }
    }

    return 0;
}

现在好吗?

于 2009-06-04T16:27:10.473 回答