dupで複製したファイル記述子、読み取り位置はどこで共有されるか

淡い緑と紫の二本の帯が一つの桃色のしおりへ集まり、離れた溝には青いしおりが立つ抽象的な構図。

同じファイルを二つの処理で読むとき、ファイル記述子を複製すれば、それぞれが先頭から読めるのでしょうか。Linuxのdup()は、ファイルの中身や読み取り位置を丸ごとコピーする操作ではありません。増えるのは操作に使う番号であり、その先にある読み取り位置は共有されます。この違いを知ると、ログの読み出しや標準出力の付け替えで「別の番号なのに互いに影響する」理由を整理できます。

簡単にまとめると?

  • dup()で得た二つの番号は、同じ「オープンファイル記述」を参照するため、ファイル内の位置とファイル状態フラグを共有します。
  • 同じファイルを別々にopen()すれば、通常は位置を別々に持てます。ただし、ファイルの内容自体が別物になるわけではありません。
  • 特定の番号へ付け替えるdup2()は、閉じる操作と再利用を不可分に行います。番号の付け替えの安全性と、複数の読み書き全体の安全性は別の問題です。

番号、開いた状態、ファイル本体を分ける

ファイル記述子(file descriptor)は、プロセスが入出力に使う小さな整数です。標準入力の0、標準出力の1、標準エラー出力の2もこの番号です。番号を指定してread()やwrite()を呼ぶと、カーネル側の開いた状態を通じてファイルなどへアクセスします。

その開いた状態が、オープンファイル記述(open file description)です。通常のファイルでは「次にどの位置から読み書きするか」というファイルオフセットや、追記モードなどのファイル状態フラグを保持します。ディスク上のファイル本体、開いた状態、プロセス内の番号は、同じものではありません。

open()は新しいオープンファイル記述を作り、それを参照する番号を返します。一方、dup(oldfd)は既存のオープンファイル記述を参照する別の番号を返します。返るのは、そのプロセス内で使われていない最小の番号です。番号が二つあっても、読み取り位置の置き場は一つ、という関係になります。

二つの読み取りが一つの位置を進める

具体例として、内容がABCDEFGHの8バイトの通常ファイルを考えます。Aで開いた直後にdup()でBを作り、Aから3バイト、続けてBから2バイトを読みます。各呼び出しが指定バイト数を正常に読み、途中でほかの読み書きや位置変更がない場合、AはABC、BはDEを返します。BがABを返さないのは、Aが共有の位置を0から3へ進めたからです。最後の共有位置は5です。

dupの例ではAの3バイト読取り後にBがDEを読み共有位置は5になる。別々のopenではAがABC、BがABを読み位置は3と2に分かれる。
同じ8バイトの内容に対する順番どおりの読み取り例。短い読み取り、割込み、ほかの処理による変更は含めない説明モデル。

これに対し、同じファイルをAとBで別々にopen()すると、二つのオープンファイル記述ができます。同じ順で読むと、AはABC、BはABとなり、位置はそれぞれ3と2です。片方が先を読んでも、もう片方の位置はそれだけでは進みません。

したがって、読み出し処理ごとに独立した位置が必要なら、単に番号を複製するだけでは足りません。別々に開く設計などを検討します。ただし、これはデータのスナップショットを作る話ではありません。同じファイルが書き換えられれば、その内容の変化は独立した位置の問題とは別に扱う必要があります。

共有するフラグと、番号ごとのフラグ

読み取り位置と同様に、ファイル状態フラグもオープンファイル記述に属します。たとえば追記を指定するO_APPENDなどです。複製した片方を通して共有の状態を変更すれば、もう片方にも影響します。独立した番号だから設定も独立だ、と考えないことが大切です。

一方、ファイル記述子フラグは番号ごとの属性です。代表がFD_CLOEXECで、新しいプログラムへ置き換えるexec系操作の際に、その番号を閉じるかを指定します。Linuxのdup()で新しく得た番号では、このフラグは解除されています。元の番号のフラグが、そのまま引き継がれるわけではありません。

複製と同時にclose-on-execを指定したい場合、Linuxではdup3(oldfd, newfd, O_CLOEXEC)という選択肢があります。これはCのシステムコールの仕様についての説明です。別言語のライブラリが安全性のために継承設定を変えている場合もあるので、そのラッパーの仕様は分けて確認します。

標準出力を付け替えるとき、なぜdup2を使うのか

dup2(oldfd, newfd)は、複製先の番号を指定できます。たとえば、すでに開いてある出力先を番号1へ割り当てれば、その後の標準出力をその開いた状態へ向けられます。既存のnewfdが開いていれば、それを閉じて番号を再利用します。

ここをclose(newfd)とdup(oldfd)の二段階に分けると、間に別のスレッドやシグナルハンドラーがファイルを開き、空いた番号を先に使う可能性があります。dup2()は、閉じる操作とその番号の再利用を不可分に行うことで、この隙間をなくします。「不可分」という保証の対象はこの付け替えであり、一連の業務処理や複数回の読み書きをまとめて保護する保証ではありません。

細部にも違いがあります。有効なoldfdとnewfdが同じなら、dup2()は何もせずその番号を返します。dup3()では同じ番号の指定はエラーです。またdup2()で元の番号が無効なら、複製先は閉じられません。失敗を「もう閉じられたはず」と決めつけず、戻り値とエラーを確認する必要があります。

用途に合わせて確認する三つの点

選び方は、何を共有したいかから考えられます。第一に、同じ読み取り位置を使うのか、別々に進みたいのか。第二に、追記などの状態フラグを共有してよいのか。第三に、番号を指定して付け替える必要と、exec時に番号を残す必要があるのかです。

今回の例は、位置を持つ通常ファイルに対する逐次処理です。パイプやソケットを同じ「ファイルの何バイト目」という図で扱うことはできません。また、実際のread()は常に要求した量を返すとは限りません。共有構造の理解と、短い読み取り・エラー・並行処理への対処を組み合わせてはじめて、実装の判断につながります。

出典・参考資料