4 分で読了 #linux-kernel #file-system
このページの目次

VFS 応用

カバー範囲: page cache と VFS の相互作用 → inode 状態マシン → ファイルロック (POSIX/OFD) → fsync/fdatasync の保証 → sendfile/splice/vmsplice によるゼロコピー カーネルバージョン: 2.6 ~ 6.x

Page Cache と VFS の相互作用

inode と address_space の関係

// inode->i_mapping は、そのファイルの page cache を指す
// これは address_space 構造体であり、メモリ内のそのファイルの全キャッシュページを管理する

// ほとんどの場合、inode->i_mapping は内蔵の inode->i_data を指す:
struct inode {
    struct address_space *i_mapping;  // 通常は &inode->i_data
    struct address_space i_data;      // 内蔵 (ブロックデバイスファイルシステム用)
};

// ブロックデバイスの裸の読み書きの場合:
struct block_device {
    struct inode bd_inode;            // ブロックデバイスも inode として扱う
    // bd_inode.i_mapping はブロックデバイス自身の page cache
    // ファイルシステムとのやり取りは buffer_head を介して行われる
};

inode 状態マシン

// include/linux/fs.h
#define I_NEW           0   // inode が新規割り当てされ、ディスクからまだ読み込まれていない
#define I_DIRTY_SYNC    1   // メタデータがダーティ、同期が必要 (例: i_size)
#define I_DIRTY_DATASYNC 2  // データがダーティ (fdatasync のみが関心を持つ)
#define I_DIRTY_PAGES   3   // i_mapping にダーティページが存在する
#define I_WILL_FREE     4   // 解放即将
#define I_FREEING       5   // 解放処理中

// inode のライフサイクル:
// alloc_inode()  → I_NEW
// insert_inode_locked() → inode ハッシュに追加
// unlock_new_inode() → I_NEW をクリアし、待機中のプロセスをウェイクアップ
// ... アクティブ期間 ...
// iput() → i_count == 0 → evict() → I_FREEING → destroy_inode()

ファイルロック

POSIX ロック (fcntl)

// POSIX 勧告的/強制ロック
struct flock {
    short l_type;       // F_RDLCK / F_WRLCK / F_UNLCK
    short l_whence;     // SEEK_SET/SEEK_CUR/SEEK_END
    off_t l_start;
    off_t l_len;
    pid_t l_pid;        // ロックを保持しているプロセスの PID (GETLK 時に設定)
};

fcntl(fd, F_SETLK, &lock);   // 非ブロッキング
fcntl(fd, F_SETLKW, &lock);  // ブロッキング待機
fcntl(fd, F_GETLK, &lock);   // 照会

// 実装: fs/locks.c
// ロックは inode->i_flctx (file_lock_context) にアタッチされる
// 競合検出: posix_locks_conflict() → conflict チェーンを走査
// プロセスが fd をクローズすると、フォーク関係に関係なく、すべての POSIX ロックが自動的に解放される

OFD ロック (Open File Description, 3.15 以降)

// POSIX ロックの問題点: 同一プロセス内の異なる fd は、それぞれロックを保持できない
// OFD ロックの解決策: ロックは「プロセス」ではなく「オープンファイル記述子 (file)」にアタッチされる
fcntl(fd, F_OFD_SETLK, &lock);

// fork シナリオ:
//   POSIX: 子プロセスは親プロセスのロックを継承する (同一 PID であるため)
//   OFD:   子プロセスはロックを継承しない (異なる file であるため、必ずしも継承されない)

FLOCK (BSD ロック)

flock(fd, LOCK_SH);  // 共有ロック
flock(fd, LOCK_EX);  // 排他ロック
flock(fd, LOCK_UN);  // ロック解除

// POSIX ロックとの違い:
//   FLOCK は file にアタッチされる (inode でもプロセスでもない)
//   同一 file に対する複数の flock 呼び出しは自動的に結合される
//   fork 時に flock が継承される (file が共有されるため)
//   勧告的ロックのみ (強制ロックは存在しない)

fsync / fdatasync: 永続性の保証

