このページの目次

VFS — 仮想ファイルシステム層

カバー範囲: VFS オブジェクトモデル (super_block/inode/dentry/file) → システムコールパス (open/read/write/close) → file_operations → RCUパスウォーク → マウントメカニズム カーネルバージョン: 2.6 ~ 6.x

概要

VFS (Virtual File System / Virtual Filesystem Switch) は、Linuxにおける「すべてはファイルである」という理念の基盤です。VFSは抽象化されたオブジェクトと操作インターフェースのセットを定義しており、ext4、NFS、procfsなど全く異なるファイルシステムをユーザー空間に対して一貫して公開することを可能にします。read() システムコールを実行する際、それがローカルディスク上にあるのかネットワークマウントされているのかを意識する必要はありません。VFS層が正しいファイルシステム実装へとルーティングを行います。

VFSの核心的な設計哲学はオブジェクト指向のC言語実装です。各VFSオブジェクト内部には *_operations 関数テーブルが埋め込まれており、ファイルシステムはこの関数テーブルを埋めることで「インターフェースを実装」します。


VFSの4大オブジェクト

super_block:  「このファイルシステムは何なのか?」  — マウントポイントごとに1つのsuper_blockが存在
inode:        「このファイルは何か?」              — ファイル/ディレクトリごとに1つ(ディスクまたはメモリ上に存在)
dentry:       「このパス名は誰を指しているのか?」  — パス名からinodeへのキャッシュマッピング
file:         「この開かれたファイルはどこにあるのか?」 — open() ごとに1つ作成され、現在のオフセットを含む

super_block

// include/linux/fs.h
struct super_block {
    unsigned long           s_blocksize;
    unsigned char           s_blocksize_bits;
    unsigned long long      s_maxbytes;        // ファイルの最大サイズ
    struct file_system_type *s_type;            // ファイルシステムタイプ (ext4, nfs, ...)
    const struct super_operations *s_op;        // 操作関数テーブル
    struct dentry           *s_root;            // ルートディレクトリのdentry
    struct list_head        s_inodes;           // 全inodeのリスト
    struct list_head        s_dentry_lru;       // 未使用のdentry (LRU)
    void                    *s_fs_info;         // ファイルシステム固有のデータ
};

inode

struct inode {
    umode_t                 i_mode;             // ファイルタイプと権限
    kuid_t                  i_uid; i_gid;
    loff_t                  i_size;             // ファイルサイズ
    struct timespec64       i_atime, i_mtime, i_ctime;

    const struct inode_operations *i_op;        // inode操作
    const struct file_operations  *i_fop;       // デフォルトのファイル操作 (open時にコピーされるテンプレート)
    struct super_block      *i_sb;
    struct address_space    *i_mapping;         // page cache (重要!)

    union {
        struct pipe_inode_info  *i_pipe;        // パイプ
        struct block_device     *i_bdev;        // ブロックデバイス
        struct cdev             *i_cdev;        // キャラクタデバイス
    };

    unsigned long           i_ino;              // inode番号 (ファイルシステム内で一意)
};

dentry — パス名キャッシュ

struct dentry {
    struct inode            *d_inode;           // 関連付けられたinode (NULLの場合あり: ネガティブ)
    struct dentry           *d_parent;          // 親ディレクトリ
    struct qstr             d_name;             // このコンポーネントの名前
    struct list_head        d_child;            // 親ディレクトリ内の子ディレクトリリスト
    struct list_head        d_subdirs;          // 子ディレクトリリストのヘッダ
    struct hlist_bl_node    d_hash;             // dcacheハッシュテーブル
    const struct dentry_operations *d_op;
};

ネガティブdentry: d_inode == NULL。これは「このパス名は存在しないことが確認済み」であることを意味します。見つからないファイルを検索した際などに生成されます。open("/tmp/noexist") のたびに再探索を行わなければならなければ、膨大なIOの無駄になります。

file — 開かれたファイルの記述

