Python で open() を呼んだとき、内部では何が起きているのか。「リソースを確保する」「ファイルを閉じる」とは具体的に何を意味するのか。OS とカーネルのレベルまで掘り下げて解説する。
ファイルを開くとは何か
Python の open("data.txt") は、最終的に OS のシステムコール open() を呼び出す。このシステムコールが返すのは「ファイルディスクリプタ」と呼ばれる小さな整数だ。
import os # Python の open() が返すファイルオブジェクト f = open("data.txt") # その内部にあるファイルディスクリプタ(整数) print(f.fileno()) # 例: 3
ファイルディスクリプタは、カーネルが管理するリソースへの「チケット番号」のようなものだ。プログラムはこの番号を使って「3 番のファイルから読んでくれ」とカーネルに依頼する。
カーネル内部の 3 つのテーブル
ファイルを開くと、カーネル内部で 3 つのデータ構造が関係する。
各プロセスが持つテーブル。ファイルディスクリプタ(整数)から、システム全体のオープンファイルテーブルへのポインタを保持する。
カーネルが管理するテーブル。ファイルの現在位置(オフセット)、読み書きモード、参照カウントなどを保持する。
実際のファイルのメタデータ。ファイルサイズ、パーミッション、ディスク上のブロック位置などを保持する。inode はファイルシステム上の「実体」への参照。
この 3 層構造のおかげで、同じファイルを複数のプロセスが異なる位置で読み書きできる。
open() システムコールで起きること
C 言語で書くと、ファイルを開く処理は以下のようになる。
#include <fcntl.h> #include <unistd.h> int main() { // open() システムコール int fd = open("data.txt", O_RDONLY); // read() でデータを読む char buf[100]; ssize_t n = read(fd, buf, sizeof(buf)); // close() でリソースを解放 close(fd); return 0; }
open() が呼ばれると、カーネル内部で以下の処理が行われる。
パス名を解決して inode を特定
オープンファイルテーブルにエントリを作成
プロセスの FD テーブルに空きスロットを確保
ファイルディスクリプタ(整数)を返す
「リソース」とは何か
ファイルを開いたときに確保される「リソース」は、メモリとは異なる。
malloc() で確保、free() で解放。プロセス内で自由に使える領域。
カーネルが管理する番号。プロセスごとに上限がある(デフォルトで 1024 程度)。カーネル全体でも上限がある。
# プロセスあたりの FD 上限を確認 ulimit -n # 1024 # システム全体の上限を確認 cat /proc/sys/fs/file-max # 9223372036854775807
ファイルを閉じ忘れると、この「枠」を消費し続ける。大量のファイルを開いたまま放置すると「Too many open files」エラーになる。
close() で何が解放されるか
close() システムコールは以下の処理を行う。
プロセスのファイルディスクリプタテーブルから該当エントリを削除し、その番号を再利用可能にする。
参照カウントがゼロになれば、オープンファイルテーブルのエントリも削除される。fork() で子プロセスと共有している場合は、両方が close() するまで残る。
オープンファイルテーブルと同様、参照がゼロになれば inode もキャッシュから解放される可能性がある。
fork() とファイルディスクリプタ
プロセスを fork() すると、ファイルディスクリプタテーブルがコピーされる。親子は同じオープンファイルテーブルエントリを共有するため、ファイルオフセットも共有される。
int fd = open("data.txt", O_RDONLY); pid_t pid = fork(); if (pid == 0) { // 子プロセス: 5 バイト読む char buf[5]; read(fd, buf, 5); // オフセットが 5 に進む } else { // 親プロセス: 子の後に読むと 6 バイト目から始まる wait(NULL); char buf[5]; read(fd, buf, 5); // オフセット 5 から読む }
これが「オープンファイル記述」を共有するということだ。
Python の with 文とシステムコール
Python の with 文は、最終的に close() システムコールを呼ぶ。
# これは with open("data.txt") as f: data = f.read() # 内部的にはこうなっている(簡略化) f = open("data.txt") # → open() システムコール try: data = f.read() # → read() システムコール finally: f.close() # → close() システムコール
with 文が保証するのは「どんな例外が起きても close() システムコールが呼ばれる」ということだ。
なぜ「閉じる」必要があるのか
ファイルを閉じないとどうなるか。
プロセスあたりの上限(通常 1024)に達すると、新しいファイルを開けなくなる。
書き込みはバッファリングされており、close() や flush() するまでディスクに書き込まれない可能性がある。
排他ロックを取得したまま閉じないと、他のプロセスがファイルにアクセスできない。
まとめ
open() でファイルオブジェクトを取得、close() で解放。with 文で自動管理。
システムコール open() で FD を取得、close() でカーネルのテーブルエントリを解放。FD はカーネルが管理する有限リソース。
「ファイルを開く」とは、カーネルにお願いして番号札(FD)をもらうこと。「ファイルを閉じる」とは、その番号札を返却してカーネル内のテーブルをクリーンアップすること。メモリとは異なる、OS レベルのリソース管理が行われている。