📁

Linuxの「ファイルの基本と実装」を調査した

に公開

概要

普段エンジニアである私たちが操作している「ファイル」について、そもそもファイルとはどのようにして構成されてたものなのか意識することはありません。

私たちが普段見ているファイルの実態とは何なのか、なぜファイルを読み込んだり、書き込んだり、更には検索ができたりするのでしょうか?

lsstat を実行したときファイルに付随する色んな情報が確認できます、find を使えばファイル名による絞り込みだけでなくファイルの最終更新日時を基準とした柔軟な検索もできます。
そもそも誰が、あるいはどこでその情報を握っているのでしょうか?

それらを成り立たせているLinuxのファイルの実装を探索した記事です。

対象読者

  • ファイルの実装をなんとなく知りたい人
  • ファイルがどの用に管理されているのか疑問を感じたことがある人
  • Linuxおよびそれに準じた環境で作業する人

注意

  • C言語やLinuxカーネルのコードが出てきますが、C言語の知識はなくて大丈夫です
  • ファイルシステムまでとの関係性は詳しく言及しておりません
  • カーネルの解読が正確ではない可能性があります(致命的な内容はコメントで教えて下さい🙇)
  • 参照先にカーネル公式を添付していますが、Githubのミラーリポジトリのこちらの方が見やすいです

検証環境

$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
ID="amzn"
ID_LIKE="fedora"
VERSION_ID="2023"
PLATFORM_ID="platform:al2023"
PRETTY_NAME="Amazon Linux 2023.6.20250128"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
HOME_URL="https://aws.amazon.com/linux/amazon-linux-2023/"
DOCUMENTATION_URL="https://docs.aws.amazon.com/linux/"
SUPPORT_URL="https://aws.amazon.com/premiumsupport/"
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
VENDOR_NAME="AWS"
VENDOR_URL="https://aws.amazon.com/"
SUPPORT_END="2029-06-30"

「すべてがファイルである」

Linux上ではすべてがファイルとして扱われます。
ファイル、ディレクトリ、ソケット通信、デバイス、プロセスなど何でもファイルとして扱われるのがLinuxの特徴です。
我々が普段ファイルという言葉を使って指すテキストファイルや画像ファイルに加えて、/proc/dev にはプロセスやカーネルの状態、デバイスとのやりとりをするインターフェースも「ファイル」の形式で存在していることが確認できます。

$ ls /proc
#たくさん出力される

$ ls /dev
#たくさん出力される

すべてをファイルとして扱うLinuxの思想が強く感じ取ることができますね。

ファイルの正体

普段ファイルを操作するときは大概「ファイル名」を使って操作しますね。

#hoge.txt の中から"Hello World" を探す
$ grep "Hello World" ./hoge.txt

#hoge.txt を fuga.txt へリネームする
$ mv ./hoge.txt ./fuga.txt

これはとても分かりやすく、今何のファイルを操作しているのか明らかです。
しかし、Linuxのファイルにとって「ファイル名」は「ファイル」と直接関連がないのです。

inode(アイノード)

Linuxはファイル名ではなく「inode(アイノード)」で管理しています。
inode は番号、つまり数値です。
カーネルが inode とファイル名の対応を知っているため、私たちはファイル名を利用してファイルへアクセスできています。

#適当にファイルを作成する
$ touch hoge.txt

#iオプションでinode番号も出力する
$ ls -i hoge.txt
10371333 hoge.tx

コンソールに10371333という番号が表示されました。
この数値がhoge.txtのinode番号です。
そしてinode番号とファイル名の対応を(10371333 hoge.txt)をリンクと呼びます。

あえてinode番号を使うことはありませんが、inode番号とファイルの結びつきが管理されているということは、inode番号でもファイルへアクセスすることが可能ということです。
例えば find コマンドにはinode番号を使って検索するオプションが存在します。

$ find ./ -inum 10371333
./hoge.txt

ファイル名を使わずとも検索できることが明らかになりました。

stat コマンドでファイルの詳細情報を確認することができます。
inode番号に加えてファイルの変更日付やアクセス権などのメタデータを参照することができます。
先ほど作成したhoge.txtに対して stat コマンドを実行すると以下のようなファイルに関するメタデータが出力されました。

