パイプを実装してみたというお話

やりたいこと

下記のように、bashのパイプの動きを再現する。
言語 : c

infile : 入力元ファイル
outfile : 出力先ファイル
pipex : 今回作るプログラムの実行ファイル名

$ cat infile
Hello World
$ < infile cat | cat -e | cat -n > outfile
$ cat outfile
     1  Hello World$
  • 今回作るプログラムの動き(引数で材料を渡す)
$ cat infile
Hello World
$ ./pipex infile "cat" "cat -e" "cat -n" outfile
$ cat outfile
     1  Hello World$

github.com

使った関数

  • pipe int pipe(int pipefd[2]);
    pipe(一時的に生成されるファイルみたいなもの)を生成して、pipefd[]にそのファイルディスクリプタを格納する。
    ※書き込み用pipefd[1]と読み込み用pipefd[0]のファイルディスクリプタがあって、pipefd[1]を閉じずにpipefd[0]から読み込みすると書き込みを待って処理が停止してしまう。 f:id:rakiyama0229:20210901000523p:plain

  • dup2 int dup2(int oldfd, int newfd);
    oldfd(fd=ファイルディスクリプタ)を複製してnewfdに割り当てる。つまり、newfdはoldfdと同じファイルを指す。f:id:rakiyama0229:20210831235631p:plain

  • execve int execve(const char *filename, char *const argv[], char *const envp[]);
    実行形式のファイルを実行する。
    通常標準入力fdから入力を受け取り、標準出力fdに実行結果を出力する。 f:id:rakiyama0229:20210901000158p:plain

  • fork pid_t fork(void);
    子プロセスの生成。
    execveを実行するとそのプログラムは終了してしまうので、
    今回はコマンド(catなど)を実行したい度に子プロセスを作成して、子プロセスでexecveを実行させた。

  • wait pid_t wait(int *status);
    親プロセスが子プロセスの終了を待つときに使った。

  • その他
    access, open, close, read, write, malloc, free, exit, perror

プログラムの流れ

f:id:rakiyama0229:20210804224252p:plain

上の図だとわかりづらいので、さらに図式化

f:id:rakiyama0229:20210901000957p:plain

課題

実は上述したプログラムでは、大きいサイズ(50Mバイトとかやってみた)のinfileを渡されるとプログラムが停止してしまうという、、。
おそらく下記の流れでこの現象が起きている。
pipeに書き込むサイズがpipeの容量を超える
→ 子プロセスでのpipeへの書き込み作業が完了しない
親プロセスは子プロセスの終了を待っているため進まない

他の人のプログラムや、linuxのmanページのpipeの容量に関する記事を参考にした。 man7.org

  • 解決方法の流れ
    親プロセスでは、子プロセスを次々生成させる(逐一子プロセスの終了を待たない)
    → 子プロセスのstdinfdには、前の子プロセスが書き込みに使うpipeのfdを繋げておく
    子プロセスを生成し終わった親プロセスは、最後の子プロセスの実行のみ終了を待つ
    小プロセスはゾンビを作らないためにも最後にすべてwait系で対処した方がいいらしい。
//全ての小プロセスを生成後
while(child_process_cnt--)
    wait(NULL);

f:id:rakiyama0229:20211005094006p:plain
子プロセスを各々走らせ、最後に全てwaitする
このようにすれば、各子プロセス間でpipeがつながっているので、パイプがフルになって書き込み作業が止まるという現象がなくなる。
これが本来shellが行っているようなマルチプロセスの処理っぽいですね。
というよりせっかくのマルチプロセスだから逐一waitしてちゃ処理速度的にもったいなかったのかもしれない。

追記

親プロセスで標準入出力fdの指す先をいじる(今回だとstdinfdをpipeに繋いでいる)場合、その後に親プロセスで標準入出力先が変わっていることに注意。
解決法の例

  • stdinfdをpipeに繋げるのではなく、pipeのfdを変数で持っておいて、次の子プロセスではそこから読み込むようにする。
  • stdinfdを指す他の変数(仮にoriginal_stdinfd)をdup()で取っておいて、全ての作業が終わったらstdinfdがoriginal_stdinfdの指す先を向くようdup2()で戻す。

参考にした記事

[https://www.c-lang.net/pipe/index.html:title] linuxjm.osdn.jp stackoverflow.com