コンピューター:C言語講座:fork,exec,pipeについて


 このテーマはどちらかというとUNIX系の話題になってしまうのですが、PC系ではDOSの時代にはマルチタスクができませんでしたので、平行には走れませんでしたが、C言語の処理系独自の関数がたくさんありました。WindowsになってからはUNIX系と似てきましたが、まだ少し違うようです。
 自分で作成したプログラムから他のコマンドを実行したい、ということは良くあることだと思います。例えば、ディレクトリーの中身を簡単に得たい場合などはUNIXではlsコマンドを実行させて、結果をもらうのが簡単に思い付くと思います。とくにUNIXのコマンドはそのように組み合わせて使いやすくできていて、必要な情報だけを明確に返答するコマンドがほとんどです(その分、初心者が自分でコマンドを使う時に不親切なのですが)。

system()
 大抵の人が上記のような事をしようと思った時にお世話になるのがsystem()関数でしょう。これは実行したいコマンドを引数で渡せばそのまま実行してくれます。厳密にはそのまま実行しているのではなく、shというシェルに渡して実行してもらうのですが。バックグラウンドで走らせて平行処理することも&を付ければできます。これで先程の目的を達成するには例えば以下のようにします。簡単にテストできるようにmainだけで作ります。

#include    <stdio.h>
#include    <string.h>

void main()
{
char  filename[80],str[512],*ptr;
FILE  *fp;

    sprintf(filename,"/tmp/ls%d.tmp",getpid());
    sprintf(str,"ls -1 > %s",filename);
    system(str);

    if((fp=fopen(filename,"r"))==NULL){
        fprintf(stderr,"error!!!\n");
        exit(-1);
    }
    while(1){
        fgets(str,512,fp);
        if(feof(fp)){
            break;
        }
        ptr=strchr(str,'\n');
        if(ptr!=NULL){
            *ptr='\0';
        }
        printf("%s\n",str);
    }
    fclose(fp);

    sprintf(str,"rm -f %s",filename);
    system(str);
}

 lsの出力は標準出力にされますので、そのままでは画面にただ表示されてしまいます。そこで、ファイルにリダイレクトし、そのファイルを読み出すようにしています。リダイレクト先のファイル名は悩むところなのですが、UNIXでは中間ファイルは/tmpが良く使われ、他人が同時に同じ事をしても問題が起きないように、プロセスIDをファイル名に含ませるテクニックが良く使われます。プロセスIDは1つのホストでは重複しませんし、他のホストから同じ場所に書き込まれると危険ですが、それでも重複はほとんどありませんし、まして/tmpを他のホストと共有することはまず無いのでOKでしょう。なお、lsのオプション-1は無くても大抵は大丈夫ですが、1行に1つのエントリの形式で出力させるオプションです。
 ファイルはとりあえず、fgets()で読み込みますが、これは改行をそのまま文字列に入れ込みますので、ここではstrchr()で改行文字を探し、カットしています。ファイルを使い追えたら不要なので忘れずに削除します。ここではrmコマンドをsystem()で呼んでいますが、-fオプションはできれば付けたほうが良いでしょう。-fは強制削除指示です。これが無い場合、運が悪いと、rmが消して良いかどうかの確認がでることも考えられます。

 このsystem()関数を使った方法でもとくに問題はないのですが、気分的には中間ファイルを使用するのが気に入りません。良くやってしまうのですが、中間ファイルを消し忘れることがあります。これではみんなの/tmpを使ううえでの礼儀知らずなプログラムといえるでしょう。

popen()
 こんな不満を解決してくれる関数がC言語では用意されていて、popen()という関数です。これはfopen()と非常に良く似ていて、fopen()ではファイル名(パス)を引数に指定して使いますが、popen()ではそのかわりにコマンドを指定できます。この関数を使って同様の処理を書くと以下のようになります。

#include    <stdio.h>
#include    <string.h>

void main()
{
char  str[512],*ptr;
FILE  *fp;

    if((fp=popen("ls -1","r"))==NULL){
        fprintf(stderr,"error!!!\n");
        exit(-1);
    }
    while(1){
        fgets(str,512,fp);
        if(feof(fp)){
            break;
        }
        ptr=strchr(str,'\n');
        if(ptr!=NULL){
            *ptr='\0';
        }
        printf("%s\n",str);
    }
    pclose(fp);
}

 いかがですか?はじめの書き方からpopen()を使う書き方はほんの簡単な手直しで移行できます。こちらでは中間ファイルも必要ありませんので、最後の削除も不要ですし、速度的にもこちらのほうが若干高速になります。なお、popen()で得たファイルポインターはpclose()で閉じます。
 popen()ははじめのpでお分かりのように、パイプを使用しています。自分のプログラムとlsとのあいだにパイプを設け、lsの標準出力をパイプを介して読み込むことができます。"r"のかわりに"w"を指定すればlsコマンドの標準入力に書き込むこともできますが、lsコマンドは標準入力は使いませんので、この場合は意味がありません。

