第2章: リソース抽象化とハンドルの理論 - ファイルディスクリプタの計算機科学的基盤
はじめに:なぜ「ハンドル」が必要だったのか
プログラムが外部リソース(ファイル、ネットワーク接続、デバイス)にアクセスする方法は、コンピュータ科学の根本的な問題の一つです。この章では、リソース抽象化の歴史と理論から始め、Unixファイルディスクリプタがなぜこのような設計になったのかを深く理解します。
2.1 リソース抽象化の歴史
2.1.1 初期のコンピュータとダイレクトI/O
1950年代:直接制御の時代
最初期のコンピュータでは、プログラムがハードウェアを直接制御していました。
ENIAC (1945)
├── パッチケーブルでプログラム
├── I/Oは物理的なスイッチとランプ
└── プログラマが完全なハードウェア制御
UNIVAC I (1951)
├── 磁気テープ駆動
├── プログラムがテープ制御を直接実行
├── READ命令:デバイスアドレスを直接指定
└── 複数プログラムの共存は不可能
この時代の「プログラム」は、ハードウェアと一体でした:
; 1950年代風の擬似コード
; テープドライブ0からデータを読む
LOAD TAPE_CTRL_ADDR ; テープコントローラのアドレス
SET READ_CMD ; 読み取りコマンドを設定
SET BLOCK_NUM, 42 ; ブロック番号
WAIT TAPE_READY ; デバイスが準備完了するまで待機
STORE DATA_ADDR ; 結果をメモリに格納
問題点:
- プログラムがハードウェアに完全に依存
- デバイスが変わるとプログラムを書き直し
- 複数プログラムがリソースを共有できない
- エラーハンドリングがプログラマの責任
2.1.2 チャネルプログラミングの登場(1960年代)
IBM System/360(1964)の革新
IBM System/360は「チャネル」という概念を導入しました。これは現代のI/Oサブシステムの原型です:
System/360 I/Oアーキテクチャ
┌─────────────────────────────────────────────────────┐
│ CPU │
│ ┌─────────────────────────────────────────────────┐ │
│ │ プログラム │ │
│ │ - EXCP (Execute Channel Program) を発行 │ │
│ │ - デバイス制御から解放 │ │
│ └───────────────────────────────────┬─────────────┘ │
└─────────────────────────────────────┼───────────────┘
│
┌───────▼───────┐
│ チャネル │
│ (Channel) │
├───────────────┤
│ CCW を解釈 │
│ データ転送 │
│ 割込み生成 │
└───────┬───────┘
│
┌───────────────────────┴───────────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ 磁気テープ │ │ ディスク │ │ プリンタ │
│ ドライブ │ │ ドライブ │ │ │
└───────────┘ └───────────┘ └───────────┘
Channel Command Word (CCW) は、I/O操作の「プログラム」でした:
CCW の構造:
┌────────────────────────────────────────────────────────────────┐
│ Command │ Data Address │ Flags │ Reserved │ Count │
│ (8 bit) │ (24 bit) │ (8) │ (8) │ (16 bit) │
└────────────────────────────────────────────────────────────────┘
Command codes:
- 0x01: Write
- 0x02: Read
- 0x03: NOP
- 0x07: Seek (ディスク位置決め)
チャネルの意義:
2.1.3 Multics:ファイルの概念の誕生(1965年)
MIT、Bell Labs、General Electricの共同プロジェクトMulticsは、「ファイル」という抽象化を本格的に導入しました:
Multics ファイルシステム
├── 階層的ディレクトリ構造
├── セグメントベースのアドレス空間
├── ファイル = 仮想メモリセグメント
└── アクセス制御リスト(ACL)
Multicsの革新的概念:
1. ファイル = 名前付きバイト列
2. ディレクトリ = ファイルのコレクション
3. パス名 = >usr>bin>ls(> がセパレータ)
4. シンボリックリンク
5. ACLによるセキュリティ
Multicsのファイルアクセスモデル:
; Multicsのファイルアクセス(PL/I風疑似コード)
dcl file_ptr pointer;
dcl segment_ptr pointer;
call hcs_$initiate_count(
directory, /* ディレクトリパス */
entry_name, /* ファイル名 */
"", /* 参照名(省略可) */
bit_count, /* OUT: ファイルサイズ */
0, /* アクセスモード */
segment_ptr, /* OUT: セグメントポインタ */
code /* OUT: エラーコード */
);
/* segment_ptr を通じてファイルに直接アクセス */
/* メモリマップドI/Oの先駆け */
問題点:
- 非常に複雑(数百万行のコード)
- ハードウェア要件が高い
- パフォーマンスの問題
2.1.4 Unix:シンプルさへの回帰(1969年)
Ken ThompsonとDennis Ritchieは、Multicsの複雑さに対する反動としてUnixを設計しました:
設計哲学: > "Keep it simple, stupid" (KISS) > "Do one thing and do it well" > "Everything is a file"
Unixの根本的な簡略化:
Multicsの複雑さ Unixの簡略化
├── セグメントアドレス空間 ├── 単純なバイト配列
├── 複雑なACL ├── user/group/other パーミッション
├── 高度なファイル属性 ├── 最小限のメタデータ
└── 数百万行のコード └── 数千行のコード
Unixファイルモデル:
┌─────────────────────────────────────────────────────┐
│ ファイル = 名前のないバイト列 │
│ │
│ - 内部構造なし(OSは関与しない) │
│ - レコード概念なし(アプリケーションの責任) │
│ - シーケンシャルまたはランダムアクセス │
│ - 固定サイズまたは可変サイズ │
└─────────────────────────────────────────────────────┘
ファイルディスクリプタの誕生:
Unix Version 1(1971)で、現代のファイルディスクリプタの原型が登場:
/* Unix V1 のファイルシステムコール(Ken Thompson, 1971)*/
/* PDP-7 アセンブリから C への進化 */
/* ファイルを開く */
fd = open(name, mode); /* 戻り値:小さな整数 */
/* 読み書き */
n = read(fd, buf, count);
n = write(fd, buf, count);
/* 閉じる */
close(fd);
なぜ「整数」だったのか?
初期Unixの設計制約:
├── PDP-7/PDP-11 のメモリ制限(最大64KB)
├── プロセスあたりの最大オープンファイル数 = 15
├── テーブルサイズの最小化が必須
└── ポインタより整数が省メモリ
ファイルディスクリプタ = 配列インデックス
┌─────────────────────────────────────────┐
│ struct file *u_ofile[NOFILE]; │
│ /* NOFILE = 15 in early Unix */ │
│ │
│ fd = 配列のインデックス (0, 1, 2, ...) │
│ u_ofile[fd] = ファイル構造体へのポインタ │
└─────────────────────────────────────────┘
この「整数をハンドルとして使う」設計は、偶然ではなく、リソース制約と設計哲学の産物でした。
2.2 ハンドルの理論
2.2.1 ハンドル(Handle)とは何か
計算機科学において、ハンドルは間接参照(indirection)の形態であり、リソースへのアクセスを抽象化します:
ハンドルの定義:
Handle = リソースを識別する不透明な(opaque)参照
特性:
1. 不透明性(Opacity)
- クライアントはハンドルの内部構造を知らない
- 「魔法の数字」として扱う
2. 間接参照(Indirection)
- ハンドル → 内部データ構造 → 実リソース
- レベルの追加による柔軟性
3. 有効期間(Validity)
- 作成から破棄までの間のみ有効
- 無効なハンドルの使用は未定義動作
ハンドルと直接ポインタの比較:
/* 直接ポインタ方式(危険) */
FILE *fp = (FILE *)malloc(sizeof(FILE));
/* クライアントがポインタを自由に操作可能 */
/* 解放後アクセス、二重解放の危険 */
/* ハンドル方式(安全) */
int fd = open("file.txt", O_RDONLY);
/* fdは単なる整数:解釈はカーネルが行う */
/* 無効なfdはエラーとして検出可能 */
2.2.2 ハンドルベース設計の利点
1. カプセル化(Encapsulation):
クライアント システム
│ │
│ open("file.txt") │
├───────────────────────────►│
│ │ 内部構造を作成
│ 3 │ ファイルテーブルに登録
│◄───────────────────────────┤ インデックスを返す
│ │
│ read(3, buf, 100) │
├───────────────────────────►│
│ │ ハンドルを検証
│ │ 内部構造を参照
│ データ │ I/O実行
│◄───────────────────────────┤
│ │
クライアントは以下を知らない:
- ファイルがどこに保存されているか
- どのデバイスを使用しているか
- バッファリングの仕組み
- カーネルのデータ構造
2. セキュリティと検証:
/* カーネルはハンドルを検証できる */
ssize_t sys_read(int fd, void *buf, size_t count)
{
/* 範囲チェック */
if (fd < 0 || fd >= OPEN_MAX)
return -EBADF;
/* 有効性チェック */
struct file *f = current->files->fd_array[fd];
if (!f)
return -EBADF;
/* 権限チェック */
if (!(f->f_mode & FMODE_READ))
return -EBADF;
/* 実際の読み取り */
return vfs_read(f, buf, count);
}
3. リソース追跡とライフサイクル管理:
ハンドルベースリソース管理
┌─────────────────────────────────────────────────────────────┐
│ │
│ プロセス終了時: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ for (fd = 0; fd < OPEN_MAX; fd++) { │ │
│ │ if (files->fd_array[fd]) { │ │
│ │ /* 自動的にすべてのハンドルを閉じる */ │ │
│ │ close(fd); │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ → ハンドルテーブルを走査するだけで全リソースを解放可能 │
│ │
└─────────────────────────────────────────────────────────────┘
2.2.3 Capability-based Security の観点
ファイルディスクリプタは、ケイパビリティ(capability) の一形態とも見なせます:
ケイパビリティ理論(Dennis & Van Horn, 1966)
───────────────────────────────────────────
定義:
Capability = (Object Reference, Access Rights)
ケイパビリティを持つ ⟺ 操作を行う権限がある
Unixファイルディスクリプタとの対応:
┌────────────────────────────────────────────────────────────┐
│ │
│ fd = open("file.txt", O_RDONLY); │
│ │
│ このfdは以下を表す: │
│ 1. Object Reference: ファイル "file.txt" │
│ 2. Access Rights: 読み取りのみ(O_RDONLY) │
│ │
│ fdを持つプロセスは: │
│ - read(fd, ...) を呼び出せる ✓ │
│ - write(fd, ...) を呼び出せない ✗ (EBADF) │
│ │
└────────────────────────────────────────────────────────────┘
ケイパビリティの移転:
/* Unixでのケイパビリティ(fd)の移転 */
/* 1. fork()による継承 */
pid_t pid = fork();
/* 子プロセスは親のfdを継承 */
/* → ケイパビリティが複製される */
/* 2. dup()/dup2()による複製 */
int fd2 = dup(fd);
/* 同じファイルへの別のケイパビリティ */
/* 3. SCM_RIGHTSによるプロセス間転送 */
/* Unix domain socket経由でfdを他プロセスに送信 */
struct msghdr msg;
struct cmsghdr *cmsg;
/* ... 複雑なセットアップ ... */
sendmsg(socket_fd, &msg, 0);
/* → ケイパビリティがプロセス間を移動 */
2.3 カーネルのファイル管理アーキテクチャ
2.3.1 三層構造の理論
Unixカーネルは、ファイルアクセスを三層構造で管理します。これは関心の分離(Separation of Concerns) の具現化です:
Unix ファイル管理の三層構造
═══════════════════════════════════════════════════════════
Layer 1: Per-Process File Descriptor Table
─────────────────────────────────────────
責務:プロセスごとのファイル参照を管理
┌──────────────────────────────────────────────────────────┐
│ Process A Process B │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ fd_table │ │ fd_table │ │
│ ├────────────────┤ ├────────────────┤ │
│ │ [0] stdin ────┼─────────────►│ [0] stdin │ │
│ │ [1] stdout ────┼──┐ │ [1] stdout │ │
│ │ [2] stderr │ │ │ [2] stderr │ │
│ │ [3] ──────────┐│ │ │ [3] ──────┐ │ │
│ │ [4] ────────┐ ││ │ └───────────┼────┘ │
│ └─────────────┼─┼┘ │ │ │
│ │ │ │ │ │
└───────────────┼─┼───┼──────────────────────┼───────────┘
│ │ │ │
▼ ▼ ▼ ▼
═══════════════════════════════════════════════════════════
Layer 2: System-wide Open File Table
─────────────────────────────────────
責務:開いているファイルのインスタンス情報を管理
┌──────────────────────────────────────────────────────────┐
│ Open File Table (struct file) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Entry #1 Entry #2 Entry #3 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │f_pos: 0 │ │f_pos: 1024│ │f_pos: 512│ │ │
│ │ │f_mode:RD │ │f_mode:RW │ │f_mode:WR │ │ │
│ │ │f_flags │ │f_flags │ │f_flags │ │ │
│ │ │f_count:2 │ │f_count:1 │ │f_count:3 │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │ └──┼───────┘ └──┼───────┘ └──┼───────┘ │ │
│ └────┼──────────────┼──────────────┼─────────────────┘ │
│ │ │ │ │
└──────┼──────────────┼──────────────┼─────────────────────┘
│ │ │
▼ ▼ ▼
═══════════════════════════════════════════════════════════
Layer 3: Inode Table (VFS Layer)
────────────────────────────────
責務:実際のファイル/デバイスへの参照を管理
┌──────────────────────────────────────────────────────────┐
│ In-memory Inode Table │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Inode #1 (file.txt) Inode #2 (data.bin) │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ i_mode: 0644 │ │ i_mode: 0600 │ │ │
│ │ │ i_size: 4096 │ │ i_size: 2048 │ │ │
│ │ │ i_uid: 1000 │ │ i_uid: 0 │ │ │
│ │ │ i_gid: 1000 │ │ i_gid: 0 │ │ │
│ │ │ i_nlink: 2 │ │ i_nlink: 1 │ │ │
│ │ │ i_blocks[...] │ │ i_blocks[...] │ │ │
│ │ │ i_op: ext4_ops │ │ i_op: ext4_ops │ │ │
│ │ └──────────────────┘ └──────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Disk Blocks │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
2.3.2 各層の詳細設計
Layer 1:プロセスファイルディスクリプタテーブル
/* Linux カーネル 6.x における実装(簡略化) */
/* プロセス構造体 */
struct task_struct {
/* ... 多くのフィールド ... */
/* ファイルシステム情報 */
struct fs_struct *fs; /* カレントディレクトリ等 */
struct files_struct *files; /* ファイルディスクリプタテーブル */
/* ... */
};
/* ファイルディスクリプタテーブル */
struct files_struct {
atomic_t count; /* 参照カウント */
struct fdtable *fdt; /* ファイルディスクリプタテーブル */
struct fdtable fdtab; /* 埋め込みテーブル(小規模用) */
spinlock_t file_lock; /* ロック */
/* RCU (Read-Copy-Update) サポート */
struct rcu_head rcu;
};
/* fdtable 構造体 */
struct fdtable {
unsigned int max_fds; /* テーブルサイズ */
struct file **fd; /* ファイルポインタの配列 */
unsigned long *close_on_exec; /* FD_CLOEXEC ビットマップ */
unsigned long *open_fds; /* オープン済みfdのビットマップ */
};
Layer 2:オープンファイルテーブル
/* struct file - オープンされたファイルのインスタンス */
struct file {
/* ファイル位置 */
loff_t f_pos; /* 現在のオフセット */
/* アクセスモード */
fmode_t f_mode; /* FMODE_READ, FMODE_WRITE */
unsigned int f_flags; /* O_RDONLY, O_NONBLOCK 等 */
/* 参照カウント */
atomic_long_t f_count; /* 複数fdが同じfileを参照可能 */
/* VFS操作 */
const struct file_operations *f_op; /* read, write等の関数ポインタ */
/* inode参照 */
struct inode *f_inode; /* 対応するinode */
struct path f_path; /* ファイルパス情報 */
/* ... その他多数のフィールド ... */
};
Layer 3:inode(Index Node)
/* struct inode - ファイルのメタデータ */
struct inode {
/* 識別 */
unsigned long i_ino; /* inode番号 */
dev_t i_rdev; /* デバイス番号(デバイスファイル用) */
/* 所有権とパーミッション */
umode_t i_mode; /* ファイルタイプとパーミッション */
uid_t i_uid; /* 所有者UID */
gid_t i_gid; /* グループGID */
/* サイズとブロック */
loff_t i_size; /* ファイルサイズ */
blkcnt_t i_blocks; /* 使用ブロック数 */
/* 時間情報 */
struct timespec64 i_atime; /* 最終アクセス時間 */
struct timespec64 i_mtime; /* 最終変更時間 */
struct timespec64 i_ctime; /* メタデータ変更時間 */
/* リンク */
unsigned int i_nlink; /* ハードリンク数 */
/* 操作関数テーブル */
const struct inode_operations *i_op; /* ディレクトリ操作等 */
const struct file_operations *i_fop; /* ファイル操作 */
/* ... */
};
2.3.3 仮想ファイルシステム(VFS)の設計
VFSは、異なるファイルシステムに共通のインターフェースを提供するカーネルサブシステムです:
VFS の設計パターン:Strategy Pattern + Template Method
══════════════════════════════════════════════════════════
User Space
────────────────────────────────────────────────────────
read(fd, buf, 100)
│
═══════════│════════════════════════════════════════════
│ Kernel Space
▼
┌──────────────┐
│ System Call │
│ Interface │
└──────┬───────┘
│
▼
┌──────────────────────────────────────────┐
│ VFS Layer │
│ ┌────────────────────────────────────┐ │
│ │ vfs_read(file, buf, count, pos) │ │
│ │ │ │
│ │ // 共通の事前処理 │ │
│ │ rw_verify_area(READ, file, ...) │ │
│ │ │ │
│ │ // ファイルシステム固有の実装を呼出 │ │
│ │ file->f_op->read(file, buf, ...) │ │
│ │ │ │ │
│ └─────────┼──────────────────────────┘ │
└────────────┼─────────────────────────────┘
│
┌────────────┼────────────────────────────────────┐
│ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ext4_read() │ │ nfs_read() │ │
│ │ (Local FS) │ │ (Network FS) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ Block Layer Network Stack │
│ │ │ │
│ ▼ ▼ │
│ Local Disk NFS Server │
│ │
│ File System Implementations │
└─────────────────────────────────────────────────┘
ポリモーフィズムの実現:
/* file_operations 構造体 - インターフェース定義 */
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
/* ... 多くの操作 ... */
};
/* ext4 ファイルシステムの実装 */
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read = ext4_file_read_iter,
.write = ext4_file_write_iter,
.open = ext4_file_open,
/* ... */
};
/* /proc ファイルシステムの実装 */
const struct file_operations proc_file_operations = {
.read = proc_file_read,
.write = proc_file_write,
/* ... */
};
/* パイプの実装 */
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.read = pipe_read,
.write = pipe_write,
/* ... */
};
これにより、read(fd, buf, count) という単一のシステムコールで:
- ローカルディスク上のファイル
- ネットワーク越しのNFSファイル
- /proc/cpuinfo のような仮想ファイル
- パイプやソケット
すべてに対応できます。
2.4 参照カウントの理論
2.4.1 参照カウント(Reference Counting)
参照カウントは、リソースライフサイクル管理の基本的な手法です:
参照カウントの理論
═══════════════════════════════════════════════════════
定義:
リソースへの参照の数を追跡し、
参照がゼロになった時点でリソースを解放する
性質:
1. 即座の解放(Immediate Deallocation)
- 最後の参照が削除されると即座に解放
2. 決定的なライフサイクル
- いつ解放されるか予測可能
3. 循環参照問題
- 相互参照するオブジェクトは解放されない
(ファイルシステムでは通常問題にならない)
操作:
acquire(resource) → count++
release(resource) → count--; if (count == 0) free(resource)
Unixファイルシステムでの参照カウント:
/* struct file の参照カウント */
struct file {
atomic_long_t f_count; /* 参照カウント */
/* ... */
};
/* 参照の取得 */
static inline struct file *get_file(struct file *f)
{
atomic_long_inc(&f->f_count);
return f;
}
/* 参照の解放 */
void fput(struct file *file)
{
if (atomic_long_dec_and_test(&file->f_count)) {
/* 最後の参照 → ファイルを閉じる */
__fput(file);
}
}
2.4.2 参照カウントの例:dup() と fork()
dup() による参照カウントの変化
═══════════════════════════════════════════════════════
初期状態:
┌──────────────┐ ┌──────────────┐
│ Process A │ │ struct file │
│ fd_table │ ├──────────────┤
├──────────────┤ │ f_count: 1 │
│ [3] ─────────┼────►│ f_pos: 0 │
└──────────────┘ └──────────────┘
dup(3) 実行後:
┌──────────────┐ ┌──────────────┐
│ Process A │ │ struct file │
│ fd_table │ ├──────────────┤
├──────────────┤ │ f_count: 2 │←── カウント増加
│ [3] ─────────┼──┬─►│ f_pos: 0 │
│ [4] ─────────┼──┘ └──────────────┘
└──────────────┘
同じ struct file を共有
fork() による参照カウントの変化
═══════════════════════════════════════════════════════
fork() 前:
┌──────────────┐ ┌──────────────┐
│ Parent │ │ struct file │
│ fd_table │ ├──────────────┤
├──────────────┤ │ f_count: 1 │
│ [3] ─────────┼────►│ f_pos: 100 │
└──────────────┘ └──────────────┘
fork() 後:
┌──────────────┐
│ Parent │ ┌──────────────┐
│ fd_table │ │ struct file │
├──────────────┤ ├──────────────┤
│ [3] ─────────┼──┬─►│ f_count: 2 │←── カウント増加
└──────────────┘ │ │ f_pos: 100 │
│ └──────────────┘
┌──────────────┐ │
│ Child │ │ 親子で同じファイルを共有
│ fd_table │ │ (オフセットも共有!)
├──────────────┤ │
│ [3] ─────────┼──┘
└──────────────┘
2.4.3 close() と参照カウント
/* close() システムコールの内部動作 */
int close(int fd)
{
struct file *file;
/* 1. fdテーブルからファイルを取得 */
file = current->files->fd_array[fd];
if (!file)
return -EBADF;
/* 2. fdテーブルからエントリを削除 */
current->files->fd_array[fd] = NULL;
/* 3. 参照カウントをデクリメント */
fput(file); /* カウントが0なら解放 */
return 0;
}
close() のタイミングと効果:
シナリオ: dup() 後の close()
════════════════════════════════════════════════════
int fd1 = open("file.txt", O_RDONLY); // f_count = 1
int fd2 = dup(fd1); // f_count = 2
close(fd1); // f_count = 1
// ファイルはまだ開いている!
read(fd2, buf, 100); // fd2 は有効
close(fd2); // f_count = 0
// ここでファイルが閉じられる
シナリオ: fork() 後の close()
════════════════════════════════════════════════════
int fd = open("file.txt", O_RDONLY); // f_count = 1
pid_t pid = fork(); // f_count = 2
if (pid == 0) {
/* Child */
close(fd); // f_count = 1
_exit(0); // ファイルはまだ開いている
}
/* Parent */
wait(NULL);
read(fd, buf, 100); // fd は有効
close(fd); // f_count = 0、ファイルが閉じる
2.5 リソースリークの理論
2.5.1 リソースリークとは
リソースリーク(Resource Leak)の定義
═══════════════════════════════════════════════════════
リソースリーク = 確保したリソースを解放せずに
その参照を失うこと
結果:
1. リソース枯渇
- ファイルディスクリプタの上限到達
- メモリ不足
2. システムパフォーマンス低下
- カーネルがより多くのリソースを管理
- キャッシュ効率の低下
3. 予測不能な障害
- 長時間実行後に突然の障害
- デバッグ困難な問題
ファイルディスクリプタリークの例:
/* リークパターン 1: 早期リターン */
int process_file(const char *path)
{
int fd = open(path, O_RDONLY);
if (fd < 0)
return -1;
char buffer[1024];
if (read(fd, buffer, sizeof(buffer)) < 0) {
return -1; /* ← リーク! fd が閉じられていない */
}
/* ... 処理 ... */
close(fd);
return 0;
}
/* リークパターン 2: 例外的状況 */
int complex_operation(const char *path1, const char *path2)
{
int fd1 = open(path1, O_RDONLY);
if (fd1 < 0)
return -1;
int fd2 = open(path2, O_RDONLY);
if (fd2 < 0) {
return -1; /* ← リーク! fd1 が閉じられていない */
}
/* ... 処理 ... */
close(fd2);
close(fd1);
return 0;
}
/* リークパターン 3: ループ内 */
void process_many_files(char **paths, int count)
{
for (int i = 0; i < count; i++) {
int fd = open(paths[i], O_RDONLY);
if (fd < 0)
continue; /* ← 一部は正しいが... */
char buffer[1024];
if (read(fd, buffer, sizeof(buffer)) < 0) {
continue; /* ← リーク! close()していない */
}
/* ... 処理 ... */
close(fd);
}
}
2.5.2 リーク検出の科学
静的解析(Static Analysis):
静的解析によるリソースリーク検出
═══════════════════════════════════════════════════════
手法:
1. 制御フロー解析(Control Flow Analysis)
- すべての実行パスを追跡
- リソース取得・解放ペアを検証
2. データフロー解析(Data Flow Analysis)
- リソースハンドルの伝播を追跡
- すべてのパスで解放されることを確認
ツール:
- Coverity
- PVS-Studio
- clang-analyzer
- Infer (Meta/Facebook)
例:clang static analyzer
┌────────────────────────────────────────────────────┐
│ $ clang --analyze -Xanalyzer -analyzer-checker=unix│
│ leak.c │
│ │
│ leak.c:10:5: warning: Opened file is never closed │
│ [unix.Stream] │
│ return -1; │
│ ^~~~~~~~~ │
└────────────────────────────────────────────────────┘
動的解析(Dynamic Analysis):
Valgrind によるファイルディスクリプタリーク検出
═══════════════════════════════════════════════════════
コマンド:
valgrind --track-fds=yes ./program
出力例:
==12345== FILE DESCRIPTORS: 3 open at exit.
==12345== Open file descriptor 4: /tmp/test.txt
==12345== at 0x4E94A3: open (in /lib64/libc.so.6)
==12345== by 0x401234: main (program.c:10)
==12345==
==12345== Open file descriptor 3: /tmp/data.bin
==12345== at 0x4E94A3: open (in /lib64/libc.so.6)
==12345== by 0x401189: process_files (program.c:25)
情報:
- どのファイルが開いたままか
- どこで open() されたか(スタックトレース)
2.5.3 リーク防止パターン
パターン 1:統一された出口点(Single Exit Point)
int process_file(const char *path)
{
int result = -1;
int fd = -1;
char *buffer = NULL;
fd = open(path, O_RDONLY);
if (fd < 0)
goto cleanup;
buffer = malloc(1024);
if (!buffer)
goto cleanup;
if (read(fd, buffer, 1024) < 0)
goto cleanup;
/* ... 処理 ... */
result = 0;
cleanup:
if (buffer)
free(buffer);
if (fd >= 0)
close(fd);
return result;
}
パターン 2:RAII風のクリーンアップ(GCC拡張)
/* cleanup属性を使った自動解放 */
static void cleanup_fd(int *fd)
{
if (*fd >= 0)
close(*fd);
}
#define AUTO_CLOSE __attribute__((cleanup(cleanup_fd)))
int process_file(const char *path)
{
AUTO_CLOSE int fd = open(path, O_RDONLY);
if (fd < 0)
return -1;
char buffer[1024];
if (read(fd, buffer, sizeof(buffer)) < 0)
return -1; /* fdは自動的に閉じられる */
/* ... 処理 ... */
return 0; /* fdは自動的に閉じられる */
}
パターン 3:ラッパー構造体
/* リソース管理構造体 */
typedef struct {
int fd;
char *buffer;
size_t buffer_size;
} FileContext;
/* 初期化 */
int context_init(FileContext *ctx, const char *path)
{
ctx->fd = -1;
ctx->buffer = NULL;
ctx->buffer_size = 0;
ctx->fd = open(path, O_RDONLY);
if (ctx->fd < 0)
return -1;
ctx->buffer_size = 1024;
ctx->buffer = malloc(ctx->buffer_size);
if (!ctx->buffer) {
close(ctx->fd);
ctx->fd = -1;
return -1;
}
return 0;
}
/* クリーンアップ */
void context_cleanup(FileContext *ctx)
{
if (ctx->buffer) {
free(ctx->buffer);
ctx->buffer = NULL;
}
if (ctx->fd >= 0) {
close(ctx->fd);
ctx->fd = -1;
}
}
2.6 エラーハンドリングの理論
2.6.1 errno の歴史と設計
errno の起源:
Unix V6(1975)における errno
═══════════════════════════════════════════════════════
背景:
- C言語は例外機構を持たない
- 関数は単一の値のみ返せる
- エラー情報をどう伝えるか?
解決策:
1. 関数は成功/失敗を示す値を返す
2. 詳細なエラーコードはグローバル変数 errno に格納
/* Ken Thompson / Dennis Ritchie の設計 */
int r = open("/path/to/file", O_RDONLY);
if (r < 0) {
/* エラー:詳細は errno を確認 */
/* errno = 2 → ENOENT: ファイルが存在しない */
}
errno の問題点と進化:
errno の問題点
═══════════════════════════════════════════════════════
問題1: グローバル変数
────────────────────
- 関数呼び出しで上書きされる可能性
- 保存しておく必要がある
fd = open("file", O_RDONLY);
if (fd < 0) {
int saved_errno = errno; /* 保存 */
printf("Error occurred\n"); /* printfがerrnoを変更する可能性 */
errno = saved_errno; /* 復元 */
}
問題2: スレッドセーフではない(1970年代)
────────────────────────────────────────
- マルチスレッドプログラムで競合状態
- スレッドAのエラーがスレッドBに影響
解決策(POSIX.1c, 1995):
────────────────────────
- errno をスレッドローカル変数に
- マクロで隠蔽
/* 現代の実装 */
#define errno (*__errno_location())
/* 各スレッドが独自の errno を持つ */
2.6.2 エラーコードの分類
POSIXエラーコードの分類
═══════════════════════════════════════════════════════
カテゴリ1: 致命的エラー(回復不可能)
─────────────────────────────────────
ENOMEM - メモリ不足
EIO - I/Oエラー(ハードウェア障害)
ENOSPC - ディスク容量不足
対処:
- エラーを報告してプログラム終了
- または操作を中断してユーザーに通知
カテゴリ2: 一時的エラー(リトライ可能)
───────────────────────────────────────
EINTR - シグナルによる中断
EAGAIN - リソースが一時的に利用不可
EBUSY - リソースがビジー
対処:
- 再試行ロジックを実装
- 適切な待機後にリトライ
カテゴリ3: 論理エラー(プログラミングミス)
─────────────────────────────────────────
EBADF - 無効なファイルディスクリプタ
EINVAL - 無効な引数
EFAULT - 無効なメモリアドレス
対処:
- アサーションやログで検出
- プログラムのバグとして修正
カテゴリ4: 権限/アクセスエラー
─────────────────────────────
EACCES - 権限不足
EPERM - 操作が許可されていない
EROFS - 読み取り専用ファイルシステム
対処:
- ユーザーに適切なメッセージ
- 代替操作の提案
2.6.3 堅牢なエラーハンドリングパターン
パターン 1:詳細なエラー処理
ssize_t safe_read(int fd, void *buf, size_t count)
{
ssize_t result;
while (1) {
result = read(fd, buf, count);
if (result >= 0)
return result;
/* エラー分類と対処 */
switch (errno) {
case EINTR:
/* シグナル中断 → リトライ */
continue;
case EAGAIN:
#if EAGAIN != EWOULDBLOCK
case EWOULDBLOCK:
#endif
/* 非ブロッキングI/O → リトライまたは待機 */
continue;
case EBADF:
case EINVAL:
/* プログラミングエラー */
/* デバッグビルドではアサート */
assert(0 && "Invalid file descriptor");
return -1;
case EIO:
/* ハードウェアエラー → 回復不可能 */
/* ログを記録して上位に伝播 */
return -1;
default:
/* その他のエラー */
return -1;
}
}
}
パターン 2:エラーコンテキストの伝播
/* エラー情報構造体 */
typedef struct {
int code; /* errno値 */
const char *function; /* エラー発生関数 */
const char *file; /* ソースファイル */
int line; /* 行番号 */
char message[256]; /* 追加メッセージ */
} ErrorContext;
/* スレッドローカルなエラーコンテキスト */
static __thread ErrorContext last_error;
/* エラー設定マクロ */
#define SET_ERROR(err_code, msg) do { \
last_error.code = (err_code); \
last_error.function = __func__; \
last_error.file = __FILE__; \
last_error.line = __LINE__; \
snprintf(last_error.message, sizeof(last_error.message), "%s", (msg)); \
} while (0)
/* 使用例 */
int open_file(const char *path)
{
int fd = open(path, O_RDONLY);
if (fd < 0) {
SET_ERROR(errno, path);
return -1;
}
return fd;
}
/* エラー報告 */
void print_last_error(void)
{
fprintf(stderr, "Error at %s:%d in %s: %s (%s)\n",
last_error.file,
last_error.line,
last_error.function,
strerror(last_error.code),
last_error.message);
}
2.7 ファイルオフセットとシークの理論
2.7.1 ランダムアクセスの理論
ストレージアクセスパターンの分類
═══════════════════════════════════════════════════════
シーケンシャルアクセス(Sequential Access)
───────────────────────────────────────────
特徴:
- データを先頭から順に読み書き
- テープドライブの自然な動作
ファイル: [ A | B | C | D | E | F | G ]
アクセス: 1 2 3 4 5 6 7
→ → → → → → →
ランダムアクセス(Random Access)
─────────────────────────────────
特徴:
- 任意の位置に直接アクセス
- ディスクドライブで実現可能
ファイル: [ A | B | C | D | E | F | G ]
アクセス: 3 1 5 2 4
↓ ↓ ↓ ↓ ↓
現代のストレージ特性:
─────────────────────
HDD: シーケンシャル >> ランダム(シーク時間が支配的)
SSD: シーケンシャル > ランダム(差は小さい)
RAM: シーケンシャル ≈ ランダム
2.7.2 lseek() システムコール
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
/* whence の値と意味 */
SEEK_SET /* ファイル先頭からの絶対位置 */
SEEK_CUR /* 現在位置からの相対位置 */
SEEK_END /* ファイル末尾からの相対位置 */
lseek の内部動作:
lseek(fd, 100, SEEK_SET) の動作
═══════════════════════════════════════════════════════
User Space Kernel Space
│ │
│ lseek(3, 100, SEEK_SET)
├───────────────────►│
│ │
│ ▼
│ ┌─────────────────┐
│ │ sys_lseek() │
│ │ │
│ │ 1. fd検証 │
│ │ 2. ファイル取得 │
│ │ 3. offset計算 │
│ │ 4. f_pos更新 │
│ └────────┬────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ struct file │
│ │ f_pos: 0 → 100 │
│ └─────────────────┘
│ │
│ 100 │
│◄──────────────────────┘
│
│ /* 注意:実際のI/Oは発生しない */
│ /* カーネル内のメタデータ更新のみ */
2.7.3 シーク不可能なファイルディスクリプタ
シーク可能性の分類
═══════════════════════════════════════════════════════
シーク可能(Seekable):
────────────────────
- 通常のファイル(ext4, NTFS等)
- ブロックデバイス(/dev/sda)
- メモリマップドファイル
特徴:
- ランダムアクセス可能
- ファイルサイズが既知
- lseek() が成功する
シーク不可能(Non-seekable):
───────────────────────────
- パイプ(pipe, FIFO)
- ソケット(TCP, UDP, Unix domain)
- 端末(/dev/tty)
- /proc, /sys の一部ファイル
特徴:
- シーケンシャルアクセスのみ
- サイズが不明または動的
- lseek() は ESPIPE エラー
get_next_line での意味:
───────────────────────
- lseek() を使わずに実装すべき
- パイプからの入力に対応可能
- 「一度読んだデータは戻せない」前提
シーク不可能fdの検出:
/* シーク可能性のテスト */
int is_seekable(int fd)
{
off_t current = lseek(fd, 0, SEEK_CUR);
if (current == (off_t)-1) {
if (errno == ESPIPE)
return 0; /* シーク不可能 */
return -1; /* エラー */
}
return 1; /* シーク可能 */
}
2.8 dup() / dup2() の理論
2.8.1 ファイルディスクリプタの複製
dup() の動作原理
═══════════════════════════════════════════════════════
dup(oldfd) の動作:
1. 最小の未使用fd番号を見つける
2. oldfdと同じファイルテーブルエントリを参照
3. 参照カウントをインクリメント
4. 新しいfd番号を返す
図解:
─────
Before: dup(3)
┌──────────────────┐ ┌────────────────────┐
│ fd_table │ │ struct file │
├──────────────────┤ ├────────────────────┤
│ [0] stdin │ │ f_count: 1 │
│ [1] stdout │ │ f_pos: 50 │
│ [2] stderr │ │ f_op: ext4_ops │
│ [3] ─────────────┼────►│ f_inode: ... │
│ [4] NULL │ └────────────────────┘
│ [5] NULL │
└──────────────────┘
After: int fd = dup(3); /* fd = 4 */
┌──────────────────┐ ┌────────────────────┐
│ fd_table │ │ struct file │
├──────────────────┤ ├────────────────────┤
│ [0] stdin │ │ f_count: 2 ←増加 │
│ [1] stdout │ │ f_pos: 50 │
│ [2] stderr │ │ f_op: ext4_ops │
│ [3] ─────────────┼──┬─►│ f_inode: ... │
│ [4] ─────────────┼──┘ └────────────────────┘
│ [5] NULL │
└──────────────────┘
重要:
- fd 3 と fd 4 は同じ f_pos を共有
- 一方で read() すると、両方の位置が進む
2.8.2 dup2() によるI/Oリダイレクション
dup2(oldfd, newfd) の動作原理
═══════════════════════════════════════════════════════
動作:
1. newfdが開いていれば、まずclose(newfd)
2. oldfdをnewfdに複製
3. newfdを返す
典型的な使用例:標準出力のリダイレクト
─────────────────────────────────────
/* 標準出力をファイルにリダイレクト */
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); /* fd の内容を fd 1 にコピー */
close(fd); /* 元の fd は不要 */
/* 以降、printf() は output.txt に書き込まれる */
printf("This goes to file\n");
図解:
─────
Step 1: open("output.txt", ...)
┌──────────────────┐ ┌────────────────────┐
│ [0] stdin │ │ file: terminal │
│ [1] ─────────────┼────►│ │
│ [2] stderr │ └────────────────────┘
│ [3] ─────────────┼────►┌────────────────────┐
└──────────────────┘ │ file: output.txt │
└────────────────────┘
Step 2: dup2(3, 1)
┌──────────────────┐ ┌────────────────────┐
│ [0] stdin │ │ file: terminal │
│ [1] ─────────────┼──┬─►│ (closed) │
│ [2] stderr │ │ └────────────────────┘
│ [3] ─────────────┼──┴─►┌────────────────────┐
└──────────────────┘ │ file: output.txt │
│ f_count: 2 │
└────────────────────┘
Step 3: close(3)
┌──────────────────┐ ┌────────────────────┐
│ [0] stdin │ │ file: output.txt │
│ [1] ─────────────┼────►│ f_count: 1 │
│ [2] stderr │ └────────────────────┘
│ [3] NULL │
└──────────────────┘
2.8.3 パイプラインの実装
/* Unix シェルのパイプライン: cmd1 | cmd2 */
void execute_pipeline(char *cmd1[], char *cmd2[])
{
int pipefd[2];
pipe(pipefd); /* pipefd[0]: 読み取り端, pipefd[1]: 書き込み端 */
pid_t pid1 = fork();
if (pid1 == 0) {
/* cmd1: 標準出力をパイプに接続 */
close(pipefd[0]); /* 読み取り端は不要 */
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execvp(cmd1[0], cmd1);
_exit(127);
}
pid_t pid2 = fork();
if (pid2 == 0) {
/* cmd2: 標準入力をパイプに接続 */
close(pipefd[1]); /* 書き込み端は不要 */
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
execvp(cmd2[0], cmd2);
_exit(127);
}
/* 親プロセス: パイプの両端を閉じる */
close(pipefd[0]);
close(pipefd[1]);
/* 子プロセスの終了を待つ */
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
}
2.9 get_next_line への応用
2.9.1 複数ファイルディスクリプタのサポート
get_next_line の仕様では、複数のfdを同時にサポートする必要があります。これはCS的観点から見ると、「マルチプレクシング」の問題です:
get_next_line のマルチプレクシング要件
═══════════════════════════════════════════════════════
要件:
- 複数のfdから並行して読み取り
- 各fdの読み取り状態を独立して管理
- fdが閉じられた時のクリーンアップ
設計選択:
方式1: 固定サイズ配列
────────────────────
static char *buffers[OPEN_MAX];
static int positions[OPEN_MAX];
利点:O(1)アクセス、シンプル
欠点:メモリ効率が悪い(ほとんど未使用)
方式2: ハッシュテーブル
─────────────────────
static HashTable *state_table;
利点:メモリ効率が良い
欠点:実装が複雑、42の規範で制限あり
方式3: 連結リスト
────────────────
typedef struct s_fd_state {
int fd;
char *buffer;
int pos;
struct s_fd_state *next;
} t_fd_state;
利点:メモリ効率が良い、動的
欠点:O(n)アクセス
42の課題での一般的な選択:
→ 方式1(固定サイズ配列)が最もシンプルで一般的
2.9.2 状態管理の実装
/* 複数fd対応のget_next_line 状態管理 */
/* 状態を保持する構造体 */
typedef struct s_buffer {
char data[BUFFER_SIZE + 1];
int len; /* データの有効長 */
int pos; /* 現在の読み取り位置 */
} t_buffer;
/* fd別の状態(静的変数) */
static t_buffer *g_states[OPEN_MAX];
/* 状態の取得または作成 */
static t_buffer *get_state(int fd)
{
if (fd < 0 || fd >= OPEN_MAX)
return NULL;
if (!g_states[fd]) {
g_states[fd] = malloc(sizeof(t_buffer));
if (!g_states[fd])
return NULL;
g_states[fd]->len = 0;
g_states[fd]->pos = 0;
}
return g_states[fd];
}
/* 状態のクリーンアップ */
static void clear_state(int fd)
{
if (fd >= 0 && fd < OPEN_MAX && g_states[fd]) {
free(g_states[fd]);
g_states[fd] = NULL;
}
}
2.9.3 エラーハンドリングの実装
/* 堅牢なget_next_line */
char *get_next_line(int fd)
{
t_buffer *state;
ssize_t bytes_read;
char *line;
/* パラメータ検証 */
if (fd < 0 || BUFFER_SIZE <= 0)
return NULL;
state = get_state(fd);
if (!state)
return NULL;
while (1) {
/* バッファ内の改行を探す */
line = extract_line(state);
if (line)
return line;
/* バッファが空または改行なし → 読み取り */
if (state->pos >= state->len) {
bytes_read = read(fd, state->data, BUFFER_SIZE);
if (bytes_read < 0) {
/* エラー:状態をクリーンアップ */
clear_state(fd);
return NULL;
}
if (bytes_read == 0) {
/* EOF:残りのデータを返す */
line = extract_remaining(state);
clear_state(fd);
return line;
}
state->len = bytes_read;
state->pos = 0;
}
}
}
2.9.4 シーク不可能fdへの対応
/* get_next_lineはシーク不可能なfdもサポートする必要がある */
/*
* 以下の入力ソースに対応:
* 1. 通常ファイル(シーク可能)
* 2. 標準入力(端末からの場合はシーク不可能)
* 3. パイプ(シーク不可能)
*
* したがって:
* - lseek()を使用しない
* - 一度読んだデータは戻せない前提
* - 「読み過ぎ」を避けるための戦略が必要
*/
/* 読み過ぎ対策:静的バッファに保存 */
/*
* 例: "Hello\nWorld" を読む場合
*
* read(fd, buf, BUFFER_SIZE=10) → "Hello\nWorl"
* ^^^^^^^^ 返す
* ^^^^ バッファに残す
*
* 次の呼び出しで "World" を使用可能
*/
まとめ
この章では、ファイルディスクリプタの背後にある計算機科学の理論を学びました:
次章では、バッファリングの理論とget_next_lineのアルゴリズム設計について学びます。