ファイルディスクリプタについて解説

Linux

本記事では、ファイルディスクリプタについて解説します。

本記事でのコマンドやプログラムの実行は全て「Ubuntu 22.04 LTS」環境で行っています。

基本的な仕組み

ファイルディスクリプタ(ファイル記述子、fd)とは、OSがファイルや他の入出力リソースへの参照を管理するために使用される番号(符号なし整数)のことです。POSIX1標準で定められており、UNIX系OSで一般的に使用されています。ファイル、ディレクトリ、ソケット、パイプなどの入出力リソースを指すために使用されます。

POSIXに関しては、以下の記事で解説しています。

ファイルディスクリプタの割り当て・解放

オープン

ファイルディスクリプタはプロセス単位で管理されており、プロセスがファイルをオープンすることで、OSはそのプロセスの利用可能な最小のファイルディスクリプタを割り当てます。プロセスは割り当てられたファイルディスクリプタを使用して、読み出しや書き込みなどの操作をそのファイルディスクリプタが指し示すファイルに対して行うことができるようになります。

プロセスが生成されると、ファイルディスクリプタ番号「0」,「1」,「2」に対して、それぞれ「標準入力(stdin)」,「標準出力(stdout)」,「標準エラー出力(stderr)」が自動的に割り当てられます。

標準入出力(標準入力、標準出力、標準エラー出力)に関しては、以下の記事で解説しています。

プロセスがファイルをオープンしたときの動作イメージは以下の図のようになります。

open()

ファイルをオープンしファイルディスクリプタを割り当てるために、 open() というシステムコール2が用意されています。

[書式]

#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

[引数]

引数名意味
pathname       オープンしたいファイルパス名を指定します。
flagsファイルをオープンするときのアクセスモードを表すフラグを指定します。以下のいずれかの値を取ります。
O_RDONLY:読み取り専用です。
O_WRONLY:書き込み専用です。
O_RDWR:読み書き可能です。

また、オプションとして、以下の値などを組み合わせることができます。
O_CREAT:指定されたファイルが存在しない場合、新しいファイルを作成します。
O_EXCL:新しいファイルを作成する際にそのファイルが既に存在する場合はエラーを報告します。「O_CREAT | O_EXCL」のようにO_CREAT と組み合わせて使用されます。
O_TRUNC:ファイルが既に存在する場合、その長さを0に切り詰めて(空にして)開きます。つまり、既存ファイルの内容は全て削除されます。
O_APPEND:書き込み操作が常にファイルの末尾に追加されます。

※「O_RDWR | O_CREAT」のように、論理和の形式で組み合わせて使用します。また、オプションは複数組み合わせて使用することが可能です。
modeファイルを新しく作成する場合、8進数でパーミッション(アクセス権)を指定します。
例えば、「0644」は所有者には読み書き権限を、グループと他のユーザーには読み取り権限のみを与えます。

[戻り値]

open() は成功すると、ファイルディスクリプタ番号を返します。失敗すると、-1 を返し、errno3 にエラー情報が設定されます。

open()には、引数が2つの形式と3つの形式が存在します。既に存在するファイルをオープンする場合は、引数が2つの形式を使用します。ファイルを新たに作成する場合は、パーミッションを指定する必要があるので、引数が3つの形式を使用します。

クローズ

オープンによって入出力リソースに対して割り当てられたファイルディスクリプタは、クローズを行うことで解放されます。これにより、プロセスによる対象の入出力リソースへのアクセスは終了し、使用されていたファイルディスクリプタ番号は再利用可能な状態となります。

close()

ファイルディスクリプタをクローズするために、close() というシステムコールが用意されています。

[書式]

#include <unistd.h>

int close(int fd);

[引数]

引数名意味
fd       クローズするファイルディスクリプタを指定します。             

[戻り値]

close() は成功すると、0を返します。失敗すると、-1 を返し、errno にエラー情報が設定されます。

リソースリーク

ファイルディスクリプタがクローズされない場合、そのファイルディスクリプタは「使用中」とみなされ続けます。ファイルディスクリプタの数は限られているため、クローズされずに次々と新規リソースのオープンが行われていくと、いずれオープンできなくなり、プロセスの動作に影響を及ぼすことがあります。これをリソースリークと呼びます。

プロセスが終了するとファイルディスクリプタも解放されますが、デーモン4などバックグラウンドで常に動作し続けるプロセスにおいては、クローズ処理が漏れるとリソースリークのリスクが高まるので注意が必要です。

ファイルの読み書き

リード

プロセスはファイルディスクリプタを通してリードを行うことで、ファイルの中身を読み出すことが可能です。

例えば、以下のようなテキストファイルがあった場合、このファイルに対応するファイルディスクリプタに対してリードを行うことで、”Hello, world!” の文字列を読み出すことが可能です。

Hello, world!
read()

ファイルディスクリプタを通してファイルの中身を読み出すために、read() というシステムコールが用意されています。

[書式]

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

[引数]

