HTTPサーバが複数のクライアントを識別できる背景にはファイルディスクリプタの存在があります。ファイルディスクリプタが存在しないと, サーバは誰にレスポンスを返して良いかわからなくなってしまいます。
今回はファイルディスクリプタの基礎について。
Stream
ファイルディスクリプタを理解するには Stream[1] が何かを知る必要があります。
下図のようにプロセスからファイルにアクセスするときに, 通り道となるのが Stream です。バイト列が流れることから byte-stream とも呼ばれます。
プロセスからプロセスに繋ぐときには pipe[2]を使いますが、この時にバイト列を結びつけるのも Stream です。
Stream を制御するのはカーネルの役割で, open(2), read(2), write(2) などのシステームコールを通じて Stream を出してもらいます。
procfs
procfs (/proc) は擬似ファイルシステムでユーザ空間にプロセスの情報を見せるためにカーネルが RAM に展開したファイルです。プロセスの内部的な情報は /proc/$(PID) 以下に格納されます。これ以外にも例えば /proc/cpuinfo にCPUに関する情報, /proc/meminfo にメモリに関する情報があります。
他にも擬似ファイルシステムにはデバイスの情報を見せるための sysfs (/sys) があります。
ファイルディスクリプタ
ファイルディスクリプタ (fd) は int型 の整数です。前述の procfs の /proc/$(PID)/fd を覗くと数字の列があり, これがプロセスが使用している fd の一覧です。例えば, HTTPサーバでは同時接続数が増えると掴んでいる fd の数が増えるのが確認できます。
前述したように Stream はカーネルが管理しており, ユーザ空間から直接はアクセスできない領域になります。
そこで fd を通じて Stream と結びつくことによってプロセスから制御することができます。身近な fd は標準入力(stdin, 0), 標準出力(stdout, 1), 標準エラー(stderr, 2)です。
C言語の stdioライブラリ で Stream を制御するにはFILE型を用います。
FILE *fp;
FILE構造体は色々と型がありますが fd とバッファを持っています。
つまり, FILEは fd にバッファを持たせたラッパー機能です。
typedef struct {
char mode;
char *ptr;
int rcount;
int wcount;
char *base;
unsigned bufsiz;
int fd;
char smallbuf[1];
} FILE;
socket通信も Stream を繋ぐことで成り立っています。
ネットワーク用語のパケットも Stream をある長さで切ったバイト列と言い換えることができます。
例えば, NIC に対してクライアントからパケットが流れこんできますが, カーネルが検出しドライバがネットワーク/トランスポート層の処理を担当しアプリ側で socket を使って誰からの通信で誰に返すかというのを制御 (実行はカーネルランド) しています。
ファイルディスクリプタとファイルシステム
Linuxカーネルがサポートしているファイルシステムは ext4, jffs など様々ありますが, fopen(3) で Stream を開く場合はファイルシステムは指定はしません。
これは, Linuxカーネルの VFS (virtual File System) が抽象化しているおかげです。
fopen(3) が実行された時の流れを簡単に追ってみます。
FILE *fp = fopen("/path/to/your/file", "r"))
プロセス内でFILE構造体へのポインタを宣言するとVFSが実際のファイルと対応付けを行います。
VFSはfopen(3)の命令を受けてI/Oスケジューラ (ブロックレイヤ)に登録し, i-node番号を用いてブロックデバイスにアクセスします。
VFSは内部にソフトウェアキャッシュを持っており, /proc/meminfo の Cached で確認できます。メモリをあまり使用していないのにメモリ使用率が高く見える原因はこのためです。
また, カーネルはアクセスが低速になるブロックデバイスにユーザ空間のプロセスが引きずられないようになっています。
具体的には, 例えば read(2) はすぐに実行しますが, write(2) は先に戻り値だけ返して実際の書き込みはすぐには行わないことで見かけ上の応答性を高めています。
[1] streamは「小川」つまり細い流れという意味があります。
[2] 通常の pipe以外にも, 拡張された named pipe (FIFO)は mkfifo(3)で作成できファイルのように扱うことができます。
[3] ファイルディスクリプタについて理解する