VFS:一個介面對接所有檔案系統#

Linux 支援上百種檔案系統:ext4、xfs、btrfs、F2FS、NTFS、FAT、NFS、procfs、sysfs、tmpfs、overlayfs… 但 userspace 只看到統一的 open/read/write/close。中間的抽象層叫 VFS(Virtual File System)

        userspace
            │ open/read/write
            ▼
        ┌───────────────┐
        │     VFS       │   ◄── 抽象介面
        └──┬───┬───┬────┘
           │   │   │
        ext4 xfs btrfs … 各檔案系統實作(plugin)
           │   │   │
        ┌──▼───▼───▼──┐
        │ block layer │
        └──────┬──────┘
               ▼
            磁碟

VFS 不是「另一個檔案系統」,它是規定「檔案系統長什麼樣」的介面。

四大物件#

VFS 抽象核心是四個物件:

物件含義
superblock一個已掛載檔案系統的整體資訊
inode一個檔案/目錄/裝置節點 ── 內容元資料(不含名字)
dentry路徑名 ➡️ inode 的對應快取
file一個被打開的檔案實例(fd 對應的東西)

關係:

struct file ──► struct dentry ──► struct inode ──► struct super_block
   每次 open                共享                       共享
   都新一個                  名字 ↔ inode               檔案系統元資料

範例:兩個 process 都 open("/tmp/a.txt") ── 兩個 struct file、共用一個 struct dentry、共用一個 struct inode、共用一個 struct super_block(tmpfs)。

inode#

struct inode {
    umode_t            i_mode;       // 檔案類型 + 權限
    kuid_t             i_uid;
    kgid_t             i_gid;
    loff_t             i_size;
    struct timespec64  i_atime, i_mtime, i_ctime;
    blkcnt_t           i_blocks;
    unsigned long      i_ino;        // inode number
    const struct inode_operations *i_op;
    const struct file_operations  *i_fop;
    struct super_block *i_sb;
    struct address_space *i_mapping; // page cache 對應
    // ...
};

inode 不含名字!名字屬於目錄條目(dentry)。一個 inode 可以有多個 hard link(多個名字),所有名字共用同一 inode。

dentry#

struct dentry {
    struct dentry *d_parent;
    struct qstr    d_name;          // 短名(這一段)
    struct inode  *d_inode;
    struct list_head d_subdirs;     // 子節點
    // ...
};

dentry 是路徑名快取,不寫到磁碟。當你 open("/usr/bin/ls"),VFS 走訪 dentry tree:/ ➡️ usr ➡️ bin ➡️ ls,每一段都查或建一個 dentry,最終到達對應 inode。

dentry cache 巨大 ── 它讓「重複存取相同路徑」幾乎零成本(不用每次重走檔案系統)。slabtopdentry 是常駐前幾名。

file#

struct file {
    struct path        f_path;       // 含 dentry
    struct inode      *f_inode;
    fmode_t            f_mode;       // 讀/寫/append/...
    loff_t             f_pos;        // 當前位移
    const struct file_operations *f_op;
    void              *private_data;
    // ...
};

每次 open 都新建一個 file,計入行程 fd table。dup/fork 時是共享同一個 file(包括 f_pos!)── 所以 fork 後父子都寫同一 fd 會搶 f_pos

superblock#

struct super_block {
    struct list_head s_list;         // 所有 sb 鏈
    dev_t            s_dev;
    unsigned long    s_blocksize;
    struct file_system_type *s_type;
    const struct super_operations *s_op;
    struct dentry   *s_root;         // 該檔案系統的根 dentry
    void            *s_fs_info;      // 私有資料
    // ...
};

每個 mount 點一個 superblock。

註冊一個檔案系統#

static struct file_system_type my_fs_type = {
    .owner       = THIS_MODULE,
    .name        = "myfs",
    .mount       = myfs_mount,
    .kill_sb     = kill_litter_super,
    .fs_flags    = 0,
};

static int __init myfs_init(void) {
    return register_filesystem(&my_fs_type);
}

static void __exit myfs_exit(void) {
    unregister_filesystem(&my_fs_type);
}

註冊後,userspace 可 mount -t myfs ...

檔案系統的種類#

類別例子特色
磁碟檔案系統ext4、xfs、btrfs、f2fs、ntfs真有磁碟介質
網路檔案系統NFS、CIFS、SMB、CephFS、Lustre跨網路
偽檔案系統procfs、sysfs、debugfs、tmpfs不對應磁碟,內容是動態生成或記憶體
Pseudo blockloop、device-mapper在區塊層之上做轉換
堆疊式overlayfs、unionfs多個檔案系統合成一層
特殊用途nfs4、fuse、ecryptfs特殊目的

檔案系統的磁碟結構(以 ext4 為例)#

+---------+---------------+---------+----------+---------+
| Super   | Group desc    | Block   | Inode    | Data    |
| block   | table         | bitmap  | bitmap   | blocks  |
|         |               | + inode |          |         |
|         |               | table   |          |         |
+---------+---------------+---------+----------+---------+
  • Superblock:檔案系統整體 metadata(block size、總 block 數、mount count、UUID)
  • Group descriptor:每個 block group 的描述
  • Bitmap:用來追蹤哪些 block / inode 被使用
  • Inode table:inode 陣列
  • Data blocks:實際檔案內容

ext4 引入 extents(延伸區)取代 ext2/3 的 indirect block,大檔效能大幅提升:

傳統 indirect block:
 inode → [block1, block2, block3, ..., indirect → [b101, b102, ...]]

