yunomuのブログ

趣味のこと

ファイルとファイルディスクリプタとファイルのようなものたち

 プログラムからファイルを読もうとしたらどうするか。
 まずディスクを選んで、ディスクに書き込まれているファイルシステムを調べる。次にファイルシステムの管理情報に基づいて目的のファイルのデータブロックを探す。具体的にはファイルシステムの形式に従ってディレクトリを辿ったりテーブルを検索したりする。
 見つかったデータブロックからデータを読む。
 以上おわり。

 でも普通、というかLinuxMacでプログラムからファイルを読む時に、ディスクを指定したり、ファイルシステムの形式に従って管理情報やディレクトリの内容を読んだりはしません。
 大抵は、ファイルシステムext4(Linux)だろうがISO 9660(CD ROM)だろうがFAT(フロッピーディスクやメモリーカードなど)だろうが、だいたいパス名をopen()で指定して、read()で読むとかそういう感じだと思います。いわゆるopen/close/read/writeというやつ。

 これがどうなっているのかというと、単にファイルシステムのドライバがopen/close/read/writeという共通インタフェースを提供するようになっていて、ファイルシステム毎に適切なドライバを選んで呼び出しているというだけです。
 この共通インタフェースを仮想ファイルシステムといいます。

 ファイルシステムを仮想ファイルシステムから使えるようにする処理をマウントといいます。mountコマンドでやるアレです。
 マウントでは、ファイルシステムの種類やマウントポイントなどの情報をどこかのテーブルに登録しています。このテーブルはだいたいOSの内部にあって、あれ、なんでOS内にあるんだっけ? まあとにかくUnixLinux系OSでは大抵仮想ファイルシステムというのはOSカーネルに実装されています。FreeBSDのソースパス上でもsys/kern/vfs_xxx.cとかにあるので、結構中核にある感じですよね。まあ、権限とか排他制御の関係でOS内部にあるわけなんですが。
 そんなわけで、以降はUnix/Linux系OSでの実装を元に話します。

 マウントポイントというのは、マウントポイントです。
 Unix/Linux系OSの仮想ファイルシステムでは、ディスクやファイルシステムが違っていてもその全てのファイルシステムを"/"から始まるパス名で、あたかも一つのファイルシステムであるかのように扱っています。
 なので、mountコマンドを引数無しで実行すると出てくるように、"/"はext4でディスク1だけど"/var"はディスク2、"/mnt/cdrom"はISO9660でCD-ROMとか、そういうことになっています。open()でファイル名を指定した時に、このテーブルと照合されて、適切なデバイスとファイルシステムドライバが選択されるようになっています。

 つまり、open()というのは、「どのデバイスを」「どのファイルシステムドライバを使って」「どのファイルに対して」readやwriteなどの処理を実行するか、という情報を決定する処理なのです。
 そして、その対応表というか、決定した情報は、OS内部の管理表に登録されて、その表のエントリ番号が、いわゆるファイルディスクリプタになります。この管理表というやつがプロセスごとにあって、だから1プロセスが開くことができるファイル数の上限というものがあるわけですね。

 ところで、ファイルディスクリプタというものは、例えば標準入力は0、標準出力は1、標準エラー出力は2と決まっていたり、パイプやソケットのようなプロセス間通信やネットワーク通信なんかでも使いますし、その他にもデバイスを扱う時にも出てきて、こいつら全然ファイルじゃないじゃんみたいな事になります。
 ファイルディスクリプタというのは、要はデバイスやファイルシステムドライバやパス名を記録したエントリのキーでしかなくて、ここで登録されているパス名やデバイスというものは、別に具体的なディスクやファイルである必要は無くて、ファイルシステムドライバに、パス名でリソースを指定する機能(=open)と、readやwriteみたいな機能がありさえすれば良いわけです。
 あ、でもパイプやソケットはopen無いですね。まあこれらはパス名とは違う方法でリソースを指定してると思ってください。ソース見た方が早いしわかりやすいと思います。pipe()やsocket()の中で似たような事をやってます。

 要するにファイルシステムドライバにopenやreadやwriteという機能があれば、ファイルがあろうがなかろうが仮想ファイルシステムにマウントすることができます。
 そうやってマウントされたものの例としては、/devだったり/procだったり/sysだったりとか、ああいうOSにログインすると存在するんだけどディスク上には存在しないファイル群があります。

 /dev以下のファイルはハードディスクそのものとか、USBポートとか、コンソールとか、そういう入出力デバイスにつながっていて、これらに読み書きするとハードディスクに書き込んだり、USBポートに繋いだデバイスを制御できたりします。
 例えば以下のプログラムをLinux上で実行するとたぶんディスクの1セクタ目が読めます。Macだとパス名を"/dev/disk0"とかにするといいかも。あとたぶんroot権限が要ります。

#include <stdio.h>
int
main()
{ 
    int fd, i, size;
    unsigned char buf[512];
    fd = open("/dev/hda", 0);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    size = read(fd, buf, 512);
    for (i = 0; i < size; i++) {
        printf("%02x ", buf[i]);
    }
    printf("\n");
    close(fd);
    return 0;
}

最後の2バイトが"55 aa"になっていたら、「このディスクから起動できます」みたいな印です。余談ですけど。

 それと、/dev/ttyというデバイスは、読むとキーボード、書くとディスプレイという事になっているので、さっきのプログラムのパス名を"/dev/tty"に変えて実行すると、改行なりCtrl+Dなりを実行するまでのキー入力をそのままprintf()で書きます。
 ソケットやパイプはちょっと面倒なんだけど、同じくファイルディスクリプタを取得してread/writeすると、他のプロセスやネットワーク上のどこかのデータを読んだり書いたり、つまりデータをやりとりすることができます。
 /procなんかは、OS内部のプロセス情報をあたかもファイルのように見せている。読むとOS内部のメモリから情報をとってきたりしています。/sysも同じ。

 これらも単にハードディスクやキーボードのドライバ、もしくはプロセス間通信のモジュールに、read/writeなどの機能が実装されていて、そのモジュールがファイルディスクリプタのテーブルに登録されているだけです。データのやりとり、入力出力というのは、突き詰めればread/writeがあればなんとかなる事が多いので、まあファイルと同じように扱えれば気楽だよねという発想なんじゃないかと思います。

 標準入力、標準出力、標準エラー出力は、プロセス生成時にあらかじめ適当なデバイスを開いておいたというもので、シェルのパイプやリダイレクトなんかはこの辺のファイルディスクリプタを別なものに付け替える処理をしています。

 FUSEなんかも、仮想ファイルシステムでマウントして使えるようなプログラムを書くためのフレームワークみたいなものです。プロセスをファイルシステムとしてマウントしているみたいな感じ。

 なんか何の話をしてるのかわからなくなってきた。
 ソースかなんか見ながらやった方が面白かったかな。