ファイルディスクリプタ(file descriptor)について調べてみた

Perl Hackers Hub 第6回 UNIXプログラミングの勘所(2)を読んでいたがよくわからなかったので、Operating System ConceptsやMANなどを読んで一から理解してみる。

Operating System Concepts

Operating System Concepts

open()システムコール

The open() system call first searches the system-wide open-file table to see if the file is already in use by another process. If it is, a per-process open-file table entry is created pointing to the existing system-wide open-file table.

If the file is not already open, the directory structure is searched for the given file name. ... Once the file is found, the FCB(File Control Block) is copied into a system-wide open-file table in memory. Next, an entry is made in the per-process open-file table, with a pointer to the entry in the system-wide open-file table.

つまり、fileが既にopenされてればsystem-wide open-fileへのポインタがプロセス毎のopen-fileテーブルにつくられるよ。なければ、system-wide open-fileテーブルにエントリーつくられ、さらにopen-fileテーブルにエントリがつくられるよ。ということですね。

The open() call returns a pointer to the appropriate entry in the per-process file-system table. All file operations are then performed via this pointer. ... UNIX systems refer to it as a file descriptor.

open()システムコールはプロセス毎のfile-systemテーブルエントリへのポインタを返して、ファイル操作はこのポインタ通じてやるよ。で、UNIXではこいつをファイルディスクリプタと呼ぶよ、ってことが書いてありますた。

ファイルディスクリプタとは

per-process file-systemテーブルへのポインタ、ってことになります。
これをLinux(CentOS)上でプログラム書いて、実際どうなってるか確認してみます。

print $$;
my @filehandlers;

for ( 1..10 ) {
    open ( my $fh, '<', './input.txt' ) or die $!;
    push @filehandlers, $fh;
}

sleep 30;

for ( @filehandlers ) {
    close ($_) or warn $!;
}

Linuxだと、/proc/[ps id]/fd/で各プロセスのファイルディスクリプタが見えるのでこいつをlsしてみると...

kotaro@mdev1:~> ls -la /proc/[id]/fd/
lrwx------ 1 kotaro kotaro 64  18 17:30 0 -> /dev/pts/1
lrwx------ 1 kotaro kotaro 64  18 17:30 1 -> /dev/pts/1
lr-x------ 1 kotaro kotaro 64  18 17:30 10 -> /home/kotaro/script/input.txt
lr-x------ 1 kotaro kotaro 64  18 17:30 11 -> /home/kotaro/script/input.txt
lr-x------ 1 kotaro kotaro 64  18 17:30 12 -> /home/kotaro/script/input.txt
lr-x------ 1 kotaro kotaro 64  18 17:30 13 -> /home/kotaro/script/input.txt
lrwx------ 1 kotaro kotaro 64  18 17:30 2 -> /dev/pts/1
lr-x------ 1 kotaro kotaro 64  18 17:30 3 -> /home/kotaro/script/input.txt
lrwx------ 1 kotaro kotaro 64  18 17:30 4 -> /home/kotaro/script/.file_descriptor.pl.swp
lr-x------ 1 kotaro kotaro 64  18 17:30 5 -> /home/kotaro/script/input.txt
lr-x------ 1 kotaro kotaro 64  18 17:30 6 -> /home/kotaro/script/input.txt
lr-x------ 1 kotaro kotaro 64  18 17:30 7 -> /home/kotaro/script/input.txt
lr-x------ 1 kotaro kotaro 64  18 17:30 8 -> /home/kotaro/script/input.txt
lr-x------ 1 kotaro kotaro 64  18 17:30 9 -> /home/kotaro/script/input.txt

てな感じに、シンボリックリンクができてます。

forkするとどうなるか?

ファイルディスクリプタはper-processなので、fork前にopen()していても、ファイルディスクリプタはprocessごとに生成されるはず。

open( my $fh, '<', './input.txt' ) or die $!;

my $pid = fork;
die $! unless defined $pid;

if ( $pid == 0 ) {
   print "child: $$\n";

   sleep 30;
   close $fh or warn $!;
   exit;
}

print "parent: $$\n";

