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 巨大 ── 它讓「重複存取相同路徑」幾乎零成本(不用每次重走檔案系統)。slabtop 看 dentry 是常駐前幾名。
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 block | loop、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#
寫入流程:
write()把資料寫進 page cache 對應頁,標記 dirtywrite()立刻回傳成功- 背景 thread(
pdflush老名、現在叫flush-N:mworkqueue)定期刷回磁碟 - 或 dirty 比例超過閾值 ➡️ 行程自己被 throttle 直到刷完
兩個重要 sysctl:
vm.dirty_ratio = 20 # 系統 dirty 達 RAM 20% 時,行程必須等待 writeback
vm.dirty_background_ratio = 10 # dirty 達 10% 時開始背景 flushfsync(fd):強制當前 fd 的 dirty 頁刷回磁碟,並等到完成。資料庫每次 commit 都需要 ── 這就是寫入 latency 的來源。
一致性:journaling#
斷電可能導致檔案系統不一致:寫到一半的 inode、空懸的 block bitmap。journaling 在主資料區之外維護一個日誌:
- 先把要寫的 metadata(甚至 data)寫進 journal
- 確認 journal 寫成功
- 再把資料寫到主區
- journal 條目可被丟棄
斷電時若主區寫到一半 ── 重新掛載時 replay journal 可恢復一致性。
ext4 三種 journal mode:
| mode | 行為 |
|---|---|
| journal | metadata + data 都進 journal(最安全、最慢) |
| ordered | 只 metadata 進 journal,但保證 data 在 metadata 之前寫 |
| writeback | 只 metadata 進 journal,data 可能滯後(最快、最危險) |
ext4 預設 ordered ── 對多數場景的安全/效能取捨。
不同檔案系統的取捨#
| 檔案系統 | 適用場景 | 特色 |
|---|---|---|
| ext4 | 通用、伺服器、桌面 | 成熟穩定、journaling |
| xfs | 大檔、大容量 | 64-bit metadata、可線上 grow、快 |
| btrfs | 想要 snapshot、subvolume、checksum | CoW、透明壓縮、RAID 0/1/10、複雜 |
| f2fs | SSD / Flash | flash-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 的設計優雅在於:
- 用四個物件抽象出所有檔案系統的共通結構
- 透過 fop / iop / sop / dop vtable 把具體實作 plug 進去
- page cache 作為共用快取層,所有檔案系統免費獲得快取能力
- 一致性靠 journaling,效能靠 page cache + writeback
- userspace 看到的永遠是統一的 syscall 介面
理解 VFS 後讀任何檔案系統原始碼都會輕鬆很多 ── 它們不過是在實作那幾個 vtable。