Linuxの「ファイルの基本と実装」を調査した
概要
普段エンジニアである私たちが操作している「ファイル」について、そもそもファイルとはどのようにして構成されてたものなのか意識することはありません。
私たちが普段見ているファイルの実態とは何なのか、なぜファイルを読み込んだり、書き込んだり、更には検索ができたりするのでしょうか?
ls
や stat
を実行したときファイルに付随する色んな情報が確認できます、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.txt
とhogepeer.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開いてみた
kernel内部のファイルの実装を調査すると dentry
, file
,inode
と合わせて3つの構造体が重要であることがわかりました。
file 構造体
/**
* 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_name
と d_inode
、それぞれがファイル名とinode番号となっているようです。
この組み合わせがセットされていることでファイル名及びinode番号を利用した検索が可能だったということですね。
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
にはファイルの具体的な情報が格納されていることがわかります。
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 の関係性
ユーザーから見たファイル
ここまでファイルの構造について記載していきましたが、実際アプリケーションを書く際にこの辺りは全てfd
を通して操作しているため私たちは裏側を意識しないでファイルの open
や read
, write
を行うことができています。
fd(ファイルディスクリプタ)
オープンしたファイルはプロセス毎に fd
を割り当てて管理されています。
現在プロセスで管理されている fd
は以下のpathで確認することができます
$ ls /proc/プロセスID/fd
#指定したプロセスIDが管理しているfd(file)が表示される
プロセスを表す task_struct 構造体
のメンバに files_struct 構造体
が格納されており、*files
を参照することでプロセスが管理しているファイルを知ることができるということです。
実際のところ fd
は file 構造体
のことを指しています。
結果的 dentry
や inode
に格納されているような情報にアクセスできるというわけです。
struct task_struct {
/** 省略 **/
/* Open file information: */
struct files_struct *files; //ここでプロセスがオープンしているファイルを管理している
};
/*
* 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を管理する
ここまでを俯瞰する
ファイルと呼ばれるものに対して様々な観点から観察してみました。
ここまで登場した内容をシンプルに表現すると以下のようなイメージになるかと思います。
cat
コマンドを実行したときファイルがどの用に探索され、fdが生成されるかを簡単に示してみました。
イメージまとめ
ファイルへの見方が少し変わってきたでしょうか?
補足資料
-
d_lookup
| 親dentryから引数で渡されたパスを探索する関数
-
struct address_space
| ファイル内容の実態
参考文献
他kernelのコードの解析のお供に ChatGPT
を活用しています。
Discussion