📁

File Descriptorsとは

に公開

みなさんこんにちは、ウタです。

前回の記事ではカーネルとは何かについて解説しました。

https://zenn.dev/uta_san1012/articles/38a1581158b65d

今回はfile descriptors(以後「fd」とします)についてです。
初めて出てきた時からなんとなくわかっているつもりだったのにいざ詳しく触れてみると全然理解していませんでした、、、
自戒も込めて記事にまとめたいと思います。

まずはサンプルコード

まず、下記のサンプルコードを見てみましょう。

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd;
    char buffer[50];
    
    // ファイルを開く
    fd = open("test.txt", O_RDONLY);
    printf("fd: %d\n", fd);  // 3が出力される
    
    // ファイルを読む
    read(fd, buffer, 10);
    printf("読んだ内容: %s\n", buffer);
    
    // ファイルを閉じる
    close(fd);
    
    return 0;
}

test.txtというファイルを読み込んでターミナルに中身を出力するコードになっています。
コード内ではopenの返り値をfdに代入しており、その値がreadで使用されています。

僕はこのコードを見てfdっていうのはtest.txtの中身を所持している変数だとずっと思っていました。なので僕の頭の中では:

  1. openでファイルを読み取り、fdにその内容を代入
  2. readfdの中身をc言語のコードで扱えるようbufferに移動
  3. printfで出力

のように理解していました。
ただこれでは完全に間違った理解ですので、この記事でfdが何たるかを説明して行きます。

fdってなんなんだいっ💪

fdは一言表現するとファイルの番号札です・

レストランで例えると:

  1. お客さん(プログラム)が注文すると番号札をもらう
  2. 番号札(fd)で料理(ファイル)を識別
  3. 「3番のお客さん、お料理できました!」= 「fd=3のファイルに書き込み完了!」
fd = open("file.txt", O_RDONLY);   // ファイルを開いて番号札(fd)をもらう
read(fd, buffer, size);             // 番号札を使ってファイルを読む
close(fd);                          // 番号札を返却

つまりfdというのはファイルの内容を取得するために提示する番号札のことです。
プログラムはfdの整数を受け取って、そこに紐づいたファイルの中身をreadなどの関数に渡しているのです。

じゃあ、ファイルの中身はどこに保存されている?

と疑問に思う方もいると思います。
こちらはカーネルの中で管理されています。
openが実行された時点で、カーネル内部に「fd番号とファイル情報の対応表」が作られます。read実行時には、この番号を使ってカーネルが実際のファイル内容を取得し、プログラムに渡しています。
つくづくカーネルさんには見えないところで頑張ってもらっているなぁと実感しますね🤩

fdの特徴

なんとなくfdについてわかってきたと思うのでプログラム的な部分から触れていこうと思います。

まず、fd0, 1, 2, 3, 4...という整数の番号です。
プロセスごとに独立した番号体系であり、再利用されるという特徴があります。
再利用というのは、close()すると番号が空いて、次のopen()で再利用されます。

標準的なfd

全てのプログラムは起動時に3つの番号札を自動的に受け取ります:

fd番号 名前 役割
0 stdin 標準入力 キーボードからの入力
1 stdout 標準出力 画面への通常出力
2 stderr 標準エラー出力 エラーメッセージ
// これらは同じ意味
write(1, "Hello\n", 6);        // fd=1に書き込み
printf("Hello\n");             // 内部的にfd=1を使用

read(0, buffer, size);         // fd=0から読み込み
scanf("%s", buffer);           // 内部的にfd=0を使用

3つの番号札の落とし穴

前章で全てのプログラムは起動時に3つの番号札を自動的に受け取ると説明したのですが、それぞれ3つの番号札が必ずしも決まった役割をするわけではないということは理解していないといけません。

fd0,1,2は「一般的に」それぞれ役割が割り当てられており、プログラムを書くときはそれに沿って書くことで可読性が上がります。ただ、それはあくまで一般的な話でありまして、自分でfdの値を指定するようなイレギュラーな処理の際にも番号札0,1,2は使用できるのです。

例えば、標準出力をファイルにリダイレクトする例だと下記のようになります。

int main() {
    // 標準出力(fd=1)を閉じる
    close(1);
    
    // fd=1でファイルを開く(本来は画面出力の番号)
    int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
    printf("このメッセージはファイルに書かれる!\n");  // 画面に出ない!
    
    // writeも同様にファイルに書かれる
    write(1, "Hello from fd=1\n", 16);
    
    close(1);
    return 0;
}

通常であればfd=1が標準出力として使用されるのですが、先にcloseすることでfd=1に空きが生まれ、次のopenファイル出力として使用されています。
これでfdが「番号札」のように機能し、番号が再利用されることがわかりますね。

複数ファイルを使った例でも、この再利用の仕組みを確認してみましょう。

int main() {
    printf("最初の状態: 0,1,2は標準入出力で使用中\n");
    
    // 新しいファイルを開く
    int fd1 = open("file1.txt", O_RDONLY);
    int fd2 = open("file2.txt", O_RDONLY);
    int fd3 = open("file3.txt", O_RDONLY);
    
    printf("file1のfd: %d\n", fd1);  // fd=3
    printf("file2のfd: %d\n", fd2);  // fd=4  
    printf("file3のfd: %d\n", fd3);  // fd=5
    
    // fd=2(stderr)を閉じる
    close(2);
    
    // 新しいファイルを開く
    int fd4 = open("file4.txt", O_RDONLY);
    printf("file4のfd: %d\n", fd4);  // 2になる!(空いた番号を再利用)
    
    return 0;
}

こちらでも最初のfile1~3.txtopenして、fdの3から5までが埋まっていることがわかります。その後close(2)をすることでfd=2が空き、4つ目のファイル(file4.txt)が空いたfd=2を再利用していることがわかります。

まとめ

この記事では、ファイルディスクリプタ(fd)について以下のポイントを説明しました:

  • fdは「ファイルの番号札」 - ファイルの内容そのものではなく、識別するための整数
  • カーネルが実際のファイル管理を担当 - fdはカーネル内の対応表で管理されている
  • 0,1,2は標準入出力 - でも絶対的な役割ではなく、再利用可能な番号
  • 番号は再利用される - close()で空いた番号は次のopen()で使われる
  • プログラムごとに独立 - 各プログラムが独自のfd番号体系を持つ

最初は「ただの数字」だと思っていたfdですが、調べてみるとカーネルとの橋渡しをする重要な仕組みだったんですね。この理解があると、今後のシステムプログラミングがぐっと理解しやすくなるはずです。

Discussion