fork(),exec*(),pipe()
 さて、問題はここからで、コマンドとのあいだにパイプを2つ持ちたい場合はどうすれば良いのでしょうか?つまり、プログラムから指示も出したいし、結果を得ることもしたい場合です。popen()では1つのパイプしか使えませんので、そのような目的には使えません。
 この要求を満たすには自分でプロセス管理をする他にありません。system()関数ではパイプは使えませんので、自分でパイプを用意しながら子プロセスを起動するような処理を作成する必要があります。ここでようやく登場するのがfork()とexec*()です。fork()は厳密には関数ではなく、システムコールです。このシステムコールの役割は自分の分身を作ることです。これで自分の分身を作成し、そこでコマンドをexec*()を用いて実行します。単純に考えれば、fork()してコマンドを実行しないことは滅多に無いので一緒にしても良いような気がしますが、それではsystem()と大差無くなります。じつは、fork()した後にコマンドを実行するまでのあいだにパイプの処理をする為にこのように2つに分かれているのです。
 では、実際に読み込み用と書き込み用の2つのパイプを開いてコマンドを実行するpopen2()関数を作ってみましょう。ちょっと長いのですが、以下にソースを記述します。

#include    <stdio.h>
#include    <signal.h>

#define R    (0)
#define W    (1)

int popen2(command,fd_r,fd_w)
char  *command;
int   *fd_r,*fd_w;
{
int   pipe_c2p[2],pipe_p2c[2];
int   pid;

    /* Create two of pipes. */
    if(pipe(pipe_c2p)<0){
        perror("popen2");
        return(-1);
    }
    if(pipe(pipe_p2c)<0){
        perror("popen2");
        close(pipe_c2p[R]);
        close(pipe_c2p[W]);
        return(-1);
    }

    /* Invoke processs */
    if((pid=fork())<0){
        perror("popen2");
        close(pipe_c2p[R]);
        close(pipe_c2p[W]);
        close(pipe_p2c[R]);
        close(pipe_p2c[W]);
        return(-1);
    }
    if(pid==0){   /* I'm child */
        close(pipe_p2c[W]);
        close(pipe_c2p[R]);
        dup2(pipe_p2c[R],0);
        dup2(pipe_c2p[W],1);
        close(pipe_p2c[R]);
        close(pipe_c2p[W]);
        if(execlp("sh","sh","-c",command,NULL)<0){
            perror("popen2");
            close(pipe_p2c[R]);
            close(pipe_c2p[W]);
            exit(1);
        }
    }

    close(pipe_p2c[R]);
    close(pipe_c2p[W]);
    *fd_w=pipe_p2c[W];
    *fd_r=pipe_c2p[R];

    return(pid);
}

 まず、この関数の仕様ですが、コマンドと、書き込み、読み込み用のファイルディスクリプタのポインターを渡すようにします。ファイルディスクリプターに開いたパイプが返るようになります。ファイルポインターで使いたい方はfdopen()で変換?して使ってください。
 まず、パイプを用意します。pipe()システムコールを使用します。これを呼び出すと、引数で渡したファイルディスクリプタの配列にパイプが返ります。配列の1つめに読み込み用、2つめに書き出し用の物が返ります。これはよく頭が混乱するので、私はR,Wでdefineして使っています。今回は2つのパイプが必要なので、pipe()は2かい呼ばれます。pipe_c2p,pipe_c2cの2組のパイプが準備されます。
 パイプの準備ができたらfork()します。ここは慣れないとなんとも気分が悪くなるのですが、fork()を呼び出した時点で自分自身の複製が新しいプロセスとして作成されます。fork()の戻り値が0より小さい場合はエラーですが、それ以外では0なら複製されたプロセス側ということになります。また、0より大きい値の場合は自分自身で、その返り値は子プロセスのプロセスIDです。したがって、一般的にfork()の後にはエラーの判定(0より小さい)があり、その後に返り値が0の条件分岐が置かれ、そこで子プロセスの処理を記述します。
 まことに気分が悪いのですが、fork()が作成した時点で自分と全く同じプロセスが同時に走っているのです。したがって、返り値が0の条件分岐などを書かずにそのまま処理を続ければ、同じ処理を2つのプロセスが別々に行なってくれます。
 今回のように子プロセスでコマンドを実行したい場合は上記のように戻り値が0の条件分岐内でコマンドをオーバーレイして実行することになります。その前に、子プロセス側では、pipe_p2c[W](親の書き込み先にする)とpipe_p2p[R](親の読み込み先にする)をclose()で閉じます。これは子プロセス側では使いません。また、dup2()を使って、子プロセスの標準入力(0)をpipe_p2c[R]に、標準出力(1)をpipe_c2p[W]に割り当てます。これによって子プロセスの標準入出力は親とのパイプになります。dup2()で割り当てた後はそれらはclose()しておきます。
 ここでexec*()を呼び出してコマンドをオーバーレイして実行します。exec*()はいろいろな種類がありますが、今回はexeclp()で文字列をそのままshに渡してみました。exec*()はコマンドの起動に成功すると戻りません。そのプロセスがそのまま起動したコマンドに置き換わる感じです。コマンドが終れば勝手に終ります。
 親のほうではfork()の戻り値が0より大きくなりますので、先に飛び、pipe_p2c[R](子プロセスの読み込み先)とpipe_p2p[W](子プロセスの書き込み先)をclose()し、pipe_p2c[W]を子プロセスに対する書き込み先とし、pipe_c2p[R]を読み込み先として、とりあえず、子プロセスのプロセスIDをリターンするようにしてあります。
 popen()では閉じる時にpclose()を使いましたが、popen2()ではclose()またはfdopen()した場合はfclose()で閉じてかまいません。ただ、正しくはwait()システムコールで子プロセスの終了を受け取るのが正しい使い方です。wait()しなくても親が終了すれば子プロセスの情報も無くなるのですが、親が生きているかぎりはプロセス管理上はwait()されるまで残ります。
 また、標準エラー出力もパイプでつなぎたければ、もう1つパイプを用意し、dup2()で標準エラー出力(2)を割り当てればできます。あまりここまでは必要無いでしょうけれども(私はpopen2()作成ででfork()などがわかった時に嬉しくてpopen3()もすぐに作りましたが、一度も使う機会がありません・・・)。

