2

私はこれについて数日かけて質問を作成しました。私の解決策は、受け入れられた回答で提案されたものです。しかし、私の友人は次の解決策を思いついた:

以下の回答の提案を反映するために、コードが数回更新されていることに注意してください (編集リビジョンを確認してください)。新しい回答をする場合は、問題の多い古いコードではなく、この新しいコードを念頭に置いてください。

#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 のようにパイプを介して一連のコマンドをシミュレートします。たとえば、次のようになります。cmd2 | ... | cmd_n. ご覧のとおり、コマンドは実際には引数から読み取られるため、「シミュレート」と言います。簡単なシェルプロンプトをコーディングする時間を割くために...

もちろん、エラー処理のように修正したり追加したりする問題がいくつかありますが、それはここでのポイントではありません。コードはある程度理解できたと思いますが、それでも、この全体がどのように機能するかについて多くの混乱を招きます。

私は何かが欠けているのでしょうか、それともこれは本当にうまくいき、問題を解決するためのきれいできれいな解決策ですか? そうでない場合、誰かがこのコードの重大な問題を指摘できますか?

4

5 に答える 5

8

合理的に見えますが、実際にはリークを修正する必要がstdありaux、子とループの後に、親のオリジナルstdinは永遠に失われます。

これは色をつけたほうがいいかも…

./a.out foo bar baz <stdin >stdout
std = dup(stdout) || |+==========================標準
                      || || ||
パイプ (fd) || || パイプ1[0] -- パイプ0[1] ||
                      || || || || ||
補助 = fd[0] || || || 補助 || ||
                      || XX || || ||
                      || /---------++----------+| ||
dup2(fd[1], 1) || // || || ||
                      || || || || ||
閉じる (fd[1]) || || || XX ||
                      || || || ||
フォーク + 実行 (foo) || || || ||
                      XX || || ||
                       /-----++-------+| ||
dup2(aux, 0) // || || ||
                      || || || ||
閉じる (aux) || || XX ||
                      || || ||
パイプ (fd) || || パイプ2[0] -- パイプ2[1] ||
                      || || || || ||
補助 = fd[0] || || || 補助 || ||
                      || XX || || ||
                      || /---------++----------+| ||
dup2(fd[1], 1) || // || || ||
                      || || || || ||
閉じる (fd[1]) || || || XX ||
                      || || || ||
fork+exec(バー) || || || ||
                      XX || || ||
                       /-----++-------+| ||
dup2(aux, 0) // || || ||
                      || || || ||
閉じる (aux) || || XX ||
                      || || ||
パイプ (fd) || || パイプ3[0] -- パイプ3[1] ||
                      || || || || ||
補助 = fd[0] || || || 補助 || ||
                      || XX || || ||
                      || /---------++----------+| ||
dup2(fd[1], 1) || // || || ||
                      || || || || ||
閉じる (fd[1]) || || || XX ||
                      || XX || ||
                      || /------++-----------------+|
dup2(標準、1) || // || ||
                      || || || ||
フォーク + 実行 (baz) || || || ||
  • foostdin=stdin_stdout=pipe1[1]
  • barstdin=pipe1[0]_stdout=pipe2[1]
  • bazstdin=pipe2[0]_stdout=stdout

stdin私の提案は、親のandのマングリングを回避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
親
    cmds = [フー、バー、バズ]
    fds = {0: 標準入力、1: 標準出力}

cmd = cmds[0] {
    次のコマンドがあります {
        パイプ(new_fds)
            new_fds = {3, 4}
            fds = {0: 標準入力、1: 標準出力、3: パイプ1[0]、4: パイプ1[1]}
    }

    フォーク => 子
                        次のコマンドがあります {
                            閉じる (new_fds[0])
                                fds = {0: stdin、1: stdout、4: pipe1[1]}
                            dup2(new_fds[1], 1)
                                fds = {0: 標準入力、1: パイプ 1[1]、4: パイプ 1[1]}
                            閉じる (new_fds[1])
                                fds = {0: stdin、1: pipe1[1]}
                        }
                        実行(コマンド)

    次のコマンドがあります {
        old_fds = new_fds
            old_fds = {3, 4}
    }
}

