ファイルエントリ

ファイルエントリは NetBSD カーネルのファイル I/O に関連するオブジェクトの中で もっとも高位に位置する。read(2) や write(2) のようなファイル操作を 行うシステムコールは基本的に、内部的にはファイルエントリに対する メソッド呼び出しという形で実装されている。 抽象的な観点では、 ファイルエントリとは UNIX における個々のストリーム I/O チャネルに対応する カーネル側の実体(entity)であると言える。

ファイルエントリとファイルデスクリプタの関係

UNIX の I/O チャネルに対する操作を行う際に、 対象となる I/O チャネルを識別するための ID が ファイルデスクリプタである。NetBSD カーネルは、 プロセスごとにファイルデスクリプタテーブルという配列を持っている。 この配列の要素はファイルエントリへのポインタであり、 そしてこの配列の添字がファイルデスクリプタである。

一つのファイルエントリは複数のファイルデスクリプタに対応することがある。 たとえば、dup(2) システムコールで複製されたファイルデスクリプタは、 複製前のファイルデスクリプタと同一のファイルエントリを参照している。 同様に、fork(2) システムコールの際に、新しいプロセスのために複製された ファイルデスクリプタテーブルの各要素が指すファイルエントリも、 親プロセスと同一のファイルエントリを参照している。 いずれのケースでも、ファイルエントリそのものが複製されることはない ということに注意すべきである。 このことがしばしばプログラマを混乱させる原因となる。

ファイルエントリの内容

ファイルエントリは実際のコードでは struct file という構造体であり、 これは次のような内容を含んでいる:

f_type

これはこのファイルエントリがどのような I/O 実体に対する ファイルエントリかを表わしている。たとえば DTYPE_VNODE, DTYPE_SOCKET, DTYPE_PIPE などがある。 オブジェクト指向という観点ではあまり好ましいスタイルではないが、 NetBSD カーネルの一部のコードでこの ID を見て動作を変えている部分がある。

f_count, f_usecount, f_msgcount

ファイルエントリは 3 種類の参照カウントをもつ。 このうち f_msgcount は LOCAL ドメイン(UNIX ドメイン) ソケットでしか使われない。このようなレイヤ侵害のせいで見通しが良くないため、 ここでは LOCAL ドメインソケットについて無視することにすると、 f_count はいわゆる強参照カウントであり、f_usecount は弱参照カウントである。 具体的には、f_count はファイルデスクリプタから参照されている数である。 一方で、f_usecount はそのファイルエントリに対して何らかの操作を行う際に 増やされ、その操作が終了したら減らされる。以下、単に参照カウンタといえば f_count を、利用カウンタといえば後者を指すこととする。

ファイルエントリが開放されるのは f_count と f_usecount の 両方が 0 になったときである。つまり、close(2) システムコールなどによって ファイルデスクリプタが閉じられて、これが参照しているファイルエントリの f_count が 0 になったとしても、利用中の(f_usecount が 0 でない)ファイル エントリはすぐには開放されない。このような状況は、 たとえばマルチスレッド環境で、二つのスレッドのうち片方が read(2) や write(2) を 行っている最中にもう片方が close(2) を行ったようなケースで起こる。 もちろん、f_count が 0 でないファイルエントリに対する操作が終了した (f_usecount が 0 になった)からといって、 このファイルエントリが開放されることがないのは当然のことである。

ファイルエントリを利用するプログラマは、これらの参照カウントエントリを 直接操作するべきではない。f_count は(前述した LOCAL ドメインソケットを除けば) src/sys/kern/kern_descrip.c におけるファイルデスクリプタ操作関数のみが 制御すべきである。f_usecount は FILE_USE(9) および FILE_UNUSE(9) マクロで 増減するべきである。これらのマクロの使い方には作法が存在するため、 注意が必要である(残念ながら file(9) における説明は不十分である)。

f_flag, f_iflag

このファイルエントリの各種フラグを表わしている。 f_flag はこの I/O チャネルに対する操作に対して影響するフラグを 保持している。具体的には、open(2) システムコールの flag 引数として 渡されるフラグや、fcntl(2) システムコールで設定されるフラグの 一部に対応する内容のフラグを持つ。

