シェルの多段パイプを自作してみる

この記事は慶應ロ技研アドベントカレンダー15日目です。

1日目

14日目16日目

・背景

こんにちは!卒業単位ギリギリまで履修をそぎ落とした結果、暇を持て余しているcrispydonutです。ロ技研のアドベントカレンダーの企画なのですが、ロボットの知識に乏しいので(あ…)、僕がとっている講義で出された課題に関するお話をします。因みに僕は情報工学科で、C言語を使う課題です。

・ターゲット

この記事の読者層として、以下のような人を想定しています。
・シェルを使って作業したことがある人
・C言語が書ける人
・シェルを作ってみたい人

非常に稀有な存在だという事が良く分かります(主に3つ目)。また、この記事は実装できるようになることを目的としており、正確な用語、表現とはかけ離れている場合があります。(これは私自身も良く分かっていないからです。)。分かりやすい説明を心がけていますが、正直全く自信がないので読み飛ばしてくれてかまいません。ソースコードだけ参考にすれば作れると思います。

・シェルとは

今回対象とするシェルとはUNIX/Linux系のOSで作業する際にお世話になるであろうあのシェルです。
分からない人はこの記事を見つけないと思うので詳細の説明は割愛します。…というのは冗談で、僕もはっきり分かっておらず、あいまいな説明になってしまうのでそこらへんは他のサイトに任せます。
今回の話は、シェルを使ったことがある人なら予備知識は十分なので、改めて詳しく調べる必要はないと思います。ただしそれなりに難しい内容だとは思います。

・出された課題

課題の内容は「シェルを自作せよ!」に要約されるのですが、いくつかの機能だけ実装すればよいので途方もない課題ではないです。そのうち同級生が一番実装に苦しんでいた(?)であろう機能にクローズアップします。それは「多段パイプ」です。

・多段パイプについて

多段パイプとはシェルの基本的な機能であるパイプ(|←これ)を2つ以上組み合わせて使う事を言います。
例えば「ls | wc」とするとlsの出力がwcの入力へとつながります。
ls | head | wc」とするとパイプを2つ使っているので多段パイプとなります。

・今回作成するプログラム

シェルに入力された文字列を扱いやすい形に整形するのも意外と難しく面倒くさいです。しかしコードが長くなっては分かりずらくなるので、ここでは入力文字列を固定し一つの命令を実行するだけのものを作成します。またエラー処理などめんどくさいものを作っておらず、本当に簡単なものとなっています。

・注意書き

詳しい解説も入れますが、学部3年の拙い知識で書いております。内容に間違いが含まれていると思われますが、僕なりの解釈で分かりやすく伝えることを目的としています。なので正式な名称で書くことにこだわっていません。間違いに気づかれた場合にはコメントなどしてくださると助かります。

・実装にあたって

プログラムを実行すると、プロセスが生成されます。生成されたプロセスは1つの入力ポート、2つの出力ポートを持ちます。入力ポートは「標準入力」、出力ポートは「標準出力」と「標準エラー出力」と呼ばれています。通常、標準入力にキーボードから入力をし、標準出力を通じて画面に実行結果が出力されます。エラーがある場合には標準エラー出力を通じて出力されます。それぞれのポートは上記左から順に0,1,2と番号が付いています。「ls | wc」を実行したい場合、イメージとしては「ls」の標準出力先を「wc」の標準入力につなげる、というプログラムを作成しようとしています。

・必要な関数

int pipe(int pipefd[2])

これは名前の通りパイプを作る関数です。pipefd[1]に入れたデータはpipefd[0]から取り出すことができます。
pipefd[0]とpipefd[1]にはポート番号が入っており、プログラム実行直後は0~2番のポートが使用されているのでpipefd[0]には3、pipefd[1]には4が入るはずです(逆だったらすみません)。以降pipeを実行すると、できるだけ小さい空いてるポート番号を返します。

int dup2(int oldfd, int newfd)