// fs/sync.c
SYSCALL_DEFINE1(fsync, unsigned int, fd)do_fsync(fd, 0)vfs_fsync(file, 0)
    ├─ file_write_and_wait_range()  // 1. ダーティな page cache の書き戻し
    │   └─ __filemap_fdatawrite_range()do_writepages()
    │       → 各ダーティな folio に対して a_ops->writepage() を呼び出し
    │   └─ __filemap_fdatawait_range()  // 書き戻しの完了を待機
    │
    └─ vfs_fsync_range()
        └─ f_op->fsync()  // 2. ファイルシステム: ジャナルの書き込み / 強制フラッシュ
            → ext4: jbd2 トランザクションのコミット + blkdev_issue_flush
            → XFS: 強制ログのディスクへの書き出し + flush
            → NFS: COMMIT 操作

// fdatasync (FDATASYNC): データ部分のみをフラッシュ (i_atime/i_mtime は対象外)
//   → vfs_fsync_range(file, start, end, 1)
//   → ファイルシステムはタイムスタンプの同期をスキップできる

ゼロコピー: sendfile / splice / vmsplice

// sendfile(): ファイルからソケットへ、ユーザー空間を経由しない
sendfile(out_fd, in_fd, &offset, count)do_sendfile()
    └─ splice_file_to_pipe() → パイプバッファ ← ファイル内容
        └─ splice_pipe_to_socket() → パイプバッファ → ソケット

// splice(): 2つの fd の間でパイプ経由の転送 (カーネル空間内)
splice(in_fd, NULL, pipefd[1], NULL, count, 0);  // fd → パイプ
splice(pipefd[0], NULL, out_fd, NULL, count, 0);  // パイプ → fd

// vmsplice(): ユーザー空間メモリからパイプへ (splice と組み合わせて使用)
vmsplice(pipefd[1], iov, nr_segs, SPLICE_F_GIFT);
  // ユーザーメモリのページが「カーネルに譲渡」される (もはやユーザー空間のものではない)
  // → パイプがこれらのページを参照 → splice でソケットへ転送 → ゼロコピーで NIC へ送信

DMA ゼロコピーの経路

ゼロコピー経路の比較: CPU コピー回数が 2 回から 0 回に減少

経路 A · 従来の読み書き ディスク page cache ユーザーバッファ ソケットバッファ NIC コンテキストスイッチ 4 回 · CPU コピー 2 回

経路 B · sendfile ディスク page cache ソケットバッファ NIC コンテキストスイッチ 2 回 · CPU コピー 1 回

経路 C · sendfile + DMA scatter-gather (推奨) ディスク page cache NIC (DMA が直接取得) コンテキストスイッチ 2 回 · CPU コピー 0 回!

コピー回数は減少し続ける: 従来 2 回 → sendfile 1 回 → sendfile+DMA 0 回; コンテキストスイッチは 4 → 2 → 2。 0 コピーには、NIC が TX チェックサムオフロードと SG (Scatter-Gather) をサポートしている必要がある。ほぼすべての現代的な NIC がこれを満たしている。

デバッグ

# inode キャッシュの統計
cat /proc/sys/fs/inode-nr      # nr_inodes, nr_free_inodes
cat /proc/sys/fs/inode-state   # 詳細な inode 使用状況

# プロセスのファイルロックの表示
cat /proc/locks                 # 現在アクティブなすべての POSIX/OFD/FLOCK
lslocks                         # より人間 readable な形式

# fsync の動作を追跡
strace -e trace=fsync,fdatasync -p <pid>

参考と拡張

  • カーネルドキュメント⁠: Documentation/filesystems/locks.rst, Documentation/filesystems/vfs.rst
  • LWN:
    • "Better file locking with OFD locks" (lwn.net/Articles/586904/)
    • "Splice and sendfile" (lwn.net/Articles/178199/)
  • ソースファイル⁠:
    • fs/locks.c — POSIX/OFD/FLOCK の実装
    • fs/sync.c — fsync/fdatasync
    • fs/splice.c — splice/sendfile/vmsplice
    • fs/inode.c — inode のライフサイクル

キーワード: VFS, inode state, POSIX lock, OFD lock, FLOCK, fsync, fdatasync, sendfile, splice, zero-copy