$ stat hoge.txt
  File: hoge.txt
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 10371333    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/ec2-user)   Gid: ( 1000/ec2-user)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2025-03-31 23:33:42.865511048 +0900
Modify: 2025-03-31 23:33:42.865511048 +0900
Change: 2025-03-31 23:33:42.865511048 +0900
 Birth: 2025-03-31 23:33:42.865511048 +0900

ハードリンク

inode番号は一見ファイル名に対してユニークな番号に見えますが、別のファイルから同じinode番号をリンクしても問題ありません。
hoge.txt のハードリンクとなるhogepeer.txt を作成して双子ちゃんを用意してみます。

#オプションをつけなければハードリンクを作成します
$ ln hoge.txt hogepeer.txt

#同じinode番号を指す hogepeer.txt ファイルが作成されます
$ ls -i .
10371333 hoge.txt
10371333 hogepeer.txt

#`Links`が`2`となっています
$ stat hoge.txt
  File: hoge.txt
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 10371333    Links: 2
Access: (0644/-rw-r--r--)  Uid: ( 1000/ec2-user)   Gid: ( 1000/ec2-user)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2025-03-31 23:33:42.865511048 +0900
Modify: 2025-03-31 23:33:42.865511048 +0900
Change: 2025-04-01 01:23:17.974012192 +0900
 Birth: 2025-03-31 23:33:42.865511048 +0900

#同様に`Links`が`2`となっています
$ stat hogepeer.txt
  File: hogepeer.txt
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 10371333    Links: 2
Access: (0644/-rw-r--r--)  Uid: ( 1000/ec2-user)   Gid: ( 1000/ec2-user)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2025-03-31 23:33:42.865511048 +0900
Modify: 2025-03-31 23:33:42.865511048 +0900
Change: 2025-04-01 01:23:17.974012192 +0900
 Birth: 2025-03-31 23:33:42.865511048 +0900

inodeのメタ情報にはリンクカウント(Links: <番号>)と呼ばれるメタデータがあります。
基本は1ですが、ハードリンクをした結果inode番号10371333と関連付けられたファイルがhoge.txthogepeer.txtの2つとなったため、カウントが増えてLinks: 2となりました。

ハードリンクはファイルシステムをまたがることはできません。
試しに別のファイルシステムに対してハードリンクができないことをチェックしてみます。

#`df`コマンドでファイルシステムの一覧を確認
$ df --human-readable
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        4.0M     0  4.0M   0% /dev
tmpfs           475M     0  475M   0% /dev/shm
tmpfs           190M  448K  190M   1% /run
/dev/xvda1      8.0G  4.5G  3.5G  56% /
tmpfs           475M     0  475M   0% /tmp
/dev/xvda128     10M  1.3M  8.7M  13% /boot/efi
tmpfs            95M     0   95M   0% /run/user/1000

#hoge.txt の所属するファイルシステムを確認
$ df ./hoge.txt
Filesystem     1K-blocks    Used Available Use% Mounted on
/dev/xvda1       8310764 4648296   3662468  56% /

#tmpfs(/tmp) にハードリンクを作る
$ ln ./hoge.txt /tmp/hogetmp.txt
ln: failed to create hard link '/tmp/hogetmp.txt' => './hoge.txt': Invalid cross-device link

期待通り別のファイルシステムにはハードリンクを作成できないことがわかりました。

また同じ inode を共有しているため、元のファイルを削除してもリンクカウント減少するだけで、元のデータは残ります。

# 適当にファイルを作成する
$ touch moto_no_file.txt

# 適当な内容を書き込み
$ echo "Hello World" >>moto_no_file.txt

$ cat moto_no_file.txt
Hello World

# ハードリンクを貼る
$ ln moto_no_file.txt hardlink_shita_file.txt

$ cat hardlink_shita_file.txt
Hello World

# inode を確認する
$ ls -i | grep -e moto -e hard
10540846 hardlink_shita_file.txt
10540846 moto_no_file.txt

# 同じinodeを共有しているためリンクカウントが2
$ stat --printf="%N (Links: %h) \n" moto_no_file.txt hardlink_shita_file.txt
'moto_no_file.txt' (Links: 2)
'hardlink_shita_file.txt' (Links: 2)

# 元のファイルを削除してみる
$  rm moto_no_file.txt

# リンクカウントが `-1` しただけ
$ stat --printf="%N (Links: %h) \n" moto_no_file.txt hardlink_shita_file.txt
stat: cannot statx 'moto_no_file.txt': No such file or directory
'hardlink_shita_file.txt' (Links: 1)