f_iflag はファイルエントリそのものの状態を表わすフラグで、 次の 2 つがある:

ファイルエントリを利用するプログラマは、 これらのフラグを直接操作したり参照したりするのは避けるべきである。 初期化が終了したファイルエントリは、FILE_SET_MATURE(9) マクロを使って FIF_LARVAL ビットを落とすことができる(mature=「成熟した」)。 また、利用可能なファイルエントリを調べるためには FILE_IS_USABLE(9) マクロを使用する。なお、利用可能なファイルエントリとは、 具体的には FIF_WANTCLOSE と FIF_LARVAL が両方とも立っていない ファイルエントリのことである。

f_cred

このファイルエントリの持つアクセス権を表わす。 setuid(2) などにより、プロセス自体のアクセス権が変化するという状況が しばしば存在するため、各ファイルエントリは自身のアクセス権を持っている。 つまり、I/O オブジェクトへのアクセス権は、そのプロセス自体の アクセス権とは無関係に、ファイルエントリが生成された時点のものを 利用することになる。

f_offset

現在のファイルオフセットを表わす。 ファイルオフセットはそれぞれの I/O チャネルごとに固有の情報であるため、 それぞれのファイルエントリに付随した情報として持つことになる。 しかしながら、すべてのファイルオブジェクトがシーク可能であるとは限らない。 具体的には DTYPE_VNODE なファイルエントリのみでシーク可能である。 それ以外の種類のファイルエントリではシーク不可能なため、 ファイルオフセットも不要である。 したがって、これがファイルエントリの一部に含まれているのは、 クラス設計という観点では若干美しくはない。しかしながら、C 言語での 実装という観点では、(サイズと実行時間の)オーバーヘッドとのバランスを 考えた上での落とし所といえるだろう。

f_ops と f_data

f_ops はこのファイルエントリに対する各種操作関数のポインタのリストを 持っており(いわゆる仮想関数表)、 また f_data は I/O オブジェクトに固有のデータを保持している 構造体などへのポインタである(いわゆるクロージャ)。

ファイルエントリに対するいくつかの操作では、f_ops にない関数を 経由するものがある。たとえば lseek(2) システムコールは対応するエントリが f_ops に存在しない。代わりに、すべてのファイルエントリで共通の sys_lseek(9) 関数で処理される。しかしながら前述したとおり、 DTYPE_VNODE 以外のファイルエントリはシーク不可能なので、 この関数は内部で f_type をチェックしている。 このように f_type を見て判断するような操作は lseek(2) 以外にもいくつか存在する。 これがオブジェクト指向的な観点であまり良い習慣ではないことは言うまでもない。 しかしながら、仮想関数表が静的な型バインディングの形で実装されているため やむをえない部分がある。これは、実装の手間や実行時コストと設計上の 美しさとの間のトレードオフと言えるだろう。

f_ops を経由しない操作としては、これ以外にファイルエントリの生成がある。 これは I/O オブジェクトの性質に強く依存するため当然といえる。 オブジェクトの生成に関しては、一般的なオブジェクト指向プログラミングでも このような傾向がある。ファイルエントリを生成する操作としては、 open(2) システムコールや socket(2) システムコールなどが存在する。

f_ops には次のようなエントリが存在する:

その他

f_list はアクティブなファイルエントリをつなぐリストのためのタブであり、 f_slock は参照カウンタなどのアトミック性確保のためのシンプルロックである。 ファイルエントリを利用するプログラマは、 これらのエントリを直接いじるべきではない。 しかしながら、f_slock に関しては慣例的な使い方が存在する。

このドキュメントにおいて「ファイルエントリのロック」 とは、対象となるファイルエントリの f_slock をロックすることであり、 「ロックされたファイルエントリ」とは、f_slock がロックされた ファイルエントリのことである。

ファイルエントリのライフタイム

ファイルエントリの生成

ファイルエントリは open(2) システムコールや socket(2) システムコールなどが 呼び出されたときに生成される。 カーネルの内部では、ファイルエントリは falloc(9) 関数によって 常にファイルデスクリプタと一緒に割り当てられ、 そして各システムコールなどに固有の初期化処理が行われる。

falloc(9) によって割り当てたれたファイルエントリは、 次のように初期化されている:

