C의 다중 파이프 코드가 의미가 있나요?
문제
나는 며칠 뒤에 이것에 대한 질문이 있어요.내 솔루션은 허용된 답변에서 제안된 내용과 일치합니다.그런데 내 친구가 다음과 같은 해결책을 내놓았습니다.
아래 답변의 제안 사항을 반영하기 위해 코드가 여러 번 업데이트되었습니다(수정 버전 확인).새로운 답변을 제공하려는 경우 문제가 많았던 이전 코드가 아닌 이 새 코드를 염두에 두고 답변해 주시기 바랍니다.
#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 명령 | cmd2 명령 | ...| cmd_n.내가 "시뮬레이트"라고 말한 이유는 보시다시피 명령이 실제로 인수에서 읽혀지기 때문입니다.간단한 쉘 프롬프트를 코딩하는 시간을 절약하기 위해...
물론 오류 처리와 같이 수정하고 추가해야 할 몇 가지 문제가 있지만 여기서는 그게 요점이 아닙니다.코드를 어느 정도 얻은 것 같지만 이 모든 것이 어떻게 작동하는지 여전히 혼란스럽습니다.
내가 뭔가를 놓치고 있는 걸까요? 아니면 이것이 실제로 효과가 있고 문제를 해결하는 훌륭하고 깨끗한 솔루션입니까?그렇지 않다면 누구든지 이 코드에 있는 중요한 문제를 지적해 줄 수 있습니까?
해결책
합리적으로 보이지만 누출 문제를 해결해야 합니다. std
그리고 aux
자식과 루프 이후, 부모의 원본 stdin
영원히 사라졌습니다.
컬러로 하면 더 좋을 것 같은데...
./a.out foo bar baz <stdin >stdout std = dup(stdout) || |+==========================std || || || pipe(fd) || || pipe1[0] -- pipe0[1] || || || || || || aux = fd[0] || || aux || || || XX || || || || /-------++----------+| || dup2(fd[1], 1) || // || || || || || || || || close(fd[1]) || || || XX || || || || || fork+exec(foo) || || || || XX || || || /-----++-------+| || dup2(aux, 0) // || || || || || || || close(aux) || || XX || || || || pipe(fd) || || pipe2[0] -- pipe2[1] || || || || || || aux = fd[0] || || aux || || || XX || || || || /-------++----------+| || dup2(fd[1], 1) || // || || || || || || || || close(fd[1]) || || || XX || || || || || fork+exec(bar) || || || || XX || || || /-----++-------+| || dup2(aux, 0) // || || || || || || || close(aux) || || XX || || || || pipe(fd) || || pipe3[0] -- pipe3[1] || || || || || || aux = fd[0] || || aux || || || XX || || || || /-------++----------+| || dup2(fd[1], 1) || // || || || || || || || || close(fd[1]) || || || XX || || XX || || || /-------++-----------------+| dup2(std, 1) || // || || || || || || fork+exec(baz) || || || ||
foo
얻다stdin=stdin
,stdout=pipe1[1]
bar
얻다stdin=pipe1[0]
,stdout=pipe2[1]
baz
얻다stdin=pipe2[0]
,stdout=stdout
내 제안은 부모의 내용을 망가뜨리는 것을 방지한다는 점에서 다릅니다. stdin
그리고 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
parent cmds = [foo, bar, baz] fds = {0: stdin, 1: stdout} cmd = cmds[0] { there is a next cmd { pipe(new_fds) new_fds = {3, 4} fds = {0: stdin, 1: stdout, 3: pipe1[0], 4: pipe1[1]} } fork => child there is a next cmd { close(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]} close(new_fds[1]) fds = {0: stdin, 1: pipe1[1]} } exec(cmd) there is a next cmd { old_fds = new_fds old_fds = {3, 4} } } cmd = cmds[1] { there is a next cmd { pipe(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 there is a previous cmd { dup2(old_fds[0], 0) fds = {0: pipe1[0], 1: stdout, 3: pipe1[0], 4: pipe1[1], 5: pipe2[0], 6: pipe2[1]} close(old_fds[0]) fds = {0: pipe1[0], 1: stdout, 4: pipe1[1], 5: pipe2[0] 6: pipe2[1]} close(old_fds[1]) fds = {0: pipe1[0], 1: stdout, 5: pipe2[0], 6: pipe2[1]} } there is a next cmd { close(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]} close(new_fds[1]) fds = {0: pipe1[0], 1: pipe1[1]} } exec(cmd) there is a previous cmd { close(old_fds[0]) fds = {0: stdin, 1: stdout, 4: pipe1[1], 5: pipe2[0], 6: pipe2[1]} close(old_fds[1]) fds = {0: stdin, 1: stdout, 5: pipe2[0], 6: pipe2[1]} } there is a next cmd { old_fds = new_fds old_fds = {3, 4} } } cmd = cmds[2] { fork => child there is a previous cmd { dup2(old_fds[0], 0) fds = {0: pipe2[0], 1: stdout, 5: pipe2[0], 6: pipe2[1]} close(old_fds[0]) fds = {0: pipe2[0], 1: stdout, 6: pipe2[1]} close(old_fds[1]) fds = {0: pipe2[0], 1: stdout} } exec(cmd) there is a previous cmd { close(old_fds[0]) fds = {0: stdin, 1: stdout, 6: pipe2[1]} close(old_fds[1]) fds = {0: stdin, 1: stdout} } }
편집하다
업데이트된 코드는 이전 FD 누출을 수정했지만… 하나를 추가했습니다.너 지금 새고 있어 std0
아이들에게.Jon이 말했듯이 이것은 아마도 대부분의 프로그램에 위험하지 않을 것입니다 ...하지만 여전히 이것보다 더 나은 동작 쉘을 작성해야 합니다.
비록 그것이 임시적이라 할지라도, 나는 여러분 자신의 쉘의 표준 in/out/err(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];
}
}
너는 볼 수있어 세게 때리다'에스 execute_cmd.c#execute_disk_command
에서 전화를 받고 execute_cmd.c#execute_pipeline
, xsh'에스 process.c#process_run
에서 전화를 받고 jobs.c#job_run
, 그리고 심지어 BusyBox'에스 다양한 작은 그리고 최소한의 껍질 그들을 분할합니다.
다른 팁
주요 문제는 많은 파이프를 만들고 모든 끝이 올바르게 닫혀 있는지 확인하지 않는다는 것입니다. 파이프를 만들면 두 개의 파일 설명자가 얻을 수 있습니다. 포크가 있으면 네 개의 파일 설명자가 있습니다. 만약 너라면 dup()
또는 dup2()
파이프의 한쪽 끝은 표준 설명 자로 이루어지면 파이프의 양쪽 끝을 닫아야합니다. 최소한 하나는 DUP () 또는 dup2 () 작동 후에야 닫기 중 하나 이상이어야합니다.
첫 번째 명령에 사용할 수있는 파일 설명자를 고려하십시오 (일반적으로 처리해야 할 것입니다. pipe()
또는 하나의 명령으로 필요한 I/O 리디렉션), 그러나 코드를 적합하게 유지하기 위해 오류 처리가 제거되었음을 알고 있습니다.)
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
따라서 모든 어린이는 원래 표준 입력을 파일 설명 자로 연결합니다. 이것은 이상적이지는 않지만 이상적이지는 않지만 이상적이지는 않습니다. 나는 이것이 중요한 상황을 찾기가 힘들다.
파일 설명자를 닫는다 4 부모의 실수입니다. std1
루프 내부에서 초기화되지 않습니다.
일반적으로 이것은 정확하지만 정확하지는 않습니다.
그것은 기대하지 않는 결과를 줄 것입니다. 그것은 좋은 해결책과는 거리가 멀다. 부모 프로세스의 표준 설명자를 엉망으로 만들고, 표준 입력을 복구하지 않으며, 설명자가 어린이에게 누출되기 등.
재귀 적으로 생각한다면 이해하기가 더 쉬울 수 있습니다. 아래는 오류 확인없이 올바른 솔루션입니다. 링크 된 목록 유형을 고려하십시오 command
, 그와 함께 next
포인터와 a 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. 나머지는합니다.
이 질문과 다른 질문 모두 (첫 번째 게시물에 링크 된대로), 일시적인 이 질문에서 가능한 해결책으로 입증 된 부모 파일 디스크립터를 엉망으로 만들지 않고 문제에 대한 해결책을 제안했습니다.
나는 그의 해결책을 얻지 못했고 이해하려고 노력했지만 이해하려고 노력했지만 그것을 얻을 수없는 것 같습니다. 나는 또한 이해하지 않고 코딩하려고했지만 작동하지 않았습니다. 아마도 내가 그것을 올바르게 이해하지 못했고 코딩되어야했을 것입니다.
어쨌든, 나는 의사 코드에서 이해했던 것들 중 일부를 사용하여 내 자신의 솔루션을 생각해 내려고 노력했으며 다음과 같습니다.
#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;
}
이것은 최고이고 가장 깨끗한 솔루션은 아니지만 제가 생각해 낼 수있는 것이었고 가장 중요한 것은 내가 이해할 수있는 것입니다. 내가 이해하지 못하는 일을하는 것이 좋은 점은 선생님이 평가하고 코드가 무엇을하는지 설명 할 수 없습니까?
어쨌든, 당신은 이것에 대해 어떻게 생각하십니까?
이것은 내 "최종"코드입니다 일시적인 제안 :
#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;
}
이제 괜찮습니까?