# 元のデータも残ったまま
$ cat hardlink_shita_file.txt
Hello World

シンボリックリンク

ハードリンクより、シンボリックリンクの方が馴染みがある方が多いかと思います(dotfilesの管理など)。
リンクを作っても同じinode番号は指しません。参照先へのパスが保存された特別なファイルのようなものです。
また、異なるファイルシステムにまたがって作成する事が可能です。
ただし、ハードリンクと異なり元のファイルを削除したらリンクが無効になるため要注意です。

$ touch piyo.txt
#同じファイルシステムに作成する
$ ln -s piyo.txt piyopeer.txt
$ ls -i
10371333 hoge.txt  10371333 hogepeer.txt  10540844 piyo.txt  10540847 piyopeer.txt

# piyo.txt へのパスが保存されていることを確認
$ readlink piyopeer.txt
piyo.txt

# 別のファイルシステムに作成する
$ ln -s piyo.txt /tmp/piyotmp.txt

# 別のinode番号が割り振られる
$ ls -i /tmp/piyotmp.txt
23997 /tmp/piyotmp.txt

$ stat --printf="%N (Links: %h) \n" ./piyo.txt /tmp/piyotmp.txt
'./piyo.txt' (Links: 1)
'/tmp/piyotmp.txt' -> 'piyo.txt' (Links: 1)

# 元のファイルを削除する
$ rm piyo.txt

# パスは保存されている
$ readlink /tmp/piyopeer.txt
piyo.txt

# 元のファイルが存在しないため、openするタイミングでパス解決できずに無効なリンクとなる
$ cat /tmp/piyotmp.txt
cat: /tmp/piyotmp.txt: No such file or directory cat /tmp/piyopeer.txt
$ cat piyopeer.txt
cat: piyopeer.txt: No such file or directory

kernelから見たファイル

私たちから見たファイルというのは 📁 や、ファイラーに表示されるものたちだと思います。

vscodeより
vscode開いてみた

kernel内部のファイルの実装を調査すると dentry, file,inode と合わせて3つの構造体が重要であることがわかりました。

file 構造体

https://web.git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fs.h?h=v6.13.9#n1035

/**
 * struct file - Represents a file
 * @f_ref: reference count
 * @f_lock: Protects f_ep, f_flags. Must not be taken from IRQ context.
 * @f_mode: FMODE_* flags often used in hotpaths
 * @f_op: file operations
 * @f_mapping: Contents of a cacheable, mappable object.
 * @private_data: filesystem or driver specific data
 * @f_inode: cached inode
 * @f_flags: file flags
 * @f_iocb_flags: iocb flags
 * @f_cred: stashed credentials of creator/opener
 * @f_path: path of the file
 * @f_pos_lock: lock protecting file position
 * @f_pipe: specific to pipes
 * @f_pos: file position
 * @f_security: LSM security context of this file
 * @f_owner: file owner
 * @f_wb_err: writeback error
 * @f_sb_err: per sb writeback errors
 * @f_ep: link of all epoll hooks for this file
 * @f_task_work: task work entry point
 * @f_llist: work queue entrypoint
 * @f_ra: file's readahead state
 * @f_freeptr: Pointer used by SLAB_TYPESAFE_BY_RCU file cache (don't touch.)
 */