struct file {
    struct path             f_path;             // dentry + vfsmount
    const struct file_operations *f_op;
    atomic_long_t           f_count;            // リファレンスカウント
    unsigned int            f_flags;            // O_RDONLY, O_NONBLOCK, ...
    fmode_t                 f_mode;             // FMODE_READ, FMODE_WRITE
    loff_t                  f_pos;              // 現在の読み書き位置
    struct address_space    *f_mapping;         // page cache
    void                    *private_data;      // ファイルシステム固有のデータ
};

システムコールパス

open(): パス名からfileへ

flowchart TD
    SYSCALL["open() システムコール"] --> GETNAME["getname()<br/>ユーザー空間からパス名をコピー"]

    GETNAME --> GETFD["get_unused_fd_flags()<br/>fd番号を割り当て"]

    GETFD --> DO_OPEN["do_filp_open()<br/>核心的なパス解析"]
    DO_OPEN --> PATH["path_openat()"]
    PATH --> WALK["link_path_walk()<br/>パスをコンポーネントごとに走査"]

    WALK --> LOOKUP{"lookup_fast()<br/>dcacheを参照 (RCU walk)"}
    LOOKUP -->|"ヒット"| DENTRY["dentryを返す<br/>ロックなし、極めて高速 ✅"]
    LOOKUP -->|"ミス"| SLOW["lookup_slow()<br/>i_op->lookup()を呼び出し<br/>ディスク/inodeブロックから探索<br/>新しいdentryを割り当て → dcacheに追加"]

    DENTRY --> LAST{"最終コンポーネントか?"}
    SLOW --> LAST

    LAST -->|"はい"| DO_LAST["do_last()"]
    LAST -->|"いいえ、次のコンポーネントへ"| LOOKUP

    DO_LAST --> CASE{"ファイルの状態は?"}
    CASE -->|"既に存在"| VFS_OPEN["vfs_open()<br/>→ f_op->open()"]
    CASE -->|"O_CREAT"| VFS_CREATE["vfs_create()<br/>→ i_op->create()"]
    CASE -->|"O_TMPFILE"| TMPFILE["無名一時ファイルを作成"]

    VFS_OPEN --> INSTALL["fd_install()<br/>fileをfdtableにインストール"]
    VFS_CREATE --> INSTALL
    TMPFILE --> INSTALL

    INSTALL --> RET["fdを返す ✅"]

    classDef sys call fill:#e3f2fd,stroke:#1565c0
    classDef step fill:#f3e5f5,stroke:#7b1fa2
    classDef decision fill:#fff3e0,stroke:#ef6c00
    classDef done fill:#e8f5e9,stroke:#2e7d32
    class SYSCALL syscall
    class GETNAME,GETFD,DO_OPEN,PATH,WALK,DENTRY,SLOW,VFS_OPEN,VFS_CREATE,TMPFILE,INSTALL step
    class LOOKUP,LAST,CASE decision
    class RET done

RCU Path Walk — VFSのパフォーマンスの秘密

従来のパス解析 (2.6 より前):
  各コンポーネントごとにロックを取得 → 競合が高くなる → マルチコアでのパフォーマンスが劣る

RCU path walk (2.6.38以降):
  1. RCU読取りロックの下でdcacheを走査
  2. dentryとinodeの読み取りのみが必要(書き込みは不要)
  3. すべてのコンポーネントがdcache内にある場合 → 全体でロック不要
  4. dcacheにないコンポーネントに出会った場合 → ref-walk(ロック付き)にフォールバック

パフォーマンス: マルチコアシステムにおいてopen()のスループットが数倍向上
実装: fs/namei.c の link_path_walk() 内に2つのロジック(RCU/REF)が存在

read(): page cacheからの読み込み

// fs/read_write.c
SYSCALL_DEFINE3(read, ...)ksys_read()vfs_read()

vfs_read(file, buf, count, pos)
  ├─ 権限チェック (FMODE_READ)
  ├─ f_op->read() または f_op->read_iter() を呼び出し
  │   └─ 通常ファイルの場合: generic_file_read_iter()
  │       └─ filemap_read()
  │           ├─ 各ページアラインされた範囲に対して:
  │           │   ├─ filemap_get_page()find_get_page()
  │           │   │   ├─ page cacheヒット → 直接返す (IOなし)
  │           │   │   └─ page cacheミス → page_cache_sync_readahead()
  │           │   │       └─ ブロック層から読み込み → ページをpage cacheに挿入
  │           │   └─ copy_page_to_iter()copy_to_user()
  │           └─ f_pos を更新
  └─ 読み込んだバイト数を返す

