第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"
- Maurice J. Bach (1986) - 第5章: ファイルシステム内部

  • "Advanced Programming in the UNIX Environment"
- W. Richard Stevens, Stephen A. Rago - 第3章: ファイルI/O

  • "The Linux Programming Interface"
- Michael Kerrisk - 第4-5章: ファイルI/O

歴史的資料

  • 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()システムコールとプロセス間通信の詳細を学びます。