struct file {
	file_ref_t			f_ref;
	spinlock_t			f_lock;
	fmode_t				f_mode;
	const struct file_operations	*f_op;
	struct address_space		*f_mapping;
	void				*private_data;
	struct inode			*f_inode;
	unsigned int			f_flags;
	unsigned int			f_iocb_flags;
	const struct cred		*f_cred;
	/* --- cacheline 1 boundary (64 bytes) --- */
	struct path			f_path;
	union {
		/* regular files (with FMODE_ATOMIC_POS) and directories */
		struct mutex		f_pos_lock;
		/* pipes */
		u64			f_pipe;
	};
	loff_t				f_pos;
#ifdef CONFIG_SECURITY
	void				*f_security;
#endif
	/* --- cacheline 2 boundary (128 bytes) --- */
	struct fown_struct		*f_owner;
	errseq_t			f_wb_err;
	errseq_t			f_sb_err;
#ifdef CONFIG_EPOLL
	struct hlist_head		*f_ep;
#endif
	union {
		struct callback_head	f_task_work;
		struct llist_node	f_llist;
		struct file_ra_state	f_ra;
		freeptr_t		f_freeptr;
	};
	/* --- cacheline 3 boundary (192 bytes) --- */
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

冒頭inode番号とファイルの結びつきをkernelが管理しているという話をしました。
そのファイルを構成するうちの1つはfileという構造で定義されています。
ファイルパスや現在のファイルポジション、ファイルが何のmodeで操作されているかなどすべてをファイルとして扱うための情報が定義されているように見えます。
その代わり具体的なファイルの種類(ファイル、ディレクトリ、ソケット、キャラクタデバイスなど)が分かるようなメンバは定義されておりません。
f_path から dentry を取得することによってファイルのメタデータを知ることができるようになります。
file 構造体を作成する際にはopen 関数が呼ばれるため、その関数内で dentry をセットしていることが確認できます。

/**
 * vfs_open - open the file at the given path
 * @path: path to open
 * @file: newly allocated file with f_flag initialized
 */
int vfs_open(const struct path *path, struct file *file)
{
	int ret;

	file->f_path = *path;
	ret = do_dentry_open(file, NULL);
	if (!ret) {
		/*
		 * Once we return a file with FMODE_OPENED, __fput() will call
		 * fsnotify_close(), so we need fsnotify_open() here for
		 * symmetry.
		 */
		fsnotify_open(file);
	}
	return ret;
}

dentry 構造体

dentry はパス解決を行い inode を特定する役割を持っています。
1度検索したパスはキャッシュされるようになっており、2回目以降のアクセスは高速に行うことができるようになっています。
dentry はディレクトリエントリであることは勿論、親の dentry (d_parent) や子の dentry (d_children) を管理しており、この構造体のおかげでいわゆるディレクトリ構造(<親dentry>/<子dentry>) を表現することができるようになっています。
親と子のdentryを知っているということは以下のls -ai を実行したときに表示される..(dot-dot,親ディレクトリ)を出力し、inode番号を知ることができることにも納得がいきます。

$ ls -ai .
 8535923 .   8537050 ..  10371333 hoge.txt  10371333 hogepeer.txt  10540844 piyo.txt  10540847 piyopeer.txt

メンバに注目すると d_named_inode 、それぞれがファイル名とinode番号となっているようです。
この組み合わせがセットされていることでファイル名及びinode番号を利用した検索が可能だったということですね。

https://web.git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/dcache.h?h=v6.13.9#n82

struct dentry {
	/* RCU lookup touched fields */
	unsigned int d_flags;		/* protected by d_lock */
	seqcount_spinlock_t d_seq;	/* per dentry seqlock */
	struct hlist_bl_node d_hash;	/* lookup hash list */
	struct dentry *d_parent;	/* parent directory */
	struct qstr d_name;
	struct inode *d_inode;		/* Where the name belongs to - NULL is
					 * negative */
	unsigned char d_iname[DNAME_INLINE_LEN];	/* small names */
	/* --- cacheline 1 boundary (64 bytes) was 32 bytes ago --- */

	/* Ref lookup also touches following */
	const struct dentry_operations *d_op;
	struct super_block *d_sb;	/* The root of the dentry tree */
	unsigned long d_time;		/* used by d_revalidate */
	void *d_fsdata;			/* fs-specific data */
	/* --- cacheline 2 boundary (128 bytes) --- */
	struct lockref d_lockref;	/* per-dentry lock and refcount
					 * keep separate from RCU lookup area if
					 * possible!
					 */

	union {
		struct list_head d_lru;		/* LRU list */
		wait_queue_head_t *d_wait;	/* in-lookup ones only */
	};
	struct hlist_node d_sib;	/* child of parent list */
	struct hlist_head d_children;	/* our children */
	/*
	 * d_alias and d_rcu can share memory
	 */
	union {
		struct hlist_node d_alias;	/* inode alias list */
		struct hlist_bl_node d_in_lookup_hash;	/* only for in-lookup ones */
	 	struct rcu_head d_rcu;
	} d_u;
};

inode 構造体

inode にはファイルの具体的な情報が格納されていることがわかります。

https://web.git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fs.h?h=v6.13.9#n635

struct inode {
	umode_t			i_mode;
	unsigned short		i_opflags;
	kuid_t			i_uid;
	kgid_t			i_gid;
	unsigned int		i_flags;
	//長いので省略
	struct address_space *i_mapping;
} __randomize_layout;

メンバを確認すると以下の内容を保存しているのが分かります。

