---
title: VFS 进阶
url: https://doc.liz6.com/linux-kernel/03-file-system/04-vfs-advanced
locale: zh
area: linux-kernel
tags:
- linux-kernel
- 文件系统
date: 2026-06-30
modified: 2026-07-11
description: '覆盖: page cache 与 VFS 交互 → inode 状态机 → file locking (POSIX/OFD) → fsync/fdatasync 保证 → sendfile/splice/vmsplice 零拷贝 内核版本: 2.6 ~ 6.x'
---

# VFS 进阶

> 覆盖: page cache 与 VFS 交互 → inode 状态机 → file locking (POSIX/OFD) → fsync/fdatasync 保证 → sendfile/splice/vmsplice 零拷贝
> 内核版本: 2.6 ~ 6.x

## Page Cache 与 VFS 交互

### inode 与 address_space 的关系

```c
// 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;      // 内嵌 (block device 文件系统)
};

// 对于块设备的裸读写:
struct block_device {
    struct inode bd_inode;            // 块设备也当 inode 管
    // bd_inode.i_mapping 是块设备自己的 page cache
    // 与文件系统通过 buffer_head 交互
};
```

### inode 状态机

```c
// include/linux/fs.h
#define I_NEW           0   // inode 刚分配, 还未从磁盘读
#define I_DIRTY_SYNC    1   // 元数据 dirty, 需要同步 (如 i_size)
#define I_DIRTY_DATASYNC 2  // 数据 dirty (仅 fdatasync 关心)
#define I_DIRTY_PAGES   3   // i_mapping 中有 dirty 页面
#define I_WILL_FREE     4   // 即将释放
#define I_FREEING       5   // 正在释放中

// inode 生命周期:
// alloc_inode()  → I_NEW
// insert_inode_locked() → 加入 inode hash
// unlock_new_inode() → 清 I_NEW, 唤醒等待者
// ... 活跃期 ...
// iput() → i_count == 0 → evict() → I_FREEING → destroy_inode()
```

---

## File Locking

### POSIX Lock (fcntl)

```c
// POSIX advisory/强制锁
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 chain
// 进程关闭 fd → 自动释放所有 POSIX lock (无论 fork 关系)
```

### OFD Lock (Open File Description, 3.15+)

```c
// POSIX lock 的问题: 同一个进程的不同 fd 不能各自持锁
// OFD lock 解决: 锁挂在 "打开文件描述" (file) 上, 不是进程
fcntl(fd, F_OFD_SETLK, &lock);

// fork 场景:
//   POSIX: 子进程继承父进程的锁 (因为同一个 PID)
//   OFD:   子进程不继承 (不同 file, 不一定)
```

### FLOCK (BSD Lock)

```c
flock(fd, LOCK_SH);  // 共享
flock(fd, LOCK_EX);  // 排他
flock(fd, LOCK_UN);  // 解锁

// 与 POSIX lock 的区别:
//   FLOCK 挂在 file 上 (不是 inode, 不是进程)
//   同一个 file 的多次 flock 自动关联
//   fork 继承 flock (因为 file 共享)
//   仅 advisory (没有强制锁)
```

---

## fsync / fdatasync: 持久性保证

```c
// fs/sync.c
SYSCALL_DEFINE1(fsync, unsigned int, fd)
  → do_fsync(fd, 0) → vfs_fsync(file, 0)
    ├─ file_write_and_wait_range()  // 1. 回写 dirty page cache
    │   └─ __filemap_fdatawrite_range() → do_writepages()
    │       → 每个 dirty folio → a_ops->writepage()
    │   └─ __filemap_fdatawait_range()  // 等回写完成
    │
    └─ vfs_fsync_range()
        └─ f_op->fsync()  // 2. 文件系统: 写 journal / 强制 flush
            → 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

```c
// sendfile(): 文件 → socket, 不经过用户空间
sendfile(out_fd, in_fd, &offset, count)
  → do_sendfile()
    └─ splice_file_to_pipe() → pipe buffer ← 文件内容
        └─ splice_pipe_to_socket() → pipe buffer → socket

// splice(): 两个 fd 之间管道传输 (内核空间)
splice(in_fd, NULL, pipefd[1], NULL, count, 0);  // fd → pipe
splice(pipefd[0], NULL, out_fd, NULL, count, 0);  // pipe → fd

// vmsplice(): 用户空间内存 → pipe (与 splice 配合)
vmsplice(pipefd[1], iov, nr_segs, SPLICE_F_GIFT);
  // 用户内存的页被"送给"内核 (不再是用户态的了)
  // → pipe 引用这些页 → splice 到 socket → 零拷贝发网卡
