LoginSignup
2
2

Perl で子プロセスが入力を読めない問題

Last updated at Posted at 2014-12-29

問題

問題は、このスクリプトを実行しても何も出力しないことだ。
なぜ何も出力されないかというと、3行目の <> で何も読み込まれないから。

$_ = do { local $/; <> };
if (open(CHLD, '|-') == 0) {
    print <>;
    exit;
}
print CHLD $_;

考察

コマンドを実行すると動く

出力する部分を外部コマンドで実行すると問題なく動作する。

$_ = do { local $/; <> };
if (open(CHLD, '|-') == 0) {
    exec "cat" or warn $!;
    exit;
}
print CHLD $_;

読み込む場所を変えると動く

fork した後で親プロセスがデータを読み込むように変更してもうまくいく。

if (open(CHLD, '|-') == 0) {
    print <>;
    exit;
}
$_ = do { local $/; <> };
print CHLD $_;

@_ に読み込めば問題ない

これは不思議なのだが、次のプログラムは問題なく動作する。

@_ = <>;
if (open(CHLD, '|-') == 0) {
    print <>;
    exit;
}
print CHLD @_;

ところが、読み込んだ後で STDIN の状態を調べるとやはり動かなくなってしまう。

@_ = <>;
warn "eof\n" if eof STDIN;
if (open(CHLD, '|-') == 0) {
    print <>;
    exit;
}
print CHLD @_;

原因は STDIN の eof フラグが立っていること

問題の原因は、fork した子プロセスが STDIN から読み込もうとした時に eof の状態にあるからだ。

親プロセスが STDIN を最後まで読み込んだことで、STDIN のファイルハンドルに eof フラグが立つ。その後 open(CHLD, '|-') で fork すると、子プロセスの STDIN はパイプに置き換えられるのだが、どうも eof フラグはそれまでのままになっているようだ。

解決策

処理系のバグと言っていいような気もするが、修正を待っても仕方ないので対処方法を考える。

seek は使えない

まず思いつくのは seek を使うことだ。seek STDIN, 0, 0 すると、ファイルの先頭に入出力ポジションを設定することができる。しかし、パイプに seek はできないので、この場合はうまくいかない。

読んでみる

eof 状態のファイルハンドルから読み込みを行っても何も返ってはこないが、それによって eof フラグは解除される。だから、とりあえず読んであげればよい。

$_ = do { local $/; <> };
if (open(CHLD, '|-') == 0) {
    <> if eof STDIN;
    print <>;
    exit;
}
print CHLD $_;

fdopen する

上のやり方でも機能的には問題ないのだが、どうも苦し紛れ感が拭えない。

次のようにすると C の fdopen 相当のことができる。そうすることでファイルハンドルが初期化されるようで、無事読み込めるようになる。

$_ = do { local $/; <> };
if (open(CHLD, '|-') == 0) {
    open STDIN, '<&', 0 if eof STDIN;
    print <>;
    exit;
}
print CHLD $_;

pipe を使う

当たり前かもしれないが、自分で pipe を準備すれば問題なく動作する。
しかし、こういうのが面倒だから用意されている機能のはずなのだが...

$_ = do { local $/; <> };
pipe PIN, POUT or die;
if (fork == 0) {
    close STDIN;
    open STDIN, '<&PIN';
    close PIN;
    close POUT;
    print <>;
    exit;
}
close PIN;
print POUT $_;

結論

上の選択肢の中では fdopen 相当のことをやるのが最善だろうか。

ということで [greple][greple] には、[このようなパッチ][1142c4d]を当てることにした。
[greple]:https://github.com/kaz-utashiro/greple
[1142c4d]:https://github.com/kaz-utashiro/greple/commit/1142c4d

なぜこのような修正が必要になったかというと、大量のファイルを検索する時に出力フィルタを設定すると、何も出力しなくてもファイルの数だけプロセスが起動されてしまうからだ。だから、データを読み込んでから子プロセスを作成する必要がある。

greple の -Mical というモジュールを使うと、OS X のカレンダーファイルを全部検索することができる。これはイベント毎に別ファイルになっているので、大量のファイルが対象になるのだ。ヒット数が多いと極端に遅くなってしまうが、ヒットしないファイルの処理は速いので、2011年製 MBP で 13000個以上のファイルを検索しても1秒やそこらの実用的な時間で終了する。

追記

v5.20.1 では直っていない。
v5.26.0 でも直っていない。
v5.28.0 でも直っていない。
v5.30.3, v5.34.0 でも直ってない。

2
2
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2