これ以外のエントリは無効となっており、したがって呼び出し元は f_type, f_ops, f_data, f_flag, f_ofs を適宜設定し、その後で FILE_SET_MATURE(9) および FILE_UNUSE(9) を実行する必要がある。

具体的には、次のようなコードになる:

	int fd, ret;
	struct file *fp;
	struct proc *p;
...
	ret = falloc(p, &fp, &fd);
	if (ret)
		return ret;
	fp->f_type = FTYPE_MISC;
	fp->f_ops = &foo_ops;
	fp->f_data = /* 好きな値 */;
	fp->f_flag = FWRITE | FREAD | /* 好きな値 */;
	fp->f_offset = /* 好きな値 */
	FILE_SET_MATURE(fp);
	FILE_UNUSE(fp, p);
	*retval = fd;

	return 0;

なお、ファイルエントリの初期化に必要となるデータのエラーチェックは、 falloc(9) を呼び出す前に行っておくべきである。 もし、どうしても初期化前のファイルエントリを削除したい場合には、 次のようにする:

	struct filedesc *fdp;
	struct file *fp;
	struct proc *p;
...
	FILE_UNUSE(fp, p);
	fdremove(fdp, fd);
	ffree(fp);

この操作を FILE_SET_MATURE(9) を実行した後のファイルエントリに対して行うと、 破滅的な結果を生じることがあるため行ってはならない。

ファイルエントリの参照

あるファイルデスクリプタに対応するファイルエントリは、 fd_getfile(9) 関数で取得できる。fd_getfile(9) で返された ファイルエントリはロックされている。fd_getfile(9) の呼び出しの後、普通は

のどちらかで受ける。いずれの操作の場合も、FILE_UNUSE(9) ないしは アンロックを行った後に、このファイルエントリを操作してはならない。 再びこのファイルエントリを操作したい場合には、 再度 fd_getfile(9) を呼び出さなければならない。

なお、割り当てられていないファイルデスクリプタや、 FIF_LARVAL や FIF_WANTCLOSE がセットされているファイルエントリに 対する fd_getfile(9) の呼び出しは失敗し、NULL を返す。

たとえば、あるファイルデスクリプタ fd に対する書き込み操作は、 次のようなコードになる:

	int fd, ret;
	struct filedesc *fdp;
	struct file *fp;
	struct proc *p;
...
	fp = fd_getfile(fdp, fd);
	if (fp == NULL)
		return EBADF;

	if ((fp->f_flag & FWRITE) == 0) {
		simple_unlock(&fp->f_slock);
		return EBADF;
	}
	FILE_USE(fp);
	ret = (*fp->f_ops->fo_write)(fp, &fp->f_offset, uio, fp->f_cred,
				     fp->f_flags);
	FILE_UNUSE(fp, p);

	return ret;

これが fd_getfile(9) を利用する際のイディオムである。

ファイルエントリの開放

先に述べた通り、参照カウンタとの兼ね合いがあるため、 ファイルエントリの削除は若干複雑である。 ファイルエントリを単に利用するプログラマとしての観点では、 不要になったファイルデスクリプタに対して fdrelease(9) 関数を呼び出すということを覚えておくだけで事が足りる:

	int fd;
	struct file *fp;
...
	fdrelease(p, fd);

もちろん、この呼び出しですぐにファイルエントリが削除されるとは限らない。 FILE_SET_MATURE(9) された後のファイルエントリを すぐに削除するような安全な方法は存在しない。

fdrelease(9) の内部では、

  1. ファイルデスクリプタに対応するファイルデスクリプタテーブルの エントリを無効とする。

  2. kqueue のための通知を行う

  3. closef(9) を呼び出して、ファイルエントリの参照(f_count)を落とし、 必要に応じてこれを削除する。

という処理が行われる。なお、一つのファイルデスクリプタからのみ 参照されているファイルエントリにおいて、 このファイルデスクリプタに対して fdrelease(9) を実行すると、 もし、このファイルエントリが別のところで使用されていない場合には、 ファイルデスクリプタとともにこのファイルエントリはすぐに開放されるが、 一方でこのファイルエントリが別のところで使用中だった場合、 このエントリに対して FILE_UNUSE(9) が実行されるまで fdrelease(9) の中でブロックすることを注記しておく。つまり、 次のようなコードはデッドロックする可能性がある:

	struct filedesc *fdp;
	struct file *fp;
	struct proc *p;
