posix_spawnで子プロセスの出力をつなぐ、ファイル操作の順序に注目する

淡いラベンダーの橋に桃色のリボンが渡り、その横で小さなミント色の足場が外されている。

別のプログラムを起動して、その出力をファイルへ保存したい。そんな処理では、起動するプログラムの名前だけでなく、起動前に標準出力をどこへつなぐかも指定します。posix_spawnのfile actionsは、この準備を順序付きの操作として渡す仕組みです。「複製してから閉じる」と「閉じてから複製する」では、同じ二つの操作でも結果が変わります。

簡単にまとめると?

  • file actionsへの追加は、後で子プロセスに行わせる操作の登録です。追加した瞬間に親の標準出力を切り替える処理ではありません。
  • 登録したファイル操作は、子プロセスが対象プログラムを実行する前に、追加した順に適用されます。複製元を先に閉じると、その番号を次の複製に使えなくなります。
  • 操作の登録、プロセスの生成、対象プログラムの終了は別の確認点です。一段階の成功から、出力の保存まで成功したとは判断できません。

登録する操作と、実際に動く場所

posix_spawnは、新しい子プロセスで指定した実行ファイルを動かすための関数です。file_actions引数には、ファイルを開く、閉じる、番号を付け替えるといった準備をまとめたオブジェクトを渡せます。Linux man-pagesでは、子側の属性設定、登録されたファイル操作、FD_CLOEXECが付いた記述子のクローズという順序が説明されています。[1]

ファイル記述子は、入出力先を指定する整数の番号です。標準出力は1番です。ある出力先を1番へ割り当てるには、dup2に相当する操作を登録するposix_spawn_file_actions_adddup2を使えます。名前のaddは、その場で複製するのではなく、後で使う操作列へ追加する、という意味で読むと整理しやすくなります。[2]

たとえば親側で「3番を1番へ複製する」という操作を登録しても、それだけで親の標準出力が3番の出力先へ変わるわけではありません。その操作列を使って子を生成したときに、子側の起動準備として適用されます。親が自分のファイル記述子をいつ閉じるかは、別の寿命管理として考えます。

3番を1番へつなぎ、その後で3番を閉じる

具体例を、番号と出力先だけの小さなモデルで見てみます。準備開始時に1番は画面、3番は出力用ファイルを指し、3番は有効に開かれているとします。さらに、この例ではclose-on-execフラグや他の操作を含めません。実際のopenが必ず3を返す、という前提ではなく、ここでは得られた番号が3だった場合を扱います。

まず「dup2(3, 1)に相当する操作」、次に「close(3)に相当する操作」を登録します。最初の操作で、子側の1番は出力用ファイルへ向きます。二つ目の操作で子側の3番を閉じても、1番はその出力先への参照を保ちます。この順序なら、対象プログラムは標準出力の1番を使ってファイルへ書ける状態で始められます。書込み自体の成功まで保証する例ではありません。

順序の比較。複製してから3番を閉じると1番はファイルを参照する。3番を閉じてから複製しようとすると、複製元が無効になる。
独自の記述子表モデル。番号1と3だけを追い、実際のプロセス生成やファイル書込みは行っていない。

順序を逆にするとどうでしょうか。先にclose(3)を適用すると、3番はもう使えません。その後に3番から1番へ複製しようとしても、複製元がないため、このモデルでは無効な記述子として失敗します。間に別の操作を挟んで3番を開き直す場合などは、別の列として評価する必要があります。

独自スクリプトでは、この二つの列を辞書で表した記述子表へ順番に適用しました。前者は最終的に1番だけがファイルを指し、後者は二つ目の操作で停止することを確認しています。これは操作順を説明する計算モデルであり、Linux上でposix_spawnを呼んだ実測ではありません。実装ごとのエラーの返し方を、このモデルから推定しないようにします。

操作を追加できても、後で使えるとは限らない

参照したPOSIX.1-2017のadddup2の記述では、追加時点で操作を実行できない記述子でも、そのこと自体を追加時のエラーとはしない場合が説明されています。対応する操作列をposix_spawnなどで使う段階に、実行できないことが分かるという区別です。[2] 「追加関数が成功したから、この出力先は最後まで使える」とは言えません。

この違いは、設定を作る処理と、実際に起動する処理が離れている場合に効いてきます。起動に必要な記述子を、操作列へ番号を登録しただけで先に閉じてしまえば、後の準備で使えなくなるおそれがあります。番号の値だけでなく、どの処理がいつまで開いた状態を保つのかを追う必要があります。

また、同じ番号を読み元と書き先にする境界条件や、close-on-execの扱いを、この単純な例へ無条件に広げることも避けます。ここで比べたのは異なる番号3と1で、途中に他の操作がない列です。実装では番号の衝突、不要な記述子の継承、エラー時の片付けまで含めて、利用するOS・ライブラリの仕様に合わせます。

成功の記録を三段階に分ける

確認項目を並べるなら、第一に操作列を作れたか、第二に子の生成と実行準備が進んだか、第三に対象プログラムが期待どおり終了したかです。Linux man-pagesは、起動準備や実行に失敗した子が終了値127となる経路も説明しています。[1] 呼出しの戻り値と子の終了状態を別に扱い、特定の失敗が必ずどちらに現れるかは使用する実装で確認します。

出力ファイルの完成が目的なら、さらに出力内容の確認が残ります。プログラムが終了しても、出力が途中で途切れたり、想定した形式でなかったりする可能性は、入出力先の接続とは別です。操作列の順序を記録し、各段階の結果を分けて残すことで、接続の準備に問題があるのか、起動後の処理に問題があるのかを絞り込めます。

本稿は、参照版の文書に基づくファイル操作の順序の解説です。プロセス生成の性能比較や、全てのPOSIX版・実装に対する試験ではありません。同じ関数名でも、対象環境の版とエラー処理を確かめたうえで、必要な操作だけの列から検証することが実装への入口になります。

出典

  1. Linux man-pages 6.19、posix_spawn(3)。DESCRIPTIONのpre-exec、RETURN VALUE、NOTES。操作の順序と各段階の説明。
  2. POSIX Programmer’s Manual、posix_spawn_file_actions_adddup2(3p)。IEEE/The Open Group 2017の掲載版、DESCRIPTION・ERRORS・APPLICATION USAGE。最新版の規格本文と同一とは扱わない。両ページを2026年10月23日に確認。図と説明モデルは独自作成。