write(): page cacheへの書き込み + バックグラウンド書き戻し

vfs_write(file, buf, count, pos)
  └─ f_op->write_iter()generic_file_write_iter()
      └─ __generic_file_write_iter()
          ├─ filemap_write()
          │   ├─ 各ページアラインされた範囲に対して:
          │   │   ├─ filemap_get_page() → page cacheページを検索または作成
          │   │   ├─ copy_page_from_iter()copy_from_user()
          │   │   └─ ページをダーティとしてマーク (SetPageDirty)
          │   └─ f_pos を更新
          │
          └─ 書き戻しはいつ行われるか?
              ここではありません! generic_file_write_iter は単にpage cacheに書き込むだけです
              実際のwritebackはバックグラウンドのflusherスレッドによって非同期で行われます
              (または fsync/fdatasync によって同期的にトリガーされます)

file_operations

// include/linux/fs.h
struct file_operations {
    struct module *owner;
    loff_t (*llseek)(struct file *, loff_t, int);
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter)(struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
    int (*mmap)(struct file *, struct vm_area_struct *);
    unsigned long (*mmap_supported_flags);
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);  // close() → 最後の参照が解放される
    int (*fsync)(struct file *, loff_t, loff_t, int datasync);
    int (*fasync)(int, struct file *, int);
    __poll_t (*poll)(struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
};

ファイルシステムは一部のみを埋め込むことも可能です。VFSがデフォルトの実装で補完します。例えば read_iter はなく read がある場合、VFSが自動的にラップします。


マウントメカニズム

// fs/namespace.c
// mount は、ファイルシステムをVFSツリー上の一点に付着させます:

struct vfsmount {
    struct mount            *mnt_parent;   // マウントポイントの親
    struct dentry           *mnt_root;     // マウントされたルートディレクトリ
    struct super_block      *mnt_sb;       // ファイルシステム
    struct list_head        mnt_mounts;    // このマウント上にマウントされた子マウント
    struct list_head        mnt_child;     // 親マウントの子ノード
};

// mount /dev/sda1 /mnt の場合:
//   1. /dev/sda1 のスーパーブロックを読み取り → super_block を作成
//   2. vfsmount を作成 → mnt_root = super_block->s_root
//   3. /mnt にマウント → /mnt のdentry がマウントポイントとなる
//   4. パス走査中にマウントポイントに到達 → vfsmount->mnt_root に切り替えて継続

// namespace:
//   各プロセスはマウントネームスペースに属する
//   CLONE_NEWNS → 新しいマウントネームスペース(コンテナの基盤)

デバッグ

# VFS統計
cat /proc/sys/fs/dentry-state
# nr_dentry, nr_unused, age_limit

# 現在のマウント
cat /proc/mounts
findmnt  # ツリー形式で表示

# ファイルシステムタイプ
cat /proc/filesystems

# open/read/write のトレース
strace -e trace=open,read,write cat /etc/hostname

参考と拡張

  • カーネルドキュメント⁠: Documentation/filesystems/vfs.rst, Documentation/filesystems/path-lookup.rst
  • LWN:
    • "Pathname lookup in Linux" (lwn.net/Articles/649115/)
    • "RCU-walk: faster pathname lookup" (lwn.net/Articles/649294/)
  • ソースコードファイル⁠:
    • fs/namei.c — パスウォーク、open()
    • fs/open.c — do_sys_open()
    • fs/read_write.c — vfs_read/vfs_write
    • include/linux/fs.h — super_block, inode, dentry, file, file_operations
    • fs/namespace.c — マウントメカニズム
    • fs/dcache.c — dentryキャッシュ

キーワード: VFS, super_block, inode, dentry, file, file_operations, RCU path walk, dcache, negative dentry, mount namespace