第3章: ファイルディスクリプタとI/O抽象化 - UNIX入出力モデルの計算機科学的基盤
3.1 入出力抽象化の歴史と理論
3.1.1 初期コンピュータのI/O問題
1950年代のコンピュータでは、入出力(Input/Output)は極めて低レベルな操作でした。プログラマは、デバイスごとに異なる制御コードやタイミングを直接扱う必要がありました:
【1950年代のI/O操作(概念)】
カードリーダーからの入力:
1. カードリーダーの準備完了を待つ
2. 読み込みコマンドを発行
3. データ転送完了を待つ
4. エラーチェック
5. 次のカードに進む
磁気テープへの出力:
1. テープ装置の状態確認
2. 書き込み位置のシーク
3. データブロックの書き込み
4. 同期待ち
5. エラー処理
各デバイスは固有のプロトコルを持ち、プログラマはデバイスドライバを自分で書く必要がありました。これは生産性と移植性の観点から深刻な問題でした。
3.1.2 I/O抽象化レイヤーの発明
1960年代、オペレーティングシステムの発展とともに、I/O抽象化の概念が登場しました。Multicsプロジェクト(1964-1969, MIT/GE/Bell Labs)は、ファイルシステムとI/Oの統一的な抽象化を先駆的に実装しました。
【I/O抽象化の階層】
アプリケーション
↓
┌──────────────────────────────┐
│ 統一されたI/Oインターフェース │
│ open(), read(), write(), close()
└──────────────────────────────┘
↓
┌──────────────────────────────┐
│ ファイルシステム │
│ ファイル、ディレクトリ管理 │
└──────────────────────────────┘
↓
┌──────────────────────────────┐
│ デバイスドライバ │
│ ハードウェア固有の処理 │
└──────────────────────────────┘
↓
物理デバイス
この抽象化により、アプリケーションはデバイスの詳細を知らずにデータを読み書きできるようになりました。
3.1.3 「全てはファイルである」哲学
UNIXは、I/O抽象化をさらに推し進め、「全てはファイルである(Everything is a File)」という革命的な設計哲学を採用しました。Ken ThompsonとDennis Ritchieは、以下の全てを同一のインターフェースで扱えるようにしました:
【UNIXにおける「ファイル」】
通常ファイル: テキスト、バイナリ等のディスク上のデータ
ディレクトリ: ファイル名とinode番号のマッピング
デバイス:
- キャラクタデバイス: /dev/tty, /dev/null, /dev/random
- ブロックデバイス: /dev/sda, /dev/sr0
パイプ: プロセス間通信チャネル
ソケット: ネットワーク通信エンドポイント(BSD追加, 1983)
シンボリックリンク: 他のファイルへの参照
全て同じシステムコールで操作可能:
open() → ファイルディスクリプタを取得
read() → データを読み込み
write() → データを書き込み
close() → リソースを解放
Dennis Ritchieは1974年の論文「The UNIX Time-Sharing System」で、この設計について述べています:
> "The file system is central to UNIX. Almost everything in the system is either a file or is accessed like a file."
3.1.4 抽象化の理論的意義
この設計は、ソフトウェア工学の抽象化(Abstraction)とカプセル化(Encapsulation)の原則を完璧に体現しています:
【抽象化の効果】
1. 関心の分離(Separation of Concerns):
- アプリケーション: データの処理に集中
- OS: デバイスとの通信を担当
- ドライバ: ハードウェア固有の詳細を隠蔽
2. インターフェースの統一:
- read()/write()だけでほぼ全てのI/Oを処理
- 新しいデバイスも同じインターフェースで扱える
3. 組み合わせ可能性(Composability):
- パイプによるプログラムの連結
- リダイレクションによる入出力の切り替え
- プログラムは入出力先を知らなくて良い
3.2 ファイルディスクリプタの内部構造
3.2.1 カーネルデータ構造の三層モデル
UNIXカーネルは、ファイルアクセスを管理するために三層のデータ構造を使用します。この設計は、Maurice Bachの名著「The Design of the UNIX Operating System」(1986年)で詳細に解説されています。
【ファイル管理の三層構造】
Layer 1: プロセスごとのファイルディスクリプタテーブル
┌─────────────────────────────────────────────────────┐
│ Process A │ Process B │
│ ┌────┬─────────┐ │ ┌────┬─────────┐ │
│ │ FD │ flags │ │ │ FD │ flags │ │
│ ├────┼─────────┤ │ ├────┼─────────┤ │
│ │ 0 │ ○───────├──────────┐│ │ 0 │ ○───────├──┤
│ │ 1 │ ○───────├────────┐ ││ │ 1 │ ○───────├──┤
│ │ 2 │ ○───────├──────┐ │ ││ │ 2 │ ○ │ │
│ │ 3 │ ○───────├────┐ │ │ ││ └────┴─────────┘ │
│ └────┴─────────┘ │ │ │ ││ │
└───────────────────────┼─┼─┼─┼┴──────────────────────┘
│ │ │ │
↓ ↓ ↓ ↓
Layer 2: システム全体のオープンファイルテーブル
┌───────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────┐ │
│ │ Entry 0: offset=0, flags=O_RDONLY, refcount=2 │──┤
│ ├─────────────────────────────────────────────────┤ │
│ │ Entry 1: offset=100, flags=O_WRONLY, refcount=1 │──┤
│ ├─────────────────────────────────────────────────┤ │
│ │ Entry 2: offset=0, flags=O_RDWR, refcount=1 │──┤
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
│
↓
Layer 3: システム全体のinodeテーブル
┌───────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────┐ │
│ │ inode 12345: type=regular, size=4096, owner=... │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ inode 67890: type=device, major=1, minor=3, ... │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
3.2.2 各層の役割
Layer 1: ファイルディスクリプタテーブル(プロセスごと)
/* カーネル内部構造(概念的)*/
struct file_descriptor_entry {
struct file *file_ptr; /* オープンファイルテーブルへのポインタ */
int flags; /* close-on-exec フラグ等 */
};
struct process {
struct file_descriptor_entry fd_table[OPEN_MAX];
/* ... 他のプロセス情報 ... */
};
/*
* ファイルディスクリプタは、このテーブルへのインデックス
* FD 3 → fd_table[3] → オープンファイルテーブルのエントリ
*/
Layer 2: オープンファイルテーブル(システム全体)
/* カーネル内部構造(概念的)*/
struct file {
off_t offset; /* 現在のファイル位置 */
int flags; /* O_RDONLY, O_WRONLY, O_APPEND等 */
int refcount; /* 参照カウント */
struct inode *inode; /* inodeへのポインタ */
struct file_ops *ops; /* ファイル操作関数へのポインタ */
};
/*
* 重要: fork()後、親子プロセスは同じfile構造体を共有する
* → offset の変更は両方に影響する
*/
Layer 3: inodeテーブル(システム全体)
/* カーネル内部構造(概念的)*/
struct inode {
mode_t mode; /* ファイルタイプと権限 */
uid_t uid; /* 所有者UID */
gid_t gid; /* グループGID */
off_t size; /* ファイルサイズ */
time_t atime, mtime; /* アクセス/修正時刻 */
int nlink; /* ハードリンク数 */
/* デバイスの場合: major, minor番号 */
/* ブロックの場合: ブロックアドレス */
};
/*
* inodeはファイルの「実体」を表す
* 同じファイルを複数回openしても、同じinodeを指す
*/
3.2.3 なぜこの構造が必要か
三層構造は、以下の要件を満たすために設計されました:
【設計要件と解決策】
要件1: 同じファイルを異なるモードで開く
→ 各open()で新しいオープンファイルテーブルエントリ
→ 異なるoffsetとflagsを持てる
要件2: fork()でファイル状態を共有
→ 子はFDテーブルをコピーするが、同じfile構造体を指す
→ offsetが共有される
要件3: dup()/dup2()でFDを複製
→ 異なるFDが同じfile構造体を指す
→ refcountで管理
要件4: 同じファイルを複数プロセスがアクセス
→ 同じinodeを共有
→ ロック機構で競合を防止
3.2.4 参照カウントと解放
/*
* close()の動作(概念的)
*/
int close(int fd)
{
struct file *f = current_process->fd_table[fd].file_ptr;
/* FDテーブルエントリを解放 */
current_process->fd_table[fd].file_ptr = NULL;
/* オープンファイルテーブルの参照カウントを減少 */
f->refcount--;
if (f->refcount == 0) {
/* 誰も参照していない → 解放 */
release_file_entry(f);
}
return 0;
}
/*
* fork()の動作(概念的)
*/
pid_t fork(void)
{
struct process *child = create_child_process();
/* FDテーブルをコピー */
for (int fd = 0; fd < OPEN_MAX; fd++) {
child->fd_table[fd] = parent->fd_table[fd];
if (child->fd_table[fd].file_ptr) {
/* 同じfile構造体を共有 → refcount増加 */
child->fd_table[fd].file_ptr->refcount++;
}
}
/* ... 他の複製処理 ... */
}
3.3 標準ストリームの設計
3.3.1 標準ストリームの歴史
標準入力(stdin)、標準出力(stdout)、標準エラー(stderr)の概念は、UNIXの初期バージョンから存在しますが、その設計は段階的に洗練されました。
【標準ストリームの発展】
UNIX Version 1 (1971):
- 初期のI/Oリダイレクションをサポート
- < と > によるファイルリダイレクション
UNIX Version 6 (1975):
- 標準入力/出力/エラーの概念が明確化
- パイプ機能の成熟
C言語標準化 (ANSI C, 1989):
- stdin, stdout, stderr がFILE*として標準化
- POSIX.1 でSTDIN_FILENO等が定義
3.3.2 FD 0, 1, 2 の特別な意味
/* <unistd.h> での定義 */
#define STDIN_FILENO 0 /* 標準入力 */
#define STDOUT_FILENO 1 /* 標準出力 */
#define STDERR_FILENO 2 /* 標準エラー */
/*
* これらの値(0, 1, 2)は単なる慣例ではなく、
* シェルとOSの深いレベルで前提とされている
*/
【なぜ0, 1, 2なのか】
シェルがプログラムを起動する際:
1. fork()で子プロセスを作成
2. 子プロセスで端末を開く:
fd = open("/dev/tty", O_RDWR);
→ 最小の未使用FD = 0 が割り当てられる
3. dup()で複製:
dup(0); → FD 1 が作成される
dup(0); → FD 2 が作成される
4. exec()で新しいプログラムを実行
→ 新プログラムはFD 0, 1, 2 が端末に接続された状態で開始
この順序により、歴史的に 0=入力, 1=出力, 2=エラー となった
3.3.3 標準ストリームの分離理由
stdout と stderr を分離する設計は、関心の分離の原則に基づいています:
# stdoutとstderrの分離の実用例
# 正常出力のみをファイルに保存
./program > output.txt
# エラーは画面に表示される
# エラーのみをファイルに保存
./program 2> errors.txt
# 正常出力は画面に表示される
# 両方を別々のファイルに
./program > output.txt 2> errors.txt
# 両方を同じファイルに
./program > all.txt 2>&1
# パイプラインでの動作
./program1 | ./program2
# program1のstdoutのみがprogram2に流れる
# stderrは端末に直接表示される
3.4 dup()とdup2()の理論
3.4.1 ファイルディスクリプタの複製
dup() と dup2() は、ファイルディスクリプタを複製するシステムコールです。複製されたFDは、同じオープンファイルテーブルエントリを指します。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
/*
* dup(oldfd):
* - oldfdを複製し、最小の未使用FD番号を返す
* - エラー時は-1を返す
*
* dup2(oldfd, newfd):
* - oldfdをnewfdに複製
* - newfdが開いていれば、先にクローズ
* - 成功時はnewfdを返す
* - エラー時は-1を返す
*/
3.4.2 dup()の内部動作
【dup(3) の動作】
Before dup(3):
┌────────────────────┐
│ Process FD Table │
├────┬───────────────┤
│ 0 │ → stdin │
│ 1 │ → stdout │
│ 2 │ → stderr │
│ 3 │ → file.txt ●──├──┐
│ 4 │ (empty) │ │
└────┴───────────────┘ │
↓
┌──────────────┐
│ Open File │
│ Table Entry │
│ refcount: 1 │
└──────────────┘
After dup(3): // Returns 4 (最小の未使用番号)
┌────────────────────┐
│ Process FD Table │
├────┬───────────────┤
│ 0 │ → stdin │
│ 1 │ → stdout │
│ 2 │ → stderr │
│ 3 │ → file.txt ●──├──┐
│ 4 │ → file.txt ●──├──┼──┐ (新しいFDが同じエントリを指す)
└────┴───────────────┘ │ │
↓ ↓
┌──────────────┐
│ Open File │
│ Table Entry │
│ refcount: 2 │ ← 参照カウント増加
└──────────────┘
3.4.3 dup2()の内部動作
【dup2(3, 1) の動作 - stdoutをファイルにリダイレクト】
Before dup2(3, 1):
┌────────────────────┐
│ Process FD Table │
├────┬───────────────┤
│ 0 │ → stdin │
│ 1 │ → terminal ●──├──→ Terminal Entry (refcount: 1)
│ 2 │ → stderr │
│ 3 │ → file.txt ●──├──→ File Entry (refcount: 1)
└────┴───────────────┘
Step 1: dup2は内部でまずclose(1)を実行
┌────────────────────┐
│ Process FD Table │
├────┬───────────────┤
│ 0 │ → stdin │
│ 1 │ (closed) │ Terminal Entry が解放される可能性
│ 2 │ → stderr │ (他に参照がなければ)
│ 3 │ → file.txt ●──├──→ File Entry (refcount: 1)
└────┴───────────────┘
Step 2: FD 1 を FD 3 と同じエントリを指すよう設定
┌────────────────────┐
│ Process FD Table │
├────┬───────────────┤
│ 0 │ → stdin │
│ 1 │ → file.txt ●──├──┐
│ 2 │ → stderr │ ├──→ File Entry (refcount: 2)
│ 3 │ → file.txt ●──├──┘
└────┴───────────────┘
Result: printf() や write(1, ...) は file.txt に書かれる
3.4.4 dup2()のアトミック性
dup2()は、close()とdup()をアトミック(原子的)に実行します。これは並行プログラミングにおいて重要です:
/* アトミックでない実装(危険) */
close(newfd);
dup(oldfd); /* ← この間に割り込みやシグナルハンドラが
別のFDを開く可能性がある */
/* dup2()はアトミック */
dup2(oldfd, newfd); /* 割り込まれない */
3.5 I/Oリダイレクションの実装
3.5.1 シェルによるリダイレクション
シェルがリダイレクションを実装する方法を理解することで、Pipexの実装が明確になります:
# シェルコマンド
./program < input.txt > output.txt
/* シェルの内部動作(概念的) */
pid_t pid = fork();
if (pid == 0) {
/* 子プロセス */
/* 1. 入力リダイレクション: stdin < input.txt */
int in_fd = open("input.txt", O_RDONLY);
if (in_fd == -1) {
perror("input.txt");
exit(1);
}
dup2(in_fd, STDIN_FILENO);
close(in_fd); /* 元のFDは不要 */
/* 2. 出力リダイレクション: stdout > output.txt */
int out_fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (out_fd == -1) {
perror("output.txt");
exit(1);
}
dup2(out_fd, STDOUT_FILENO);
close(out_fd); /* 元のFDは不要 */
/* 3. プログラム実行 */
execve("./program", argv, envp);
perror("execve");
exit(126);
}
/* 親プロセス */
waitpid(pid, &status, 0);
3.5.2 リダイレクションの順序
dup2()を呼ぶ順序は、close()との関係で重要です:
/* 正しい順序 */
int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
dup2(fd, STDOUT_FILENO); /* まず複製 */
close(fd); /* それから元を閉じる */
/* 間違った順序 */
int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
close(fd); /* 先に閉じると... */
dup2(fd, STDOUT_FILENO); /* 無効なFDを使うことに! */
3.5.3 複数のリダイレクション
複数のリダイレクションを設定する場合、順序と依存関係に注意が必要です:
/* 例: stdin, stdout, stderr を全て設定 */
void setup_redirections(const char *in, const char *out, const char *err)
{
int in_fd = -1, out_fd = -1, err_fd = -1;
/* 全てのファイルを先に開く */
if (in) {
in_fd = open(in, O_RDONLY);
if (in_fd == -1) { perror(in); exit(1); }
}
if (out) {
out_fd = open(out, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (out_fd == -1) { perror(out); exit(1); }
}
if (err) {
err_fd = open(err, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (err_fd == -1) { perror(err); exit(1); }
}
/* リダイレクションを設定 */
if (in_fd != -1) {
dup2(in_fd, STDIN_FILENO);
close(in_fd);
}
if (out_fd != -1) {
dup2(out_fd, STDOUT_FILENO);
close(out_fd);
}
if (err_fd != -1) {
dup2(err_fd, STDERR_FILENO);
close(err_fd);
}
}
3.6 Pipexにおけるファイルディスクリプタ管理
3.6.1 Pipexで使用するFD
Pipexプログラムでは、以下のFDを管理する必要があります:
【Pipexで使用するファイルディスクリプタ】
親プロセス(pipex開始時):
┌────┬───────────────────────────────┐
│ FD │ 説明 │
├────┼───────────────────────────────┤
│ 0 │ stdin (端末から継承) │
│ 1 │ stdout (端末から継承) │
│ 2 │ stderr (端末から継承) │
│ 3 │ infile (argv[1]を開く) │
│ 4 │ outfile (argv[4]を開く) │
│ 5 │ pipe[0] (パイプ読み込み端) │
│ 6 │ pipe[1] (パイプ書き込み端) │
└────┴───────────────────────────────┘
子プロセス1(cmd1実行後):
┌────┬───────────────────────────────┐
│ FD │ 説明 │
├────┼───────────────────────────────┤
│ 0 │ infile (入力ファイル) │
│ 1 │ pipe[1] (パイプへ書き込み) │
│ 2 │ stderr (端末、エラー表示用) │
└────┴───────────────────────────────┘
子プロセス2(cmd2実行後):
┌────┬───────────────────────────────┐
│ FD │ 説明 │
├────┼───────────────────────────────┤
│ 0 │ pipe[0] (パイプから読み込み) │
│ 1 │ outfile (出力ファイル) │
│ 2 │ stderr (端末、エラー表示用) │
└────┴───────────────────────────────┘
3.6.2 子プロセス1の実装
void child_process_1(int infile, int *pipefd, int outfile, char *cmd, char **envp)
{
/*
* 子プロセス1の役割:
* - 入力: infile (argv[1]のファイル)
* - 出力: パイプの書き込み端 (pipefd[1])
* - 実行: cmd1 (argv[2])
*/
/* Step 1: stdinをinfileに接続 */
if (dup2(infile, STDIN_FILENO) == -1)
{
perror("dup2 stdin");
exit(EXIT_FAILURE);
}
/* Step 2: stdoutをパイプの書き込み端に接続 */
if (dup2(pipefd[1], STDOUT_FILENO) == -1)
{
perror("dup2 stdout");
exit(EXIT_FAILURE);
}
/* Step 3: 不要なFDを全て閉じる */
close(infile); /* dup2でコピーしたので不要 */
close(outfile); /* この子プロセスでは使わない */
close(pipefd[0]); /* 読み込み端は使わない */
close(pipefd[1]); /* dup2でコピーしたので不要 */
/* Step 4: コマンドを実行 */
execute_command(cmd, envp);
/* ここに到達したらexecveが失敗 */
exit(EXIT_FAILURE);
}
3.6.3 子プロセス2の実装
void child_process_2(int infile, int *pipefd, int outfile, char *cmd, char **envp)
{
/*
* 子プロセス2の役割:
* - 入力: パイプの読み込み端 (pipefd[0])
* - 出力: outfile (argv[4]のファイル)
* - 実行: cmd2 (argv[3])
*/
/* Step 1: stdinをパイプの読み込み端に接続 */
if (dup2(pipefd[0], STDIN_FILENO) == -1)
{
perror("dup2 stdin");
exit(EXIT_FAILURE);
}
/* Step 2: stdoutをoutfileに接続 */
if (dup2(outfile, STDOUT_FILENO) == -1)
{
perror("dup2 stdout");
exit(EXIT_FAILURE);
}
/* Step 3: 不要なFDを全て閉じる */
close(infile); /* この子プロセスでは使わない */
close(outfile); /* dup2でコピーしたので不要 */
close(pipefd[0]); /* dup2でコピーしたので不要 */
close(pipefd[1]); /* 書き込み端は使わない */
/* Step 4: コマンドを実行 */
execute_command(cmd, envp);
exit(EXIT_FAILURE);
}
3.6.4 親プロセスのFD管理
void pipex_main(int infile, int outfile, char **argv, char **envp)
{
int pipefd[2];
pid_t pid1, pid2;
int status;
/* パイプを作成 */
if (pipe(pipefd) == -1)
{
perror("pipe");
exit(EXIT_FAILURE);
}
/* 子プロセス1を生成 */
pid1 = fork();
if (pid1 == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (pid1 == 0)
{
child_process_1(infile, pipefd, outfile, argv[2], envp);
}
/* 子プロセス2を生成 */
pid2 = fork();
if (pid2 == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (pid2 == 0)
{
child_process_2(infile, pipefd, outfile, argv[3], envp);
}
/*
* 親プロセス: 全てのFDを閉じる
*
* 重要: パイプの両端を閉じないと、
* 子プロセスがEOFを検出できない
*/
close(infile);
close(outfile);
close(pipefd[0]);
close(pipefd[1]);
/* 両方の子プロセスを待つ */
waitpid(pid1, &status, 0);
waitpid(pid2, &status, 0);
}
3.7 ファイルディスクリプタのデバッグ
3.7.1 FD状態の確認
/* デバッグ用: 現在のFD状態を表示 */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
void debug_print_fds(const char *label)
{
fprintf(stderr, "[%s] PID=%d FD状態:\n", label, getpid());
for (int fd = 0; fd < 10; fd++)
{
struct stat st;
if (fstat(fd, &st) == 0)
{
char type;
if (S_ISREG(st.st_mode)) type = 'R'; /* Regular file */
else if (S_ISDIR(st.st_mode)) type = 'D'; /* Directory */
else if (S_ISCHR(st.st_mode)) type = 'C'; /* Character device */
else if (S_ISBLK(st.st_mode)) type = 'B'; /* Block device */
else if (S_ISFIFO(st.st_mode))type = 'P'; /* Pipe/FIFO */
else if (S_ISSOCK(st.st_mode))type = 'S'; /* Socket */
else type = '?';
fprintf(stderr, " FD %d: type=%c, inode=%lu\n",
fd, type, (unsigned long)st.st_ino);
}
}
fprintf(stderr, "\n");
}
3.7.2 システムコマンドによる確認
# 実行中のプロセスのFDを確認(Linux)
ls -l /proc/[PID]/fd
# 出力例:
# lrwx------ 1 user user 64 ... 0 -> /dev/pts/0
# lrwx------ 1 user user 64 ... 1 -> /dev/pts/0
# lrwx------ 1 user user 64 ... 2 -> /dev/pts/0
# lr-x------ 1 user user 64 ... 3 -> /home/user/input.txt
# l-wx------ 1 user user 64 ... 4 -> pipe:[12345]
# lsofコマンドでより詳細に
lsof -p [PID]
# straceでシステムコールをトレース
strace -e trace=open,close,dup2,pipe ./pipex ...
3.7.3 よくある問題と対策
【問題1: プログラムがハングする】
症状: パイプラインが永久にブロックする
原因: パイプの書き込み端が全てクローズされていない
→ 読み込み側がEOFを受け取れない
解決: 親プロセスで必ずパイプの両端をクローズ
子プロセスでも不要な端をクローズ
【問題2: "Bad file descriptor" エラー】
症状: write()やread()が-1を返し、errnoがEBADF
原因: 既にクローズしたFDを使おうとしている
またはdup2の順序が間違っている
解決: close()とdup2()の順序を確認
dup2()の前にクローズしない
【問題3: FDリーク】
症状: 長時間実行で"Too many open files"エラー
原因: open()したFDをclose()していない
解決: 全てのopen()に対応するclose()を確認
子プロセスでexec前に不要なFDをクローズ
3.8 学習リソース
推奨書籍
- "The Design of the UNIX Operating System"
- "Advanced Programming in the UNIX Environment"
- "The Linux Programming Interface"
歴史的資料
- Thompson, K., & Ritchie, D. (1974). "The UNIX Time-Sharing System"
- Kernighan, B., & Pike, R. (1984). "The UNIX Programming Environment"
3.9 次章への準備
次章では、pipe()システムコールを詳しく学び、プロセス間通信の完全な実装を行います。以下の準備課題を試してください:
/* 準備課題: パイプの基本動作確認 */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int pipefd[2];
char buf[100];
/* パイプ作成 */
if (pipe(pipefd) == -1)
{
perror("pipe");
return 1;
}
printf("読み込み端: FD %d\n", pipefd[0]);
printf("書き込み端: FD %d\n", pipefd[1]);
/* 書き込み */
const char *msg = "Hello through pipe!";
write(pipefd[1], msg, strlen(msg));
close(pipefd[1]);
/* 読み込み */
int n = read(pipefd[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("読み込んだ内容: %s\n", buf);
close(pipefd[0]);
return 0;
}
---
まとめ: この章では、UNIXのI/O抽象化の歴史と理論、ファイルディスクリプタの内部構造、そしてdup2()によるリダイレクションの仕組みを学びました。この知識は、Pipexでパイプラインを正しく実装するための基盤となります。次章では、pipe()システムコールとプロセス間通信の詳細を学びます。