Linuxのファイル操作を徹底解説|システムコール・ファイルディスクリプタ・inodeの仕組みLinuxファイル操作

はじめに

Linuxでのファイル操作について、概要は理解しているものの、カーネルレベルでの詳細な挙動まで理解している人は意外と少ないかもしれません。

本記事では、Linuxカーネルがファイルに対して実行する Open、Read、Write、Close の処理の流れや、ファイル識別の仕組みを詳しく解説します。Linux内部でのファイル操作の仕組みを理解したい方は、ぜひ参考にしてください。

なお、ファイルシステムを経由したファイル操作の概要については、下記の記事で整理しています。初学者の方はこちらからご覧いただくと理解が進みやすいです。

Linuxエンジニア必見!ファイルシステムの基礎知識と仕組みをわかりやすく解説

Linuxファイル操作

straceによるシステムコール追跡

まずは実機でファイル操作を行い、挙動を確認してみましょう。例として test.txt を作成します。

[root@appserver-dev tmp]# echo "Hello Linux" > test.txt
[root@appserver-dev tmp]# cat test.txt
Hello Linux

続いて、straceコマンドにてシステムコールを追跡します。-e trace=オプションにて、追跡したいシステムコールを指定して実行してみてください。ここでは、システムコールとしてopenat()※、read()、write()、close()を指定しています。

※openat() は open() に代わるシステムコールで、ディレクトリを表すファイルディスクリプタを基準にファイルを開くことができます。これにより、カレントディレクトリに依存せず安全にファイルを開くことができ、ディレクトリ切替による競合状態(race condition)を防ぐことができます。

下記の通りトレース結果が表示されれば成功です。

[root@appserver-dev tmp]# strace -e trace=openat,read,write,close cat test.txt
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\227\2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

~~~~

