Имеет ли смысл этот код с несколькими каналами на C?

StackOverflow https://stackoverflow.com/questions/948221

  •  09-09-2019
  •  | 
  •  

Вопрос

Я создал вопрос по этому поводу через несколько дней.Мое решение - это что-то в духе того, что было предложено в принятом ответе.Однако мой друг предложил следующее решение:

Пожалуйста, обратите внимание, что код обновлялся несколько раз (проверьте правки), чтобы отразить предложения, содержащиеся в ответах ниже.Если вы намерены дать новый ответ, пожалуйста, сделайте это, имея в виду этот новый код, а не старый, в котором было много проблем.

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

Теперь все в порядке?

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top