これはポート番号oldfdにあるものを複製して、ポート番号newfdに上書きする関数です。
例えばpipe(pipefd)とパイプを作成したとき、dup2(pipefd[1], 1)とすると標準出力先がpipefd[1]、つまりパイプの入り口とつながります。このとき、ポート番号pipefd[1]が不要となるのでclose関数(後述)で閉じておく必要があります。

int close(int fd)

この関数はポート番号fdを閉じる関数です。閉じるというのはそこのポートがどこにもつながっていない状態するという事です。例えばclose(1)としてしまうと標準出力先がなくなってしまうので、printf(“Hello, world.\n”)としても画面に表示されなくなってしまいます。余談ですがこのプログラムを作る際、よく標準入力を閉じるので、デバッグする際はprintfではなくfprintfで標準エラー出力を指定しましょう。

int execvp(const char *file, char *const argv[])

これはfileにあるコマンドを実行してくれる関数です。実行が成功した場合、実行したプロセスは終了してしまうので、fork関数(後述)で作った子プロセスの中で実行する必要があります。argvは引数リストであり、最後にNULLを入れておく必要があります。例えば「ls -l」を実行したい場合、

char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);

のようにすれば実行できます。

pid_t fork(void)

これは子プロセスを作る関数です。基本的に親プロセス(実行元)と子プロセスは独立して動きます。
fork関数を実行すると両プロセス共に次の行から実行していきますが、子プロセスの場合は返り値が0になるので、そこでどっちのプロセスかを判別できます。

pid_t wait(int *status)

これは親プロセスが子プロセスの終了を待つ関数です。これをしないと、終了した子プロセスが取り残され、いわゆるゾンビプロセスとして取り残されることがあります。

・作っていこう!

今回は「ls | head | wc」の実行を目指していこうと思います。が、今から作るプログラムでは9個のパイプまで対応できるはずです。少しずつプログラムを書いていきます。

#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    char *argv[] = {"ls", "|", "head" , "|", "wc", NULL};
    int i, pipe_locate[10], pipe_count = 0;
    pipe_locate[0] = -1;
    for (i = 0; argv[i] != NULL; i++) {
        if (strcmp(argv[i], "|") == 0) {
            pipe_count++;
            pipe_locate[pipe_count] = i;
            argv[i] = NULL;
        }
    }


ここではargvの”|”が入っているところをNULLにしています。
こうするとargvの中身が{“ls”, NULL, “head”, NULL, “wc”, NULL}となり、コマンドを3つに分けられます。つまり、「argv」からみると「ls」、「argv + 2」から見ると「head」、「argv + 4」から見ると「wc」というコマンドになります(execvpはNULLがあるところを引数リストの終点とみるため)。pipe_countにはパイプの数が、pipe_locate[10]にはパイプがある場所が記録されております。便宜上、0個目のパイプが「argv[-1]」のところにあると考えて、pipe_locate[0]は-1に初期化してあります。

    int pfd[9][2];
    if (pipe_count == 0) {
        if (fork() == 0) {
            execvp(argv[0], argv);
            exit(0);
        }
        else {
            int status;
            wait(&status);
        }
    }

pfd[9][2]はパイプを格納するための変数です。パイプ9個分まで格納できます。その下の行からはパイプが一切なかった時の処理を書いています。fork()で子プロセスを作り、子プロセスでコマンドを実行しています。親プロセス(elseのブロック)では子プロセスの実行終了を待っています。

    for (i = 0; i < pipe_count + 1 && pipe_count != 0; i++) {
        if (i != pipe_count) {
            pipe(pfd[i]);  //最後のコマンドでなければパイプを作成
        }
        if (fork() == 0) {
            //子プロセス
            if (i == 0) {
                //最初のコマンドなので、標準出力をパイプの入り口へつなげる
                dup2(pfd[i][1], 1);  
                close(pfd[i][0]); close(pfd[i][1]);
            } else if (i == pipe_count) {
          //最後のコマンドなので、標準入力をパイプの出口へつなげる
                dup2(pfd[i - 1][0], 0);
                close(pfd[i - 1][0]); close(pfd[i - 1][1]);
            } else {
                //途中のコマンドなので、上記の処理を両方やる
                dup2(pfd[i - 1][0], 0);
                dup2(pfd[i][1], 1);
                close(pfd[i - 1][0]); close(pfd[i - 1][1]);
                close(pfd[i][0]); close(pfd[i][1]);
            }
            execvp(argv[pipe_locate[i] + 1], argv + pipe_locate[i] + 1);
            exit(0);
        }
        else if (i > 0) {
            //親プロセス
            //つなげ終わったパイプは閉じる
            close(pfd[i - 1][0]); close(pfd[i - 1][1]);
        }
    }

