Имеет ли смысл этот код с несколькими каналами в C? - PullRequest
2 голосов
/ 04 июня 2009

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

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

#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. Я говорю «симулировать», потому что, как вы можете видеть, команды фактически читаются из аргументов. Просто чтобы сэкономить время написания простого приглашения оболочки ...

Конечно, есть некоторые проблемы, которые нужно исправить и добавить, например, обработку ошибок, но здесь дело не в этом. Я думаю, что вроде как получил код, но это все еще заставляет меня запутаться, как все это работает.

Я что-то упустил или это действительно работает, и это хорошее и чистое решение для решения проблемы? Если нет, то может ли кто-нибудь указать мне основные проблемы, с которыми сталкивается этот код?

Ответы [ 5 ]

8 голосов
/ 04 июня 2009

Выглядит разумно, хотя на самом деле нужно исправить утечку 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 & hellip; но добавляет одно: вы теперь просачиваете std0 детям. Как говорит Джон, это, вероятно, не опасно для большинства программ ... но вы все равно должны написать оболочку с лучшим поведением, чем эта.

Даже если это временно, я бы настоятельно рекомендовал не искажать стандартную оболочку собственной / 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];
    }
}

Вы можете видеть, как Bash execute_cmd.c#execute_disk_command вызывается из execute_cmd.c#execute_pipeline, xsh process.c#process_run вызывается из jobs.c#job_run, и даже каждый из BusyBox * различные маленькие и минимальные оболочки разбивают их на части.

3 голосов
/ 04 июня 2009

Ключевая проблема заключается в том, что вы создаете группу каналов и не убедитесь, что все концы закрыты правильно. Если вы создаете канал, вы получаете два файловых дескриптора; если вы разветвляетесь, то у вас есть четыре файловых дескриптора. Если вы 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 не инициализируется внутри цикла.

Обычно это близко к правильному, но не совсем правильному.

1 голос
/ 04 июня 2009

Это даст результаты, некоторые из которых не ожидаются. Это далеко не хорошее решение: оно портит стандартные дескрипторы родительского процесса, не восстанавливает стандартный ввод, утечка дескрипторов детям и т. Д.

Если вы думаете рекурсивно, это может быть легче для понимания. Ниже приведено правильное решение без проверки ошибок. Рассмотрим тип связанного списка 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. Это делает все остальное.

0 голосов
/ 04 июня 2009

Это мой "окончательный" код с ephemient предложениями:

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

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

0 голосов
/ 04 июня 2009

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

Я не получил его решения, я пытался и пытался понять, но, похоже, я не могу его получить. Я также пытался закодировать это без понимания, но это не сработало. Возможно, потому что я не смог понять это правильно и не смог закодировать это, это должно было быть закодировано.

В любом случае, я попытался найти собственное решение, используя некоторые вещи, которые я понял из псевдокода, и придумал следующее:

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

Возможно, это не самое лучшее и чистое решение, но это было то, что я мог придумать, и, самое главное, то, что я могу понять. Что хорошего в том, что что-то работает, чего я не понимаю, и тогда меня оценивает мой учитель, и я не могу объяснить ему, что делает код?

В любом случае, что вы думаете об этом?

...