まとめ
 かなり長い説明になってしまいましたが、fork(),exec*(),pipe()の関係は理解していただけましたか?私自身、このfork(),exec*()はなかなか理解できず、system()と何が違うのかよくわからなかったのですが、pipe()をからめると2つにわかれている意味が理解できましたので、こういった形で紹介してみました。究極的にはdup2()をする為にわかれているといっても良いかも知れません。また、わかれているおかげで、fork()で複製を作って、そのまま別の処理をバックグラウンドで行なわせることにも使えます。私も以前、CADを組んでいた時にデータの自動バックアップにこれを使ったことがあります。定期的にfork()してそこで保存処理を行なってexit()する感じです。これなら編集作業は中断せずに自動バックアップできます。ただ、複製を作るので、CADのようにメモリーを多量に使用しているシステムではそもそも複製を作るのにシステムが重くなったり、ひどい時はメモリー不足で複製が作れない状況もあるのであまり賢い方法ではありませんが。
 このようにパイプを使いながら子プロセスを制御できると、子プロセスとのあいだで指示を出し合いながら処理をすることもできますし、簡単な1対1の通信手段として使用できます。パラメーターファイルを作りながらコマンド間で情報を受け渡して行なう処理よりはだいぶ細かい制御ができます。
 sh,cshなどのシェルもコマンドを起動する時にはfork(),exec*()とpipe()を使ってコマンドラインで指定されたパイプを処理しながらコマンドを起動しています。fork()した時にプロセスIDも返って来るので、jobsなどで見れるように管理することもできます。また、fork()は完全な複製を作るので、大きなプロセスではそのまま他のコマンドをexec*()するのに無駄だ、ということからvfork()という全てを複製しないバージョンもあります(制限も多いですが)。これらを使って自分のシェルを作ってみるのも良い勉強になると思いますので是非、チャレンジしてみてください。


よろしければブログもご覧ください
ゴルフ練習場紹介サイト:ゴルフ練習場行脚録更新中!
ipv400046624 from 1998/3/4