引数名意味
fd読み込むファイルのファイルディスクリプタを指定します。
buf読み込んだデータを格納するバッファへのポインタを指定します。         
count     読み込むバイト数を指定します。

[戻り値]

read() は成功すると、読み込んだバイト数(0 以上)を返します。 0 の場合、ファイルの終わり(EOF)に達したことを意味します。失敗すると、-1 を返し、errno にエラー情報が設定されます。

ライト

プロセスはファイルディスクリプタを通してライトを行うことで、ファイルにデータを書き込むことができます。

例えば、文字列データ “Hello, world!” をファイルディスクリプタにライトを行うと、対応するファイルに “Hello, world!” の文字列が書き込まれます。

write()

ファイルディスクリプタを通してファイルにデータを書き込む場合、write() というシステムコールが用意されています。

[書式]

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

[引数]

引数名意味
fd書き込むファイルのファイルディスクリプタを指定します。
buf書き込むデータを含むバッファへのポインタを指定します。            
count    書き込むバイト数を指定します。

[戻り値]

write() は成功すると、書き込まれたバイト数(0以上)を返します。書き込むデータの量(count)よりも少ない値が返されることもあります(例えば、ディスクの空き容量が不足している場合など)。失敗すると、-1 を返し、errno にエラー情報が設定されます。

オフセット

オフセットとは、ファイルディスクリプタを通してファイルアクセスする場合において、ファイルの先頭からの距離(バイト数)を表し、現在のファイル読み書き位置を指定します。

初期オフセット

ファイルをオープンした直後、オフセットは通常、ファイルの先頭(つまり、0番目のバイト)に設定されます。この位置から読み書き操作が開始されます。

読み書き時のオフセット更新

read() や write() を使用してファイルからデータを読み込んだり、ファイルにデータを書き込んだりすると、オフセットは自動的に読み書きされたバイト数分進みます。例えば、100バイト読み込んだ場合、オフセットは次の読み書きのために100バイト進んだ位置に設定されます。

lseek()

lseek() というシステムコールを使用して、任意の位置にオフセットを設定することができます。これにより、ファイルの任意の位置から読み書きを開始できます。

[書式]

#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

[引数]

引数名意味
fdオフセットの変更を行うファイルのファイルディスクリプタを指定します。     
offset移動する相対位置(バイト数)を指定します。
whence   オフセットの基準点を指定します。以下のいずれかの値を取ります。
・SEEK_SET:ファイルの先頭からのオフセットになります。
・SEEK_CUR:現在の読み書き位置からの相対的オフセットになります。
・SEEK_END:ファイルの末尾からの相対的オフセットになります。

[戻り値]

lseek() は成功すると、ファイル内の新しい読み書き位置(ファイルの先頭からのバイト数)を返します。失敗すると、-1 を返し、errno にエラー情報が設定されます。

ファイルのロック

ファイルディスクリプタが指すファイルに対して読み書きを行う際、ファイルにロックをかけることができます。複数のプロセスが同時に同一のファイルにアクセスする可能性がある場合、ロックをかけることで、読み書きにおけるデータの整合性を保つことが可能です。

flock()

flock() というシステムコールを使用してファイルのロックを行うことが可能です。

[書式]

#include <sys/file.h>

int flock(int fd, int operation);

[引数]

引数名意味
fdロックしたいファイルのファイルディスクリプタを指定します。          
operation     実行する操作を指定します。以下のいずれかの値を取ります。
LOCK_SH:共有ロックを行います。
LOCK_EX:排他ロックを行います。
LOCK_UN:ロック解除を行います。

また、オプションとして、以下の値を組み合わせることができます。
LOCK_NB:ノンブロッキングモードでロック操作を行います。そのため、指定したロックを取得できない場合、flock() はすぐに制御を戻し、エラーを返します。
※「LOCK_SH | LOCK_NB」のように、論理和の形式で組み合わせて使用します。

[戻り値]

flock() は成功すると、0 を返します。失敗すると、-1 を返し、errno にエラー情報が設定されます。

flock() には以下のような特徴があります。

  • ファイル全体に対してのみロックを適用します(部分的なロックはできません)。
  • ロックはプロセスベースであり、同一プロセス内の全てのスレッドに影響します。
  • 親プロセスから子プロセスへファイルディスクリプタが継承されても、ロック状態は継承されません
flock() よりも詳細なロックを行いたい場合、fcntl()を使用できます。この関数は以下のような特徴を持ちます。
・ファイルの特定のセクションに対してロックを適用することができます(バイト範囲指定)。
・ロックはファイルディスクリプタに関連付けられ、ファイルディスクリプタの複製(例えば、dup() による)や親プロセスから子プロセスへの継承が可能です。
・ロックはプロセスではなく、ファイルディスクリプタに対して行われるため、同一プロセス内の異なるスレッドやファイルディスクリプタ間で独立してロックを管理できます。

サンプルコードの説明・実行

ファイルディスクリプタを扱うサンプルコード(sample.c)を用意したので、内容について簡単に説明し、実行していきたいと思います。

サンプルコード

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

