C中的这个多管道代码是否有意义?

几天我就提出了一个问题 。 我的解决方案符合接受的答案中的建议。 但是,我的一个朋友提出了以下解决方案:

请注意,代码已更新几次(查看编辑修订版)以反映下面答案中的建议。 如果您打算给出新的答案,请考虑这个新代码,而不是那些有很多问题的旧代码。

#include  #include  #include  #include  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 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 | cmd2 | …… | cmd_n。 我说“模拟”,因为正如你所看到的,命令实际上是从参数中读取的。 只是为了空闲时间编写一个简单的shell提示符……

当然,有一些问题需要修复,并添加像error handling,但这不是重点。 我觉得我有点得到代码,但它仍然让我很困惑这整个事情的工作原理。

我错过了什么或者这确实有效,这是解决问题的一个很好的清洁解决方案吗? 如果没有,有人能指出这个代码的关键问题吗?

看起来很合理,虽然它确实需要修复泄漏的stdaux给孩子们和循环后,父母的原始stdin永远丢失。

颜色可能会更好……

 ./a.out foo bar baz  stdout
 std = dup(stdout)||  | + ===== STD
                       ||  ||  ||
管(fd)||  ||  pipe1 [0]  -  pipe0 [1] ||
                       ||  ||  ||  ||  ||
 aux = fd [0] ||  ||  aux ||  ||
                       ||  XX ||  ||  ||
                       ||  / ------- ++ ---------- + |  ||
 dup2(fd [1],1)||  // ||  ||  ||
                       ||  ||  ||  ||  ||
关闭(fd [1])||  ||  ||  XX ||
                       ||  ||  ||  ||
 fork + exec(foo)||  ||  ||  ||
                       XX ||  ||  ||
                        / ----- ------- ++ + |  ||
 dup2(aux,0)// ||  ||  ||
                       ||  ||  ||  ||
关闭(辅助)||  ||  XX ||
                       ||  ||  ||
管(fd)||  ||  pipe2 [0]  -  pipe2 [1] ||
                       ||  ||  ||  ||  ||
 aux = fd [0] ||  ||  aux ||  ||
                       ||  XX ||  ||  ||
                       ||  / ------- ++ ---------- + |  ||
 dup2(fd [1],1)||  // ||  ||  ||
                       ||  ||  ||  ||  ||
关闭(fd [1])||  ||  ||  XX ||
                       ||  ||  ||  ||
 fork + exec(bar)||  ||  ||  ||
                       XX ||  ||  ||
                        / ----- ------- ++ + |  ||
 dup2(aux,0)// ||  ||  ||
                       ||  ||  ||  ||
关闭(辅助)||  ||  XX ||
                       ||  ||  ||
管(fd)||  ||  pipe3 [0]  -  pipe3 [1] ||
                       ||  ||  ||  ||  ||
 aux = fd [0] ||  ||  aux ||  ||
                       ||  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

我的建议不同之处在于它避免了修改父级的stdinstdout ,只是在子级内部操纵它们,并且永远不会泄漏任何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:stdin,1:stdout}

 cmd = cmds [0] {
    有一个下一个cmd {
        管(new_fds)
             new_fds = {3,4}
             fds = {0:stdin,1:stdout,3:pipe1 [0],4:pipe1 [1]}
     }

     fork => child
                        有一个下一个cmd {
                            关闭(new_fds [0])
                                 fds = {0:stdin,1:stdout,4:pipe1 [1]}
                             dup2(new_fds [1],1)
                                 fds = {0:stdin,1:pipe1 [1],4:pipe1 [1]}
                            关闭(new_fds [1])
                                 fds = {0:stdin,1:pipe1 [1]}
                         }
                         EXEC(CMD)

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

 cmd = cmds [1] {
    有一个下一个cmd {
        管(new_fds)
             new_fds = {5,6}
             fds = {0:stdin,1:stdout,3:pipe1 [0],4:pipe1 [1],
                                         5:pipe2 [0],6:pipe2 [1]}
     }

     fork => child
                        有一个以前的cmd {
                             dup2(old_fds [0],0)
                                 fds = {0:pipe1 [0],1:stdout,
                                        3:pipe1 [0],4:pipe1 [1],
                                        5:pipe2 [0],6:pipe2 [1]}
                            关闭(old_fds [0])
                                 fds = {0:pipe1 [0],1:stdout,
                                                     4:pipe1 [1],
                                        5:pipe2 [0] 6:pipe2 [1]}
                            关闭(old_fds [1])
                                 fds = {0:pipe1 [0],1:stdout,
                                        5:pipe2 [0],6:pipe2 [1]}
                         }
                        有一个下一个cmd {
                            关闭(new_fds [0])
                                 fds = {0:pipe1 [0],1:stdout,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]}
                         }
                         EXEC(CMD)

    有一个以前的cmd {
        关闭(old_fds [0])
             fds = {0:stdin,1:stdout,4:pipe1 [1],
                                         5:pipe2 [0],6:pipe2 [1]}
        关闭(old_fds [1])
             fds = {0:stdin,1:stdout,5:pipe2 [0],6:pipe2 [1]}
     }

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

 cmd = cmds [2] {
     fork => child
                        有一个以前的cmd {
                             dup2(old_fds [0],0)
                                 fds = {0:pipe2 [0],1:stdout,
                                        5:pipe2 [0],6:pipe2 [1]}
                            关闭(old_fds [0])
                                 fds = {0:pipe2 [0],1:stdout,
                                                     6:pipe2 [1]}
                            关闭(old_fds [1])
                                 fds = {0:pipe2 [0],1:stdout}
                         }
                         EXEC(CMD)

    有一个以前的cmd {
        关闭(old_fds [0])
             fds = {0:stdin,1:stdout,6:pipe2 [1]}
        关闭(old_fds [1])
             fds = {0:stdin,1:stdout}
     }
 }

编辑

您更新的代码确实修复了以前的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]; } } 

你可以看到Bash的execute_cmd.c#execute_disk_command是从execute_cmd.c#execute_pipeline调用的jobs.c#job_runjobs.c#job_runprocess.c#process_run是从jobs.c#job_run ,甚至是每一个BusyBox的各种 小 而 小的 贝壳将它们分开。

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


考虑第一个命令可用的文件描述符(假设至少有两个 – 一般应该处理的东西(只需要一个命令就不需要pipe()或I / O重定向),但我认识到error handling被消除了保持代码适合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未在循环内初始化。

一般来说,这接近正确 – 但不太正确。

它会给出结果,有些是不期望的。 它远非一个很好的解决方案:它与父进程的标准描述符混淆,不恢复标准输入,描述符泄露给儿童等。

如果你以递归方式思考,可能会更容易理解。 下面是一个正确的解决方案,没有错误检查。 考虑一个链表类型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。 它完成剩下的工作。

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

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

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

 #include  #include  #include  #include  #include  #include  #include  #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; } 

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

无论如何,你怎么看待这个?

这是我的“最终”代码与流行的建议:

 #include  #include  #include  #include  #include  #include  #include  #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; } 

现在好吗?