- 物理機リージョン/AZ
- ハイパーバイザVM exit で捕まえる
- VMゲストOS
- コンテナnamespace / cgroup
- プロセスアプリ / DB今ここ
アプリ層『Webサーバはただのプロセス — listen するとは何か』では socket(2) が返すのが fd であることまで見ました。今回はその番号の先を開きます。
まず結論
fd はファイルを指していません。open(2) は「fd は open file description への参照である」と書き、その open file description が「開いたファイルの表(システム全体)の1エントリ」で、オフセットとフラグはそこに記録されます。
つまり fd → open file description → ファイルの実体(inode)の三層です。同じファイルを2回 open するのと fd を dup するのとで結果が変わるのは、増えた層が違うからです。
同じファイルを2回 open して fd を2本得たあと、片方だけ lseek で50バイト目へ移動させました。もう片方の読み書き位置はどうなりますか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
番号の先は、ファイルではない
アプリ層『Webサーバはただのプロセス — listen するとは何か』で、socket(2) が返すのは fd だというところまで見ました。今回はその番号の先を開きます。
「fd はファイルを指す番号」と言いたくなりますが、open(2) はそう書いていません。
この1段落で三つの層が出そろいます。
- fd — "a reference to an open file description"。参照する側
- open file description — "an entry in the system-wide table of open files"。オフセットと状態フラグはここ
- ファイルの実体 — path が指す先。fd の参照は path が消えても壊れないと明言されている
層が分かれていることは、NOTES がもう一度書いています。
1つの inode に対して description が複数ありうる。description 層と実体の層は別という宣言です。
表の帰属も書き分けられています。description の表は system-wide。一方 dup(2) は新しい fd 番号を "the lowest-numbered file descriptor that was unused in the calling process" と規定します。第21週『ポートという多重化 — 1本の線に何本もの会話』の「fd 3番はそのプロセス内でのみ一意」は、この書き分けの帰結です。
オフセットがどこにあるか、数値で見る
Docker のコンテナ(Linux 6.10.14-linuxkit、aarch64、Python 3.12.14)で確かめます。同じファイルを2回 open して fd 3 と fd 4 を得、fd 3 を dup して fd 5 を作り、fd 3 だけを 42 バイト目へ動かしました。
=== /proc/self/fdinfo: pos is per open file description ===fd 3: pos: 42 | flags: 02400002 | mnt_id: 270fd 4: pos: 0 | flags: 02400002 | mnt_id: 270fd 5: pos: 42 | flags: 02400002 | mnt_id: 270
動かしていない fd 5 が 42 になり、同じファイルを開いただけの fd 4 は 0 のまま。 fd は3本、pos は2種類です。位置が fd ごとにあるなら3種類、ファイルごとにあるなら1種類のはずで、どちらでもありません。間に層があることが数値で見えます。
同じことを lseek の戻り値で追ったものです。
=== [1] two separate open() calls ===fd a=3 b=4initial offsets: a=0 b=0after lseek(a,50): a=50 b=0=== [2] dup() ===fd a=3 dup->c=5offsets: a=50 c=50after lseek(a,10): a=10 c=10
[1] は open 2回なので description が2つ、[2] は dup なので1つ。man-page 自身がこの挙動を例示しています。
/proc/<PID>/fdinfo は Linux 固有で、macOS や BSD にはありません。上の実験はすべて Docker のコンテナ内です。
読み書きの位置(オフセット)を保持しているのは、三層のうちどれですか。
何を増やしたのかで、結果が決まる
三層が分かると、「fd が増える場面」を1つの基準で仕分けできます。増えたのが参照(fd)か、状態(description)か。
openをもう一度呼ぶ → description が増える。オフセットは別々dupする → fd だけが増える。オフセットは共有forkする → fd 表はコピーされるが、指す先は同じ
3つ目は誤解しやすいので原文を置きます。
"copies of ... descriptors"(表はコピー)なのに "the same open file description"(指す先は同一)。二段構えが原文にそのまま書かれています。
- ・オフセットは fd にも inode にも無く、真ん中の層にある
- ・open(2): there may be multiple open file descriptions corresponding to a file inode
- ・FD_CLOEXEC は上の段(fd 表)にあるので dup しても共有されない
対比例: 書き込みで何が起きるか
オフセットがどの層にあるかは、書くときに結果へ現れます。同じファイルを O_APPEND 無しで2回 open し、片方に AAAAA、もう片方に BBBBB を書いた実測です。
=== [3] kcmp-free check: write via two independent fds (NO O_APPEND) ===file content after a.write+b.write (independent fds): b'BBBBB'file size: 5=== [4] same test with O_APPEND ===content with O_APPEND: b'AAAAABBBBB'
10バイト書いたのに5バイトしか残っていません。 両方の description がオフセット0を持っていたので、2本目が1本目を踏みました。[4] の O_APPEND 付きでは10バイトが残ります。この差は open(2) に書かれています。
注意が2つあります。ひとつは、man-page が書いているのは O_APPEND があるときアトミックだという側だけで、「無いと上書きになる」とは明文では書いていないことです。無い場合の挙動は「open ごとに独立した description がある」からの帰結で、[3] がその実測にあたります。
もうひとつは但し書きです。同じ項は続けて、複数プロセスが同時に追記する場合、NFS 上では O_APPEND がファイルを壊しうると書いています。NFS が追記に対応せずクライアント側のカーネルが模倣するため、競合が残るという理由です。
fork も見ます。
=== [6] fork: parent and child share offset? ===[child] offset after write: 5[parent] offset seen after child wrote: 5[parent] offset after own write: 11final content: b'CHILDPARENT'
親は何も書いていないのに、子が5バイト書いた後で親から見たオフセットが5になっています。だから親の PARENT は子の CHILD を踏まず、CHILDPARENT と連結されました。共有されていなければ親は0から書き、CHILD が消えていたはずです。
`dup(2)` で複製した2本の fd について、共有されるものとされないものの組み合わせが正しいのはどれですか。
同じファイルを2回 open する
open(2) は「ファイルを open するたびに新しい open file description が作られる」と書いています。description が2つできるので、オフセットも2つ。実測では fd 3 を 42 へ動かしても、fd 4 の pos は 0 のままでした。
fd を dup で増やす
dup(2) は「oldfd と同じ open file description を参照する新しい fd を割り当てる」と書いています。description は1つのまま。実測では fd 3 を 42 へ動かすと、触っていない fd 5 の pos も 42 になりました。
どちらも「fd が2本」ですが、片方だけ lseek したときの結果が逆になります。何が違うのでしょうか。
増えたのは参照する側か、参照される側か。オフセットがどちらに書かれているかと合わせて考えてみてください。
この章のまとめ
open(2)は **fd を「open file description への参照」**と定義している。fd はファイルを直接指してはいない。- オフセットとファイル状態フラグは open file description に記録される。 fd 表はプロセスごと、description の表はシステム全体。
openを2回呼ぶと description が2つできてオフセットは独立(実測a=50 b=0)。dupは fd だけを増やすので連動(実測a=10 c=10)。forkは表をコピーするが、指す先は同じ description。実測でも親子がオフセットを共有し、CHILDPARENTと連結された。dupで共有されるのは状態フラグ、共有されないのはFD_CLOEXEC。名前が似た2つが別の層にある。O_APPEND無しで2本の独立した fd から書くと上書きが起こる(実測で5バイトのBBBBBだけが残った)。man-page が明記しているのは「O_APPENDがあるときアトミック」という側だけ。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
「fd 表はプロセスごと、開いたファイルの表はシステム全体」という対比の根拠になるのはどれですか。
用語集(4語)
- ファイルディスクリプタ (file descriptor, fd)
- open(2) の言い方では「open file description への参照」。dup(2) は「呼び出したプロセス内で未使用の最小番号」を割り当てる。本教材では説明のため「表の番号」と呼ぶが、man-page が「添字」と書いているわけではない。
- open file description
- open(2) の言い方では「開いたファイルのシステム全体の表の1エントリ」。ファイルオフセットとファイル状態フラグを記録する。man-page 自身が「open file object」「file handle」「open file table entry」、カーネル開発者の言い方では「struct file」とも呼ばれると書いている。
- 2種類のフラグ
- ファイル状態フラグ(`O_APPEND` など)は open file description 側にあり、dup した fd の間で共有される。ファイルディスクリプタフラグ(FD_CLOEXEC)は fd 表の側にあり、dup(2) は「2本はこれを共有しない」と明記している。名前が似ているが別の層。
- O_APPEND
- open(2) の追記モード。「各 write(2) の前に、lseek(2) したかのようにファイルオフセットが末尾へ置かれる。オフセットの変更と write は単一のアトミックな手順として行われる」。ただし man-page は、複数プロセスが同時に追記する NFS 上では壊れうるとも書いている。