  • ファイルの種類
  • ファイルのオーナー
  • ファイルのサイズ
  • ファイルの作成日時
  • ファイルの更新日時
  • ファイル内容へのポインタ
  • ...etc

私たちが参照したいファイルの情報はまさに inode が管理していたということですね。

例えば i_mode はファイルの種類を表す役割をしています。
/usr/include/linux/stat.h にファイルの種類の定義があります。

//stat.h へのpath
/** $ less /usr/include/linux/stat.h **/

/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _LINUX_STAT_H
#define _LINUX_STAT_H

#include <linux/types.h>

#if defined(__KERNEL__) || !defined(__GLIBC__) || (__GLIBC__ < 2)

#define S_IFMT  00170000
#define S_IFSOCK 0140000
#define S_IFLNK  0120000
#define S_IFREG  0100000
#define S_IFBLK  0060000
#define S_IFDIR  0040000
#define S_IFCHR  0020000
#define S_IFIFO  0010000
#define S_ISUID  0004000
#define S_ISGID  0002000
#define S_ISVTX  0001000

//ファイルの種類の判定
#define S_ISLNK(m)      (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m)      (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)
#define S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m)      (((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m)      (((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m)     (((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m)     (((m) & S_IFMT) == S_IFSOCK)

S_ISREG(inode->i_mode)S_ISDIR(inode->i_mode) とすることでファイルの種類が分かるようになっているということですね。

イメージ図

これら3つの関係性をもう少し理解しやすいように可視化してみました。

file-dentry-inode の関係性
file-dentry-inode の関係性

ユーザーから見たファイル

ここまでファイルの構造について記載していきましたが、実際アプリケーションを書く際にこの辺りは全てfdを通して操作しているため私たちは裏側を意識しないでファイルの openread, write を行うことができています。

fd(ファイルディスクリプタ)

オープンしたファイルはプロセス毎に fd を割り当てて管理されています。
現在プロセスで管理されている fd は以下のpathで確認することができます

$ ls /proc/プロセスID/fd
#指定したプロセスIDが管理しているfd(file)が表示される

プロセスを表す task_struct 構造体 のメンバに files_struct 構造体 が格納されており、*files を参照することでプロセスが管理しているファイルを知ることができるということです。
実際のところ fdfile 構造体 のことを指しています。
結果的 dentryinode に格納されているような情報にアクセスできるというわけです。

https://web.git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/sched.h?h=v6.13.9

struct task_struct {
	/** 省略 **/
	/* Open file information: */
	struct files_struct		*files; //ここでプロセスがオープンしているファイルを管理している
};

https://web.git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fdtable.h?h=v6.13.9

/*
 * The default fd array needs to be at least BITS_PER_LONG,
 * as this is the granularity returned by copy_fdset().
 */
#define NR_OPEN_DEFAULT BITS_PER_LONG

struct fdtable {
	/** 省略 **/
	//コメント通り現在のファイルディスクリプタ(file 構造体)がここ
	struct file __rcu **fd;      /* current fd array */
	/** 省略 **/
};

//files_struct のメンバにfdtableがある
/*
 * Open file table structure
 */
struct files_struct {
	/** 省略 **/
	struct fdtable __rcu *fdt;
	/** 省略 **/
};

イメージ図

プロセスとfdの関係性をもう少し分かりやすいように可視化してみました。
プロセスがオープンしたfdを管理する
プロセスがオープンしたfdを管理する

ここまでを俯瞰する

ファイルと呼ばれるものに対して様々な観点から観察してみました。
ここまで登場した内容をシンプルに表現すると以下のようなイメージになるかと思います。
cat コマンドを実行したときファイルがどの用に探索され、fdが生成されるかを簡単に示してみました。

まとめ
イメージまとめ

ファイルへの見方が少し変わってきたでしょうか?

補足資料

  • d_lookup | 親dentryから引数で渡されたパスを探索する関数

https://web.git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/dcache.c?h=v6.13.9#n2273

  • struct address_space | ファイル内容の実態

https://web.git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fs.h?h=v6.13.9#n467

参考文献

https://www.kernel.org/

https://www.oreilly.co.jp/books/9784873113623/

他kernelのコードの解析のお供に ChatGPT を活用しています。

GitHubで編集を提案

Discussion