...
	fp = fd_getfile(fdp, fd);
	FILE_USE(fp);
	fdrelease(p, fd); /* !!!デッドロック!!! */

これは、あるファイルエントリの利用権を持っている LWP は、 そのファイルエントリに対して fdrelease(9) を呼び出しては ならないということを意味する。 利用権を持っている LWP は、必ず FILE_UNUSE(9) を呼び出して 利用権を返却したのちに fdrelease(9) を呼ぶ必要がある。 もちろん、他の LWP が利用権を持っていても気にする必要はない (しかしながら、ブロックする可能性があることは覚えておくべきだろう)。

f_ops の fo_close が指す関数は、ファイルエントリが削除されるときに 一度だけ呼び出される。複数のファイルデスクリプタから参照されている ファイルエントリも、fo_close が呼び出されるのは最後の fdrelease(9) 時の 一回だけである。

ファイルエントリ関連関数/マクロ解説

ここで解説する関数のいくつかは、ファイルデスクリプタテーブルの操作と 重複している。

falloc()

int falloc(struct proc *p, struct file **resultfp, int *resultfd);

行うことは次のとおりである:

  1. プロセス構造体 *p がもつファイルデスクリプタテーブルに 新しいファイルデスクリプタを確保する。

  2. 新しいファイルエントリを一つ作成する。

  3. 1 のデスクリプタが 2 のファイルエントリを参照するようにする。

結果として得られたファイルデスクリプタは *resultfd に、 ファイルエントリへのポインタは *resultfp へと格納される。 ファイルエントリへのポインタが必要なければ resultfp として NULL を渡すことができるが、これは事実上無意味である。 なぜならば、resultfp に頼る以外に、 このファイルエントリのポインタを取得する方法が存在しないからである (fd_getfile() は初期化前のファイルエントリを無視する)。

正常終了時は 0 が返り、それ以外の時にはエラーコードが返る。

この関数で作成されたファイルエントリの状態や、具体的な使い方については ファイルエントリの生成 を参照のこと。

fd_getfile()

struct file *fd_getfile(struct filedesc *fdp, int fd);

ファイルデスクリプタテーブル fdp のファイルデスクリプタ fd に 対応するファイルエントリへのポインタを返す。対応する fd に対応するファイルデスクリプタが存在しない場合や、 対応するファイルエントリに FIF_LARVAL および FIF_WANTCLOSE が設定されている場合には NULL が返る。

この関数はファイルエントリを返す前に f_slock をロック状態にする。 したがって、これを呼び出す際には再帰ロックとならないよう注意する必要がある。 それ以外のメンバは操作しない。 この関数の使い方については ファイルエントリの参照 を参考のこと。

fdrelease()

int fdrelease(struct proc *p, int fd);

プロセス p がもつファイルデスクリプタテーブル中の ファイルデスクリプタ fd を開放する。 同時に、対応するファイルエントリへの参照カウンタを一つ減らす。 成功すれば 0 を返し、失敗したときはエラーコードを返す。

fdrelease() はファイルデスクリプタを閉じるための標準的な関数である。 詳しくは ファイルエントリの開放 を参照のこと。

fdremove()

ffree()

closef()

void fdremove(struct filedesc *fdp, int fd);
void ffree(struct file *fp);
int closef(struct file *fp, struct proc *p);

これらの関数はファイルエントリの削除などにかかわる関数である。 しかしながら、ファイルエントリを単に利用するプログラマがこれらの 関数を直接呼び出すことが必要になるケースはそれほど多くはない。

fdremove() は、指定されたファイルデスクリプタを ファイルデスクリプタテーブルから削除する。この際、 このファイルデスクリプタが参照していたファイルエントリは 一切影響を受けない。これは参照カウンタについても例外ではないため、 fdremove() を呼び出した後に呼び出し元の責任で参照カウンタを 注意深く操作し、必要に応じて closef() などの関数を呼び出す必要がある。