```

### DMA 零拷贝路线

<svg viewBox="0 0 720 380" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="零拷贝路径对比:传统读写、sendfile、sendfile+DMA scatter-gather 的 CPU 拷贝次数递减">
  <defs><marker id="zcah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="380" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="18" font-weight="700" fill="#1f2933">零拷贝路径对比:CPU 拷贝次数从 2 次降到 0 次</text>

  <text x="40" y="54" font-size="12" font-weight="700" fill="#475569">路径 A · 传统读写</text>
  <rect x="90" y="68" width="54" height="32" rx="6" fill="#e2e8f0"/>
  <text x="117" y="88" text-anchor="middle" font-size="11" fill="#334155">磁盘</text>
  <line x1="144" y1="84" x2="160" y2="84" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="160" y="68" width="90" height="32" rx="6" fill="#e2e8f0"/>
  <text x="205" y="88" text-anchor="middle" font-size="11" fill="#334155">page cache</text>
  <line x1="250" y1="84" x2="266" y2="84" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="266" y="68" width="94" height="32" rx="6" fill="#e2e8f0"/>
  <text x="313" y="88" text-anchor="middle" font-size="11" fill="#334155">用户缓冲区</text>
  <line x1="360" y1="84" x2="376" y2="84" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="376" y="68" width="104" height="32" rx="6" fill="#e2e8f0"/>
  <text x="428" y="88" text-anchor="middle" font-size="11" fill="#334155">socket buffer</text>
  <line x1="480" y1="84" x2="496" y2="84" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="496" y="68" width="54" height="32" rx="6" fill="#e2e8f0"/>
  <text x="523" y="88" text-anchor="middle" font-size="11" fill="#334155">网卡</text>
  <text x="90" y="118" font-size="11" fill="#64748b">4 次上下文切换 · 2 次 CPU 拷贝</text>

  <text x="40" y="146" font-size="12" font-weight="700" fill="#3730a3">路径 B · sendfile</text>
  <rect x="90" y="160" width="54" height="32" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="117" y="180" text-anchor="middle" font-size="11" fill="#3730a3">磁盘</text>
  <line x1="144" y1="176" x2="160" y2="176" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="160" y="160" width="90" height="32" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="205" y="180" text-anchor="middle" font-size="11" fill="#3730a3">page cache</text>
  <line x1="250" y1="176" x2="266" y2="176" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="266" y="160" width="104" height="32" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="318" y="180" text-anchor="middle" font-size="11" fill="#3730a3">socket buffer</text>
  <line x1="370" y1="176" x2="386" y2="176" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="386" y="160" width="54" height="32" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="413" y="180" text-anchor="middle" font-size="11" fill="#3730a3">网卡</text>
  <text x="90" y="210" font-size="11" fill="#4f46e5">2 次上下文切换 · 1 次 CPU 拷贝</text>

  <text x="40" y="238" font-size="12" font-weight="700" fill="#0f766e">路径 C · sendfile + DMA scatter-gather(推荐)</text>
  <rect x="90" y="252" width="54" height="32" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="117" y="272" text-anchor="middle" font-size="11" fill="#0f766e">磁盘</text>
  <line x1="144" y1="268" x2="160" y2="268" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="160" y="252" width="90" height="32" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="205" y="272" text-anchor="middle" font-size="11" fill="#0f766e">page cache</text>
  <line x1="250" y1="268" x2="266" y2="268" stroke="#475569" stroke-width="1.6" marker-end="url(#zcah)"/>
  <rect x="266" y="252" width="160" height="32" rx="6" fill="#0d9488"/>
  <text x="346" y="272" text-anchor="middle" font-size="11" font-weight="700" fill="#ffffff">网卡(DMA 直接取)</text>
  <text x="90" y="302" font-size="11" font-weight="700" fill="#0f766e">2 次上下文切换 · 0 次 CPU 拷贝!</text>

  <rect x="60" y="318" width="600" height="42" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="76" y="336" font-size="12.5" fill="#115e59">拷贝次数一路走低:传统 2 次 → sendfile 1 次 → sendfile+DMA 0 次;上下文切换 4 → 2 → 2。</text>
  <text x="76" y="353" font-size="12.5" fill="#115e59">0 拷贝依赖网卡支持 TX checksum offload + SG——几乎所有现代网卡都满足。</text>
</svg>

---

## 调试

```bash
# inode cache 统计
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                         # 更好的人类可读格式

# 追踪 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*