extent:
 inode → [(start=100, count=50), (start=200, count=30)]

一次 read 的全程#

read(fd, buf, count)
  │
  ▼
sys_read → vfs_read → fop->read(檔案系統的 read,例如 ext4_file_read_iter)
  │
  ▼
generic_file_read_iter(多數檔案系統用這個通用實作)
  │
  ▼
page cache 查詢
  ├── 有:copy_page_to_iter → user buffer
  └── 無:mpage_readahead → filesystem 提供 get_block → 提交 bio
       │
       ▼
block layer
  │
  ▼
device driver → 硬體

關鍵抽象:所有檔案 I/O 都經過 page cache(除 O_DIRECT)。

Page cache 與 writeback#

寫入流程:

  1. write() 把資料寫進 page cache 對應頁,標記 dirty
  2. write() 立刻回傳成功
  3. 背景 thread(pdflush 老名、現在叫 flush-N:m workqueue)定期刷回磁碟
  4. 或 dirty 比例超過閾值 ➡️ 行程自己被 throttle 直到刷完

兩個重要 sysctl:

vm.dirty_ratio = 20             # 系統 dirty 達 RAM 20% 時,行程必須等待 writeback
vm.dirty_background_ratio = 10  # dirty 達 10% 時開始背景 flush

fsync(fd):強制當前 fd 的 dirty 頁刷回磁碟,並等到完成。資料庫每次 commit 都需要 ── 這就是寫入 latency 的來源。

一致性:journaling#

斷電可能導致檔案系統不一致:寫到一半的 inode、空懸的 block bitmap。journaling 在主資料區之外維護一個日誌:

  1. 先把要寫的 metadata(甚至 data)寫進 journal
  2. 確認 journal 寫成功
  3. 再把資料寫到主區
  4. journal 條目可被丟棄

斷電時若主區寫到一半 ── 重新掛載時 replay journal 可恢復一致性。

ext4 三種 journal mode:

mode行為
journalmetadata + data 都進 journal(最安全、最慢)
ordered只 metadata 進 journal,但保證 data 在 metadata 之前寫
writeback只 metadata 進 journal,data 可能滯後(最快、最危險)

ext4 預設 ordered ── 對多數場景的安全/效能取捨。

不同檔案系統的取捨#

檔案系統適用場景特色
ext4通用、伺服器、桌面成熟穩定、journaling
xfs大檔、大容量64-bit metadata、可線上 grow、快
btrfs想要 snapshot、subvolume、checksumCoW、透明壓縮、RAID 0/1/10、複雜
f2fsSSD / Flashflash-aware、wear leveling 友善
zfs想要終極資料完整性不在主線(授權問題)、需要外掛
tmpfs揮發性快取純記憶體、可 swap
overlayfs容器映像層upper + lower 合成

/proc 與 /sys 的特殊性#

procfs 與 sysfs 都不對應磁碟:

// 寫一個 /proc/myinfo 檔案
static int myinfo_show(struct seq_file *m, void *v) {
    seq_printf(m, "hello: %d\n", some_counter);
    return 0;
}

static int myinfo_open(struct inode *inode, struct file *file) {
    return single_open(file, myinfo_show, NULL);
}

static const struct proc_ops myinfo_ops = {
    .proc_open    = myinfo_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

proc_create("myinfo", 0444, NULL, &myinfo_ops);

seq_file 是 procfs 標準介面 ── 處理「邊讀邊產生」的串流,避免一次配大 buffer。

fuse ── 在 userspace 寫檔案系統#

對 prototype 或不想搞 kernel 模組的情況,FUSE(Filesystem in Userspace) 是極好的選擇:

userspace           kernel
─────────           ──────
your app   ───►  VFS  ───►  FUSE driver
                              │
                              ▼
                          /dev/fuse
                              │
                              ▲
your fs daemon ◄───────────────

GlusterFS、SSHFS、s3fs、NTFS-3G 都是 FUSE 實作。代價是每次 syscall 多一次 user/kernel 來回,效能比 in-kernel 差數倍 ── 但開發容易許多。

觀測#

mount                                # 已掛載清單
df -hT                               # 用量 + 類型
findmnt                              # 樹狀
lsof | head                          # 哪些檔案被誰打開
cat /proc/<pid>/fd/                  # 一個行程的所有 fd
fuser -m /mnt/foo                    # 誰在用這個 mount
iostat -x 1                          # 磁碟層 I/O
iotop                                # 哪個行程在做 I/O
biolatency  (bcc tool)               # block I/O 延遲分布
ext4slower  (bcc tool)               # ext4 慢操作

# inode 狀況
df -i                                # inode 用量
slabtop                              # dentry / inode_cache 用量

dentry / inode cache 太大時可手動 drop(不要在生產隨便做,會讓接下來的存取慢):

echo 1 > /proc/sys/vm/drop_caches    # page cache
echo 2 > /proc/sys/vm/drop_caches    # dentry + inode
echo 3 > /proc/sys/vm/drop_caches    # 全部

小結#

VFS 的設計優雅在於:

  1. 用四個物件抽象出所有檔案系統的共通結構
  2. 透過 fop / iop / sop / dop vtable 把具體實作 plug 進去
  3. page cache 作為共用快取層,所有檔案系統免費獲得快取能力
  4. 一致性靠 journaling,效能靠 page cache + writeback
  5. userspace 看到的永遠是統一的 syscall 介面

理解 VFS 後讀任何檔案系統原始碼都會輕鬆很多 ── 它們不過是在實作那幾個 vtable。