ここが多段パイプの核となるコードです。コマンドはパイプの数+1個あるので、pipe_count + 1回forループを回しています。後はコメントにある通りですが、execvpの部分がしっくりこないかもしれませんね。まずi = 0の場合、pipe_locate[0]を-1で初期化していましたね。
つまりexecvp(argv[0], argv);を実行することになります。argv[0]は"ls"です。また上にも書いた通り、execvpはNULLがあるところまで引数リストがあると認識するので、引数も"ls"だけになります。i = 1の時は、pipe_locateはパイプの場所を記録してあるので、pipe_locate[1] = 1となります。
よってexecvp(argv[2], argv + 2);となります。argv[2]は"head"であり、argv + 2で"head"から引数リストを見ていきます。この様にして実装してあります。追記:子プロセス内にいっぱいあるclose関数ですが、必ず書きましょう。正直言ってなんで必要なのかはっきり分からないですが、書かないとうまく動かないと思います。理由が分かる方がいらしたら教えてくださると嬉しいです。

    int status;

    for (i = 0; i < pipe_count + 1; i++) {
        wait(&status);
    }
    return 0;
}

後は解説不要でしょう。コマンドの数だけ子プロセスの終了を待っているだけです。

・最後に

今回は自分としてはかなり難しい話題を選んでしまいました。ですが僕自身、多段パイプを実装するのが難しかったので、他にも苦しんでいる人がいると思い記事を書いてみました。結構長い記事となりましたが、まあ読みたい部分だけ読んでくれていることでしょう。もし理解の一助となれたなら幸いです。最後に今回作成したコード全文を記しておきます。繰り返しとなりますが、このコードはエラーが起きた時の処理を書いていません。ほどほどに参考にしてください。

#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    char *argv[] = {"ls", "|", "head" , "|", "wc", NULL};
    int i, pipe_locate[10], pipe_count = 0;
    pipe_locate[0] = -1;
    for (i = 0; argv[i] != NULL; i++) {
        if (strcmp(argv[i], "|") == 0) {
            pipe_count++;
            pipe_locate[pipe_count] = i;
            argv[i] = NULL;
        }
    }

    int pfd[9][2];
    if (pipe_count == 0) {
        if (fork() == 0) {
            execvp(argv[0], argv);
            exit(0);
        }
        else {
            int status;
            wait(&status);
        }
    }

    for (i = 0; i < pipe_count + 1 && pipe_count != 0; i++) {
        if (i != pipe_count) pipe(pfd[i]);

        if (fork() == 0) {
            if (i == 0) {
                dup2(pfd[i][1], 1);
                close(pfd[i][0]); close(pfd[i][1]);
            } else if (i == pipe_count) {
                dup2(pfd[i - 1][0], 0);
                close(pfd[i - 1][0]); close(pfd[i - 1][1]);
            } else {
                dup2(pfd[i - 1][0], 0);
                dup2(pfd[i][1], 1);
                close(pfd[i - 1][0]); close(pfd[i - 1][1]);
                close(pfd[i][0]); close(pfd[i][1]);
            }

            execvp(argv[pipe_locate[i] + 1], argv + pipe_locate[i] + 1);
            exit(0);
        }
        else if (i > 0) {
            close(pfd[i - 1][0]); close(pfd[i - 1][1]);
        }
    }
    int status;

    for (i = 0; i < pipe_count + 1; i++) {
        wait(&status);
    }
    return 0;
}

ここまで読んでくださり、ありがとうございました。まだまだ慶應ロ技研アドベントカレンダーは続きますので、どうぞよろしくお願いいたします。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中