ファイルエントリを単に利用するプログラマが fdremove() を直接呼び出す必要があるのは、 FILE_SET_MATURE() される前のファイルエントリを削除する場合に 限られるだろう。この場合、f_count が 1 であることと f_usecount が 高々 1 (= 自分自身のコンテキストによる参照分)であることが保証されており、 また fo_close による後処理も普通は必要がないため、 あとはこのファイルエントリを削除するだけである( ファイルエントリの生成 を参照)。

ffree() 関数は、ファイルエントリ fp を開放する。 この関数は、アクティブなファイルエントリのリストからの削除と f_cred の開放を除けば単なるメモリ領域の開放を行うだけの関数である。 参照カウンタのたぐいには一切関知しないため、たとえば ファイルデスクリプタテーブルから参照されているファイルエントリに対して ffree() を実行すると破滅する。 つまり、この関数を呼び出すときには、他からこのファイルエントリが 参照されていないことを保証するのはプログラマの責任となる。 また、ファイルエントリのロックは開放状態にしておくべきである。 ファイルエントリを単に利用する プログラマが ffree() 関数を直接呼び出す必要があるのは fdremove() と同様の場合のみであろう。

closef() 関数は、ファイルエントリ fp の参照カウンタを減らし、 これが 0 になったら開放する。 開放される直前に一回だけ f_ops の fo_close を呼び出す。 ただし、参照カウンタが 0 になったとしても、 このファイルエントリの利用カウンタが 0 にならないうちは 開放は遅延される。このとき、closef() を呼び出した LWP は遅延している間ブロックする。ブロックされる前には FIF_WANTCLOSE フラグが設定される。 closef() におけるデッドロックの可能性については ファイルエントリの開放 と同様である。 この関数は内部で f_slock を区間的にロックするため、 これを呼び出す際には再帰ロックとならないよう注意する必要がある。 プログラマがこの関数を直接呼び出す必要はほとんどないだろう。

FILE_USE()

void FILE_USE(struct file *fp);

FILE_USE() マクロはファイルエントリの利用カウンタを増やし、 f_slock のロックを解除する。したがって、このマクロを呼び出す際には f_slock のロックをあらかじめ取得しておく必要がある。 ファイルエントリを単に利用するプログラマがこのマクロを呼び出すのは ファイルエントリの参照 で挙げたような使い方に限られるだろう。

FILE_UNUSE()

void FILE_UNUSE(struct file *fp, struct proc *p);

FILE_UNUSE() マクロはファイルエントリの利用カウントを減らす。 FIF_WANTCLOSE フラグが立っているファイルエントリの利用カウントを 0 にするような FILE_UNUSE() の呼び出しは、内部で closef() を実行して このファイルエントリを実際に削除することになる。 このマクロは内部で f_slock を区間的にロックするため、 これを呼び出す際には再帰ロックとならないよう注意する必要がある。

FILE_SET_MATURE()

void FILE_SET_MATURE(struct file *fp);

このマクロは、ファイルエントリの初期化が終了したことをマークする。 具体的には FIF_LARVAL フラグを落とす。

FILE_IS_USABLE()

int FILE_IS_USABLE(struct file *fp);

このマクロは、ファイルエントリが利用可能であるかどうかを判定する。 利用可能なファイルエントリとは、FIF_LARVAL も FIF_WANTCLOSE も 立っていないファイルエントリのことである。

サンプル

/dev/null と同様な振る舞いをするファイルデスクリプタを生成する システムコールを追加する LKM と、そのサンプルプログラムの例を ./struct-file-example/ へと示す。

これは次のようにビルド/実行される:

% make obj MAKEVERBOSE=0
obj ===> test
% make dependall MAKEVERBOSE=0
dependall ===> test
% sudo modload ./obj/nullfile.o
Module loaded as ID 0
% ./test/obj/test_nullfile
call syscall(210).
nullfile: open fd=4, fp=0xd22a8c90
fd = 4
call read.
0 bytes read.
call write.
256 bytes written.
sleeping ... done.
nullfile: close fp=0xd22a8c90
% sudo modunload 0    

ファイルエントリの問題点

f_ops の機能が足りない

ファイルエントリの f_ops は、あくまでも基本的なストリーム I/O の機能しか提供していない。 たとえば mmap に相当する機能を実現することができない。 このような機能のほとんどは vfs/vnode に固有のものである。

参考

更新履歴