ファイルを取り扱うシステムコール
ユーザが通常ファイルやディレクトリの内容にアクセスすると、実際にはハードウェアの ブロック型デバイス に保存されているデータへアクセスすることになります。この観点から見ると、ファイルシステムは、ハードディスクパーティションなどの物理的な構成を抽象化し、ユーザに論理的な視点で提供する仕組み であるといえます。
ユーザモードのプロセスは、低レベルのハードウェアを直接操作することはできません。実際のファイル操作はカーネルモードで行われる必要があります。そのため、UNIX オペレーティングシステムでは、ファイルを操作するためのシステムコールが提供されています。プロセスが特定のファイルに対して何らかの処理を行う場合、ファイルのパス名などの引数を指定して適切なシステムコールを発行する必要があります。
また、すべての UNIX カーネルは システム全体の性能向上のために、ブロック型デバイスの効率的な操作に重点を置いています。これには、ディスクキャッシュや遅延書き込み(write-back)、バッファリング機構 などが含まれます。これらの最適化により、ファイルアクセスの速度が向上し、システムの全体的なパフォーマンスが向上します。
ファイルのオープン
プロセスがアクセスできるのは、「オープンされている」ファイルだけです。プロセスはファイルを開くために、次のシステムコールを呼び出します。
fd = open(path, flag, mode);
- path:オープンするファイルの相対パスまたは絶対パス。
- flag:ファイルのオープン方法を指定する(例:読み取り専用、書き込み専用、読み書き両用、追記用など)。また、ファイルが存在しない場合に新しく作成するかどうかも指定できる。
- mode:新規作成するファイルのアクセス権を指定する(flag に O_CREAT を指定した場合のみ使用)。
オープンファイルオブジェクトとファイルディスクリプタ
open() システムコールは、オープンファイルオブジェクト を作成し、ファイルディスクリプタ(file descriptor, fd) と呼ばれる識別子をプロセスに返します。
オープンファイルオブジェクトには、以下の情報が含まれます。
-
ファイルの操作に関するデータ
- ファイルのオープン時に指定した flag の値
- カーネルのバッファメモリ領域へのポインタ
- ファイル内の現在の読み書き位置(offset)。これを ファイルポインタ と呼ぶこともある
-
カーネルの関数ポインタ
- プロセスが呼び出せるカーネル関数の集合
- flag の値に応じて、呼び出せる関数が決まる
オープンファイルオブジェクトは、プロセスとファイルのやり取りを管理するデータ構造 です。一方、ファイルディスクリプタは、そのオブジェクトを識別するための番号 です。そのため、1つのオープンファイルオブジェクトを複数のファイルディスクリプタが指すこともあります(例:dup() や fork() による共有)。
複数プロセスによるファイルのオープン
複数のプロセスが同じファイルを同時にオープンすることも可能です。その場合、ファイルシステムは、各プロセスに対して個別のオープンファイルオブジェクトとファイルディスクリプタを割り当てます。
ただし、UNIXファイルシステムは、複数のプロセスが同じファイルを操作する際の同期処理を提供しません。つまり、複数のプロセスが同じファイルに対して同時に書き込みを行うと、データの整合性が保証されません。
プロセス間でファイルアクセスの競合を防ぐには、flock() や fcntl() などのロック機構を使用する必要があります。これにより、ファイルの全体または一部をロックし、他のプロセスと同期を取ることができます。
新しいファイルの作成
新しいファイルを作成するためには、creat() システムコールを使用することもできます。
fd = creat(path, mode);
ただし、creat() は内部的に open(path, O_CREAT | O_WRONLY | O_TRUNC, mode) と同じ処理を行うため、通常は open() を使うことが推奨されます。
オープンされたファイルへのアクセス
UNIXの通常のファイルは、シーケンシャル(順次的)アクセス と ランダムアクセス の両方が可能です。一方、デバイスファイル や 名前付きパイプ は通常、シーケンシャルアクセスのみ をサポートします。
どちらの場合でも、カーネルはオープンファイルオブジェクト内に ファイルポインタ(オフセット) を保持しており、次に読み書きが行われるファイル内の位置を管理します。
シーケンシャルアクセスとランダムアクセス
UNIXのファイル操作は、シーケンシャルアクセスを基本 としています。つまり、read() や write() システムコールは、常に現在のファイルポインタの位置 を基準にデータの読み書きを行います。
ファイルポインタを変更する必要がある場合は、lseek() システムコールを明示的に呼び出す必要があります。ファイルをオープンすると、カーネルは ファイルポインタを先頭(オフセット 0) に設定します。
ファイルポインタの移動 (lseek)
lseek() システムコールは、ファイルポインタを指定した位置に移動させます。
newoffset = lseek(fd, offset, whence);
引数の説明
- fd :オープンされたファイルのファイルディスクリプタ
- offset :ファイルポインタの新しい位置を計算するための符号付き整数値
- whence :offset の基準を指定する値(以下のいずれか)
- SEEK_SET:ファイルの先頭(オフセット 0) から offset バイト移動
- SEEK_CUR:現在のファイルポインタ位置 から offset バイト移動
- SEEK_END:ファイルの末尾 から offset バイト移動
データの読み取り (read)
read() システムコールは、ファイルからデータを読み取り、プロセスのメモリに格納します。
nread = read(fd, buf, count);
引数の説明
- fd :オープンされたファイルのファイルディスクリプタ
- buf :読み取ったデータを格納するプロセスのバッファのアドレス
- count :読み取るバイト数
カーネルは、ファイルポインタの現在位置から count バイトを読み取り、buf に格納しようとします。
ただし、以下のような場合、count バイトを読み取れないこともあります。
- ファイルの終端(EOF)に達した場合:読み取れるバイト数は count より少なくなる
- パイプやソケットが空の場合:データが供給されるまでブロック(待機)されることがある
このとき、nread の値は実際に読み取れたバイト数を示し、0 の場合は EOF に到達したことを意味します。
また、読み取りが成功すると、ファイルポインタは nread バイト分進みます。
データの書き込み (write)
write() システムコールの引数も read() とほぼ同様ですが、データをファイルに書き込む役割を持ちます。
nwritten = write(fd, buf, count);
書き込み時の動作
- カーネルは count バイトを buf からファイルに書き込もうとする
- 実際に書き込めたバイト数 nwritten を返す(count より少ない場合もある)
- 書き込み成功後、ファイルポインタが nwritten バイト分進む
ファイルのクローズ
プロセスがファイルの内容にアクセスする必要がなくなった場合、以下のシステムコールを呼び出してファイルを閉じることができます。
res = close(fd);
この呼び出しにより、ファイルディスクリプタ fd に関連付けられたオープンファイルオブジェクトが解放されます。
また、プロセスが終了すると、その時点で開いていたすべてのファイルはカーネルによって自動的にクローズされます。
ファイルの名前変更と削除
ファイルの名前を変更したり削除したりする際、プロセスはそのファイルを明示的にオープンする必要はありません。
これらの操作はファイルの中身に対するものではなく、ディレクトリの管理情報に対する処理だからです。
たとえば、以下のシステムコールは、ファイルのエントリ名を変更します。
res = rename(oldpath, newpath);
一方、次のシステムコールは、指定されたパスのファイルエントリをディレクトリから削除し、対応するリンク数(ハードリンクの数)を1つ減らします。
res = unlink(pathname);
ファイルのリンクカウントが 0 になると、カーネルはそのファイルに割り当てられていたディスク領域を解放します。
免責事項
本記事は、筆者の理解に基づいて執筆したものです。正確性には十分配慮していますが、内容の誤りや最新の情報と異なる可能性があります。
本記事の内容を参考にしたことによるいかなる損害についても、筆者は責任を負いかねますのでご了承ください。
正確な情報や書籍に書かれている根拠等はサポートしませんので、ご自身で公式ドキュメントをお調べください。
よって、この内容をAIの学習データに活用することはおすすめしません。