openat(AT_FDCWD, "/usr/lib/locale/C.utf8/LC_CTYPE", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
openat(AT_FDCWD, "test.txt", O_RDONLY)  = 3
read(3, "Hello Linux\n", 131072)        = 12
write(1, "Hello Linux\n", 12Hello Linux
)           = 12
read(3, "", 131072)                     = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
+++ exited with 0 +++
[root@appserver-dev tmp]#

それでは、このトレース結果を解説していきます。

openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\227\2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

~~~~

openat(AT_FDCWD, "/usr/lib/locale/C.utf8/LC_CTYPE", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0

最初に多くの openat() が出力されています。ここでは、cat コマンドが利用するライブラリやロケール情報を読み込んでいます。

openat(AT_FDCWD, "test.txt", O_RDONLY) = 3

ここで test.txt を読み取り専用(O_RDONLY)で開いています。システムコールは成功し、ファイルディスクリプタとして 3 が割り当てられました。AT_FDCWD はカレントディレクトリを基準にしていることを示します。

ファイルディスクリプタ
Linux などの Unix 系 OS において、プロセスがファイルや入出力リソースにアクセスするための識別子で、数値で表されます。この値はプロセスごとに割り当てられます。

read(3, "Hello Linux\n", 131072) = 12

ファイルディスクリプタ 3(= test.txt)から “Hello Linux\n” という 12 バイトのデータが読み込まれました。3 つ目の引数 131072 は「最大で 131072 バイトを読み込む」指定ですが、実際に読み込まれたのは 12 バイトです。

write(1, "Hello Linux\n", 12Hello Linux ) = 12

読み込んだ “Hello Linux\n” を 12 バイト書き込みました。書き込み先はファイルディスクリプタ 1(標準出力 stdout)で、この処理によりターミナルにファイルの内容が表示されます。

read(3, "", 131072) = 0

再度ファイルディスクリプタ 3 から読み込みを試みましたが、既にファイルの終端に達しているため、返り値は 0 となっています。

close(3) = 0 close(1) = 0 close(2) = 0

ファイルディスクリプタ 3、1、2 を順に閉じ、リソースを解放しています。

+++ exited with 0 +++

0 は成功コード(エラーなし)を示しており、プログラムが正常終了したことを表しています。

なお、ファイルディスクリプタ番号には標準的な割り当てがあります。

番号名称内容
0stdin標準入力(キーボードなど)
1stdout標準出力(画面表示など)
2stderr標準エラー出力

今回の test.txt の読み込みでは、これらとは別に 3 が新たに割り当てられました。

ファイル識別の仕組み

ユーザとしてはファイル名を指定して操作を行いますが、Linuxカーネルは inode という識別子でファイルを認識しています。では、どのようにファイル名と inode が紐付けられているのかを見てみましょう。

ファイルディスクリプタとファイルディスクリプタテーブル

各プロセスが操作するファイルには ファイルディスクリプタ という識別子が割り当てられます。これはプロセスごとに管理され、ファイルディスクリプタテーブル に格納されます。

ファイルディスクリプタテーブルの各エントリは、カーネル内部の構造体 struct file へのポインタを保持しています。

struct file(オープンファイルテーブル)

struct file は「オープンファイルテーブル」の実体であり、プロセスが open() したファイルの状態を管理します。open() を呼び出すたびに、新しい struct file が生成されます。

主な項目は以下の通りです:

  • f_pos: ファイル読み書き位置(オフセット)
  • f_flags: open 時のフラグ(O_RDONLY, O_APPEND など)
  • f_op: ファイル操作関数テーブル
  • f_path: ファイルのパス解決結果(dentry への参照を含む)
  • f_inode: 対応する inode への参照(実際には f_path.dentry->d_inode を介して取得)

struct inode(inodeテーブル)

struct file はさらに inode を参照します。inode テーブルの実体は struct inode であり、これはファイル自体のメタデータを保持する構造体で、ディスク上の inode に対応します。

struct inode の主な項目は以下の通りです。

  • i_mode: 種類 (ファイル/ディレクトリ/デバイス) とパーミッション
  • i_uid / i_gid: 所有者情報
  • i_size: ファイルサイズ
  • i_blocks / i_mapping: データブロックへのマッピング
  • i_op / i_fop: inode やファイルに対応する操作関数

特に i_mapping は、ファイルの論理オフセットを実際のデータブロックやページキャッシュに対応付ける重要な役割を担っています。

データブロック

inode を介して、ファイルの実体であるデータブロックにアクセスします。データブロックとは、ファイルシステムがファイル内容をディスク上に保存する最小単位です。物理セクタとは異なる論理単位で、サイズはファイルシステムによって異なりますが、一般的には 4KB(4096バイト) がよく使われます。

データブロックへの割り当て

作成した test.txt がどのようにデータブロックに割り当てられているかを確認してみましょう。filefrag -v コマンドを実行すると、ファイルに関する各種情報を取得できます。

[root@appserver-dev tmp]# filefrag -v test.txt
Filesystem type is: 58465342
File size of test.txt is 12 (1 block of 4096 bytes)
ext:     logical_offset:        physical_offset: length:   expected: flags:
0:        0..       0:    1154091..   1154091:      1:             last,eof
test.txt: 1 extent found
[root@appserver-dev tmp]#
  • Filesystem type is : ファイルシステムの種類。58465342 は XFS を表します。
  • File size of test.txt is : 実際のファイルサイズ。1ブロックは 4096 バイトですが、このファイルは 12 バイトしか使っていません。
  • ext:extent の番号(連続したブロックのまとまりを表す)。
  • logical_offset:ファイル内での論理ブロック番号。ここでは 0 番目の論理ブロックが割り当てられています。
  • physical_offset:ディスク上の物理ブロック番号。ここでは 1154091 番に配置されています。
  • length:extent が占めるブロック数。
  • expected:連続ブロックの期待位置(非連続の場合に差分が表示されます)。
  • flags:extent に関するフラグ。last は最後の extent、eof はファイル末尾を含むことを示します。

※ファイルのデータは必ずしもディスク上で連続して格納されるとは限りません。断片化(フラグメンテーション)が発生すると、extent が複数に分かれ、物理的に不連続なブロックに分散配置されることがあります。今回の例では extent は 1 つだけで、断片化は発生していません。

さいごに

本記事では、Linuxカーネルにおけるファイル操作の流れを、システムコールの挙動からファイルディスクリプタ、inode、データブロックの関係まで順を追って解説しました。普段意識せずに使っている open や read の裏側で、カーネルがどのようにリソースを管理しているのか理解することで、より深くLinuxの仕組みを捉えられるようになるでしょう。

コメント