IPC::Open3 の正しい使い方 (re .pl な config ファイルのコンパイルがとおるかチェックしてみる - TokuLog 改メ tokuhirom’s blog)

http://d.hatena.ne.jp/tokuhirom/20100813/1281666615 の件ですが、IPC::Open3 の使い方が気になったので、勝手に添削 (c) dankogai。

use IPC::Open3;
...
my($wtr, $rdr, $err);
my @cmd = ($^X, (map { "-I$_" } grep { !ref $_ } @INC), '-c', $file);
my $pid = open3($wtr, $rdr, $err, @cmd);
waitpid($pid, 0);

IPC::Open3 は子プロセスの標準入出力と標準エラー出力を、親プロセスとパイプでつないでプロセス間通信を行うための仕組みです。

パイプには一定サイズのバッファがあり (Linux だと 4KB)、パイプにこれを超える大きさのデータが書き込まれると、読み込み側のプロセスがデータを読むまで、書き込み側のプロセスはブロックされます。

つまり、上のコードは、子プロセスがその標準出力か標準エラー出力にパイプのバッファサイズ以上の大きさのデータを書いた場合に、いつまでも終了しない(デッドロックする)ことになります。

では、どう書けばいいのでしょう。

my $pid = open3($wtr, $rdr, $err, @cmd);
close $rdr;
close $err;
waitpid($pid, 0);

でしょうか?

違います。これだと、子プロセスがクローズされた標準出力(あるいは標準エラー出力)に書き込もうとした瞬間に、SIGPIPE が発生することになります。SIGPIPE のデフォルトでの動作はプロセスの終了なので、子プロセスは異常終了することになります。

それでは、waitpid する前に、

() = <$rdr>; () = <$err>;

とすればいいのでしょうか?

まだ違います。これだと、子プロセスが標準エラー出力にバッファサイズを超える書き込みを行った場合に、親プロセスは子プロセスの標準出力から読みこもうとしたまま、子プロセスは標準エラー出力に出力しようとしたままの状態で、デッドロックに陥ります。

正解は、IPC::Open3 の次のような仕様を利用して、

If CHLD_ERR is false, or the same file descriptor as CHLD_OUT, then STDOUT and STDERR of the child are on the same filehandle.

my $pid = open3($wtr, $rdr, 0, @cmd);
() = <$rdr>;
waitpid($pid, 0);

とする形です*1。open3 の直後に close $wtr とすると、よりお行儀がいいかもしれませんね。

以上、ロートルの上から目線でのつぶやきでした。

18:13追記:

いるのはexit codeなのだから、リダイレクトで捨てちゃ駄目なの?

はてなブックマーク - mattnのブックマーク / 2010年8月13日

はい。今回の場合は読み捨てているだけなので、IPC::Open3 の以下の機能を利用して、

If CHLD_IN begins with "<&", then CHLD_IN will be closed in the parent, and the child will read from it directly. If CHLD_OUT or CHLD_ERR begins with ">&", then the child will send output directly to that filehandle. In both cases, there will be a dup(2) instead of a pipe(2) made.

my $pid = open3(
    '<&' . File::Spec->devnull,
    '>&' . File::Spec->devnull,
    0,
    @cmd,
);
waitpid($pid, 0);

としてもいいと思います。

*1:どうしても標準出力と標準エラー出力を読み分けたい場合は、select を使って読み込み可能な方のファイルハンドルから読む、というコードを書く必要があると思います