int main()
{
	char *filepath = "sample.txt";
	char writeBuffer[] = "Hello, world!";
	char readBuffer[100];
	int bytesWritten, bytesRead;

	int fd = open(filepath, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
	if (fd == -1)
	{
		perror("Error opening file");
		return 1;
	}

	bytesWritten = write(fd, writeBuffer, strlen(writeBuffer));
	if (bytesWritten == -1)
	{
		perror("Error writing to file");
		close(fd);
		return 1;
	}

	if (lseek(fd, 0, SEEK_SET) == -1)
	{
		perror("Error seeking file");
		close(fd);
		return 1;
	}

	bytesRead = read(fd, readBuffer, sizeof(readBuffer) - 1);
	if (bytesRead == -1)
	{
		perror("Error reading from file");
		close(fd);
		return 1;
	}

	readBuffer[bytesRead] = '\0';

	printf("Read from file: %s\n", readBuffer);

	if (close(fd) == -1)
	{
		perror("Error closing file");
		return 1;
	}

	return 0;
}

処理の流れ

このサンプルコードは以下のような流れで処理を行っています。

  1. sample.txt をオープン(新規作成)
  2. sample.txt に “Hello, world!” の文字列を書き込み
  3. ファイルディスクリプタのオフセットをファイルの先頭に移動
  4. sample.txt の中身を読み出し
  5. 読み出したデータを標準出力
  6. sample.txt をクローズ

1. sample.txt をオープン(新規作成)

open() で sample.txt のファイルディスクリプタを作成します。

int fd = open(filepath, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);

第1引数にファイルパス(sample.txt)を指定しています。

第2引数に「O_RDWR | O_CREAT」を指定しています。O_RDWR は、読み書き可のアクセス権を表します。また、O_CREAT は、指定されたファイルが存在しない場合には、新しいファイルを作成することを意味します。

第3引数に「S_IRUSR | S_IWUSR」を指定しています。S_IRUSR は8進数における 0400 を表し、ファイルの所有者に読み込み権限を与えることを意味します。S_IWUSR は8進数における 0200 を表し、ファイルの所有者に書き込み権限を与えることを意味します。そのため、S_IRUSR | S_IWUSR は 0600 を表し、ファイル所有者に読み書き権限を与えることを表します。

2. sample.txt に “Hello, world!” の文字列を書き込み

write() で sample.txt に文字列を書き込みます。

bytesWritten = write(fd, writeBuffer, strlen(writeBuffer));

第1引数に作成したファイルディスクリプタを指定し、第2引数に “Hello, world!” の文字列を指定しています。

3. ファイルディスクリプタのオフセットをファイルの先頭に移動

lseek() でファイルディスクリプタのオフセットをファイルの先頭に移動させます。

if (lseek(fd, 0, SEEK_SET) == -1)

第2引数に 0 、第3引数に SEEK_SET を指定することで、ファイルの先頭から0バイト目にオフセットを移動させることができます。

4. sample.txt の中身を読み出し

read() で sample.txt の中身を読み出します。lseek() でオフセットをファイルの先頭に変更したので、先頭から第2引数に指定している sizeof(readBuffer) のサイズ分(100バイト) 読み出されます。

bytesRead = read(fd, readBuffer, sizeof(readBuffer) - 1);

読み出したデータは、第2引数に指定している readBuffer に格納されます。また、読み出したデータのバイト数が戻り値として返ります。

5. 読み出したデータを標準出力

read() で読み出したデータを標準出力します。

まず、読み出したデータの末尾に終端文字 ‘\0’ を書き込みます。

readBuffer[bytesRead] = '\0';

そして、printf() で標準出力します。

printf("Read from file: %s\n", readBuffer);

6. sample.txt をクローズ

close() でファイルディスクリプタをクローズします。

if (close(fd) == -1)

処理の実行

サンプルコードをビルドし、実行ファイル(sample) を作成します。

$ gcc sample.c -o sample
$ ls
sample  sample.c

sample を実行すると、sample.txt が作成され、「Read from file: Hello, world!」と標準出力されます。

$ ./sample 
Read from file: Hello, world!
$ ls
sample  sample.c  sample.txt

sample.txt には、”Hello, world!” が書き込まれています。

$ cat sample.txt 
Hello, world!

最後に

ファイルディスクリプタの基本的な仕組みと、サンプルコードの解説を行ってきました。

ファイルディスクリプタに関しては、まだまだ学ぶべき深い内容が多く存在するので、今後記事にしたいと思っています。

引き続きよろしくお願いします。

参考資料

脚注

  1. UNIX系OS間での互換性を確保するための、IEEE標準の規格です。C言語からシステムコールを利用するためのAPIなどが定められています。 ↩︎
  2. アプリケーションがカーネルの機能を呼び出すためのインターフェースのこと。 ↩︎
  3. 標準のエラーコードを保持するグローバル変数のこと。 ↩︎
  4. システムの起動時に開始され、システム運用中もバックグラウンドで継続して動作するプログラムのこと。こちらの記事で解説しています。 ↩︎
タイトルとURLをコピーしました