wait;
close $fh or warn $!;
kotaro@mdev1:~> ls -la /proc/[parent]/fd
lrwx------ 1 kotaro kotaro 64  18 22:00 0 -> /dev/pts/1
lrwx------ 1 kotaro kotaro 64  18 22:00 1 -> /dev/pts/1
lrwx------ 1 kotaro kotaro 64  18 22:00 2 -> /dev/pts/1
lrwx------ 1 kotaro kotaro 64  18 22:00 3 -> /home/kotaro/script/.file_descriptor2.pl.swp
lr-x------ 1 kotaro kotaro 64  18 22:00 4 -> /home/kotaro/script/input.txt

kotaro@mdev1:~> ls -la /proc/[child]/fd
lrwx------ 1 kotaro kotaro 64  18 22:00 0 -> /dev/pts/1
lrwx------ 1 kotaro kotaro 64  18 22:00 1 -> /dev/pts/1
lrwx------ 1 kotaro kotaro 64  18 22:00 2 -> /dev/pts/1
lrwx------ 1 kotaro kotaro 64  18 22:00 3 -> /home/kotaro/script/.file_descriptor2.pl.swp
lr-x------ 1 kotaro kotaro 64  18 22:00 4 -> /home/kotaro/script/input.txt

やっぱりプロセスごとにファイルディスクリプタがありますねー。
さて、ちゃんとMANを読んでみると....

open(2)

http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/open.2.html

open() を呼び出すと、「オープンファイル記述」 (open file description) が作成される。ファイル記述とは、システム全体の オープン中のファイルのテーブルのエントリである。 このエントリは、ファイル・オフセットとファイル状態フラグ (fcntl(2) F_SETFL 操作により変更可能) が保持する。 ファイル・ディスクリプタはこれらのエントリの一つへの参照である。 この後で pathname が削除されたり、他のファイルを参照するように変更されたりしても、 この参照は影響を受けない。 新しいオープンファイル記述は最初は他のどのプロセスとも 共有されていないが、 fork(2) で共有が起こる場合がある。

fork(2)

http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/fork.2.html

子プロセスは親プロセスが持つ オープンファイルディスクリプタの集合のコピーを引き継ぐ。 子プロセスの各ファイルディスクリプタは、 親プロセスのファイルディスクリプタに対応する 同じオープンファイル記述 (file description) を参照する (open(2) を参照)。 これは 2 つのディスクリプタが、ファイル状態フラグ・ 現在のファイルオフセット、シグナル駆動 (signal-driven) I/O 属性 (fcntl(2) における F_SETOWN, F_SETSIG の説明を参照) を共有することを意味する。

オープンファイル記述(ファイルオフセットとか)はforkすると共有されるよーということが書いてある。なので、親プロセスでopen()した状態からforkする時は、子プロセスでclose()をしておくのが安全っぽい。(ここまででようやくkazuhoさんの記事の意図がわかった)

どう書くべきか@perl

open( my $fh, '<', './input.txt' ) or die $!;

my $pid = fork;
die $! unless defined $pid;

if ( $pid == 0 ) {
   close $fh or warn $!;
   exit;
}

print <$fh>;
close $fh or warn $!;

forkする前に親プロセスでopen()していたら、子プロセスではすぐにclose()してしまう。ファイルディスクリプタはプロセス毎に管理されるので、子でclose()しても親では問題なく使える。

fileno

標準関数filenoはファイルハンドルを引数に取って、ファイルディスクリプタ(整数値)を返してくれる。

fileno FILEHANDLE
Returns the file descriptor for a filehandle, or undefined if the filehandle is not open. This is mainly useful for constructing bitmaps for "select" and low-level POSIX tty-handling operations. If FILEHANDLE is an expression, the value is taken as an indirect filehandle, generally its name.

open( my $fh, '<', './input.txt' );
warn fileno $fh;
close $fh;

socket

ネットワークプログラミングで使われるsocketもファイルディスクリプタで管理される。

use Socket qw(PF_INET SOCK_STREAM);

socket(my $socket, PF_INET, SOCK_STREAM, 0) or die "socket: $!";
printf "fd: %d\n", fileno $socket;

fileno関数にsocketを渡すと、ファイルディスクリプタの番号を知ることが可能。

まとめ

ファイルディスクリプタについて詳しくなった!そして、OSレベルで挙動がわかるっておもしろいな!