Имеет ли смысл этот код с несколькими каналами на 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
, манипулируя ими только внутри ребенка, и никогда не сливает никаких FDS.Однако это немного сложнее изобразить на диаграмме.
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
к детям.Как говорит Джон, это, вероятно, не опасно для большинства программ...но вам все равно следует написать оболочку с лучшим поведением, чем эта.
Даже если это временно, я бы настоятельно рекомендовал не искажать стандартный ввод / вывод / ошибку вашей собственной оболочки (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];
}
}
Вы можете видеть Удар's execute_cmd.c#execute_disk_command
будучи вызванным из execute_cmd.c#execute_pipeline
, xsh's process.c#process_run
будучи вызванным из jobs.c#job_run
, и даже каждый из Деловой ящик's различные маленький и минимальный раковины разделяет их.
Другие советы
Ключевая проблема заключается в том, что вы создаете кучу труб и не следите за тем, чтобы все концы были закрыты должным образом.Если вы создаете канал, вы получаете два файловых дескриптора;если вы разветвляетесь, то у вас есть четыре файловых дескриптора.Если вы dup()
или dup2()
один конец канала для стандартного дескриптора вам необходимо закрыть оба конца канала - по крайней мере, одно из закрытий должно быть после операции dup() или dup2().
Рассмотрим файловые дескрипторы, доступные первой команде (при условии, что их по крайней мере два - то, что должно обрабатываться в целом (нет pipe()
или требуется перенаправление ввода-вывода всего одной командой), но я признаю, что обработка ошибок исключена, чтобы сохранить код, подходящий для 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.Это делает все остальное.
Как в этом вопросе, так и в другом (как указано в первом сообщении), эфемерный предложил мне решение проблемы без вмешательства в дескрипторы родительских файлов, как продемонстрировано возможным решением в этом вопросе.
Я не понял его решения, я все пытался и пытался понять, но, похоже, у меня ничего не получается.Я также пытался закодировать это, не понимая, но это не сработало.Вероятно, потому, что я не смог понять это правильно и не смог закодировать это так, как это должно было быть закодировано.
В любом случае, я попытался придумать свое собственное решение, используя некоторые вещи, которые я понял из псевдокода, и пришел к этому:
#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;
}
Теперь все в порядке?