cmd = cmd[1] {
    次のコマンドがあります {
        パイプ(new_fds)
            new_fds = {5, 6}
            fds = {0: stdin、1: stdout、3: pipe1[0]、4: pipe1[1]、
                                        5: パイプ2[0]、6: パイプ2[1]}
    }

    フォーク => 子
                        前のコマンドがあります {
                            dup2(old_fds[0], 0)
                                fds = {0: パイプ 1[0]、1: 標準出力、
                                       3: パイプ1[0]、4: パイプ1[1]、
                                       5: パイプ2[0]、6: パイプ2[1]}
                            閉じる (old_fds[0])
                                fds = {0: パイプ 1[0]、1: 標準出力、
                                                    4: パイプ1[1]、
                                       5: パイプ2[0] 6: パイプ2[1]}
                            閉じる (old_fds[1])
                                fds = {0: パイプ 1[0]、1: 標準出力、
                                       5: パイプ2[0]、6: パイプ2[1]}
                        }
                        次のコマンドがあります {
                            閉じる (new_fds[0])
                                fds = {0: パイプ 1[0]、1: 標準出力、6: パイプ 2[1]}
                            dup2(new_fds[1], 1)
                                fds = {0: パイプ 1[0]、1: パイプ 2[1]、6: パイプ 2[1]}
                            閉じる (new_fds[1])
                                fds = {0: パイプ 1[0]、1: パイプ 1[1]}
                        }
                        実行(コマンド)

    前のコマンドがあります {
        閉じる (old_fds[0])
            fds = {0: stdin、1: stdout、4: pipe1[1]、
                                        5: パイプ2[0]、6: パイプ2[1]}
        閉じる (old_fds[1])
            fds = {0: 標準入力、1: 標準出力、5: パイプ 2[0]、6: パイプ 2[1]}
    }

    次のコマンドがあります {
        old_fds = new_fds
            old_fds = {3, 4}
    }
}

cmd = cmds[2] {
    フォーク => 子
                        前のコマンドがあります {
                            dup2(old_fds[0], 0)
                                fds = {0: pipe2[0]、1: stdout、
                                       5: パイプ2[0]、6: パイプ2[1]}
                            閉じる (old_fds[0])
                                fds = {0: pipe2[0]、1: stdout、
                                                    6: パイプ2[1]}
                            閉じる (old_fds[1])
                                fds = {0: pipe2[0]、1: stdout}
                        }
                        実行(コマンド)

    前のコマンドがあります {
        閉じる (old_fds[0])
            fds = {0: stdin、1: stdout、6: pipe2[1]}
        閉じる (old_fds[1])
            fds = {0: 標準入力、1: 標準出力}
    }
}

編集

更新されたコードは、以前の FD リークを修正しますが、1 つ追加します。現在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];
    }
}

Bashexecute_cmd.c#execute_disk_commandから呼び出されexecute_cmd.c#execute_pipelinexshがからprocess.c#process_run呼び出され、 BusyBoxさまざまな小さく最小限のシェルjobs.c#job_runのすべてがそれらを分割していることがわかります。

于 2009-06-04T02:50:01.450 に答える
3

重要な問題は、多数のパイプを作成し、すべての端が適切に閉じられていることを確認していないことです。パイプを作成すると、2 つのファイル記述子が得られます。フォークすると、4 つのファイル記述子があります。パイプの一方の端を標準記述子に接続する場合は、パイプの両端を閉じる必要があります。少なくとも 1 つの閉じは、dup() または dup2() 操作の後である必要がありますdup()dup2()


最初のコマンドで使用できるファイル記述子を検討してください (少なくとも 2 つあると仮定します - 一般的に処理する必要があるもの ( pipe()1 つのコマンドだけで I/O リダイレクトが不要または I/O リダイレクトは必要ありません))。 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、ループ内で初期化されていないため機能しません。

一般的に、これはほぼ正しいですが、完全には正しくありません。

于 2009-06-04T02:54:32.567 に答える
1

それは結果をもたらしますが、予期しないものもあります。親プロセスの標準記述子を台無しにし、標準入力を回復せず、記述子が子プロセスにリークするなどです。

再帰的に考えるとわかりやすいかもしれません。以下は、エラーチェックなしの正しい解決策です。ポインタと配列commandを持つ連結リスト型を考えてみましょう。nextargv

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 で呼び出します。それは残りを行います。

于 2009-06-04T03:54:28.247 に答える
0

この質問と別の質問(最初の投稿でリンクされている)の両方で、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;
}

これは最善かつ最もクリーンな解決策ではないかもしれませんが、それは私が思いつくことができたものであり、そして最も重要なことに、私が理解できるものでした。私が理解していない何かが機能していると、先生から評価されて、コードが何をしているのかを彼に説明できないのは、何が良いことでしょうか。

とにかく、これについてどう思いますか?

于 2009-06-04T14:40:32.363 に答える
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;
}

今大丈夫?

于 2009-06-04T16:27:10.473 に答える