mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4221 字
11 分钟
VFS 与文件系统
2024-07-03

在上一章中,追踪了一个 I/O 请求从用户态 write() 系统调用出发,穿越页缓存、脏页写回,最终抵达块设备层的完整路径。但有一个关键问题被刻意留到了本章:当你调用 open("/home/user/doc.txt", O_RDONLY) 时,内核如何知道这个路径对应磁盘上的哪个位置?为什么同一个 read() 系统调用既能读取 ext4 上的普通文件,也能读取 /proc/cpuinfo 里的内核信息,甚至能读取挂载到 /mnt/usb 的 FAT32 U 盘?

答案就是 VFS(Virtual File System,虚拟文件系统)——Linux 内核中最优雅的抽象层之一。VFS 在用户态系统调用与具体文件系统实现之间插入了一层统一接口,使得”一切皆文件”的 Unix 哲学真正落地。本章将深入剖析 VFS 的四大核心对象、目录项缓存、文件打开的完整路径、ext4 的磁盘布局,以及 procfs/sysfs/tmpfs 等特殊文件系统。

flowchart TB subgraph 用户空间 APP[用户进程] -->|open/read/write/close| SYSCALL[系统调用接口] end subgraph VFS层 SYSCALL --> VFS_OPS[VFS 通用操作] VFS_OPS --> SB[super_block] VFS_OPS --> INODE[inode] VFS_OPS --> DENTRY[dentry] VFS_OPS --> FILE[file] end subgraph 具体文件系统 SB --> EXT4_SB[ext4 super_ops] SB --> PROC_SB[proc super_ops] SB --> SYSFS_SB[sysfs super_ops] INODE --> EXT4_INO[ext4 inode_ops] INODE --> PROC_INO[proc inode_ops] INODE --> SYSFS_INO[sysfs inode_ops] FILE --> EXT4_FO[ext4 file_ops] FILE --> PROC_FO[proc file_ops] FILE --> SYSFS_FO[sysfs file_ops] end subgraph 存储后端 EXT4_SB --> BLOCK[块设备] EXT4_INO --> BLOCK EXT4_FO --> BLOCK PROC_SB --> KDATA[内核数据] PROC_INO --> KDATA PROC_FO --> KDATA SYSFS_SB --> KOBJ[内核对象] SYSFS_INO --> KOBJ SYSFS_FO --> KOBJ end style VFS_OPS fill:#4a90d9,color:#fff,stroke:#333 style SB fill:#f9a825,stroke:#333 style INODE fill:#66bb6a,stroke:#333 style DENTRY fill:#ab47bc,stroke:#333 style FILE fill:#ef5350,stroke:#333

一、VFS 的设计目标#

1.1 “一切皆文件”的统一接口#

Unix 的核心哲学之一是”一切皆文件”——普通文件、目录、设备、管道、套接字,统统用同一套 open/read/write/close 接口操作。但底层实现千差万别:ext4 的数据在磁盘上按块存储,procfs 的数据由内核函数动态生成,tmpfs 的数据驻留在内存中。VFS 的使命就是在这千差万别之上提供统一抽象。

VFS 的设计目标可以概括为:

  • 接口统一:用户态程序无需关心底层文件系统类型,同一套系统调用适用于所有文件系统
  • 文件系统无关性:内核子系统(如页缓存、IPC、命名空间)通过 VFS 间接访问文件系统,不直接依赖具体实现
  • 可扩展性:新增文件系统类型只需实现 VFS 规定的操作接口,无需修改上层代码
  • 跨文件系统操作:支持将不同类型的文件系统挂载到同一棵目录树上,路径解析透明地穿越挂载点

1.2 VFS 的核心抽象#

VFS 通过四个核心对象实现上述目标:

对象结构体代表什么生命周期
超级块struct super_block一个已挂载的文件系统实例从挂载到卸载
索引节点struct inode文件系统中的一个对象(文件/目录/设备)从查找到释放
目录项struct dentry路径中的一个组成部分,关联名字与 inode被 dcache 缓存
打开文件struct file进程打开的一个文件实例从 open 到 close
Note

inodedentry 的区别是理解 VFS 的关键:inode 代表”文件本身”(元数据 + 数据位置),dentry 代表”文件在目录树中的名字”。同一个 inode 可以有多个 dentry(硬链接),一个 dentry 恰好指向一个 inode。

二、四大核心对象详解#

2.1 super_block:文件系统的总管#

每个已挂载的文件系统在内核中对应一个 struct super_block 实例。它存储了该文件系统的全局元信息,是 VFS 访问具体文件系统的入口。

// include/linux/fs.h(简化)
struct super_block {
dev_t s_dev; // 设备号
unsigned long s_blocksize; // 块大小(字节)
loff_t s_maxbytes; // 文件最大尺寸
struct file_system_type *s_type; // 文件系统类型
const struct super_operations *s_op; // 超级块操作表
struct dentry *s_root; // 根目录项
struct list_head s_inodes; // 该文件系统所有 inode 链表
void *s_fs_info; // 具体文件系统的私有数据
// ... 更多字段
};

super_operations 定义了 VFS 对超级块的操作接口,由具体文件系统实现:

struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb); // 分配 inode
void (*destroy_inode)(struct inode *); // 销毁 inode
void (*dirty_inode)(struct inode *, int flags); // 标记脏 inode
int (*write_inode)(struct inode *, int wait); // 写回 inode
void (*drop_inode)(struct inode *); // 释放 inode 引用
void (*evict_inode)(struct inode *); // 驱逐 inode
void (*put_super)(struct super_block *); // 卸载时释放超级块
int (*sync_fs)(struct super_block *, int); // 同步文件系统
int (*statfs)(struct dentry *, struct kstatfs *); // 文件系统统计
int (*remount_fs)(struct super_block *, int *, char *); // 重新挂载
// ...
};

以 ext4 为例,ext4_sops 实现了上述接口,其中 write_inode 会将内存中的 inode 元数据写回磁盘上的 inode 表。

2.2 inode:文件的身份证#

struct inode 是 VFS 中最核心的对象,代表文件系统中的一个实体——无论它是普通文件、目录、符号链接还是设备节点。inode 存储了文件的静态元数据,与文件内容的位置信息。

// include/linux/fs.h(简化)
struct inode {
umode_t i_mode; // 文件类型与权限
uid_t i_uid; // 所有者 UID
gid_t i_gid; // 所有者 GID
loff_t i_size; // 文件大小(字节)
struct timespec i_atime; // 最后访问时间
struct timespec i_mtime; // 最后修改时间
struct timespec i_ctime; // 最后状态变更时间
const struct inode_operations *i_op; // inode 操作表
const struct file_operations *i_fop; // 文件操作表
struct super_block *i_sb; // 所属超级块
struct address_space *i_data; // 页缓存映射
void *i_private; // 具体文件系统私有数据
// ... 更多字段
};

inode_operations 定义了与 inode 相关的命名空间操作(创建、查找、链接等):

struct inode_operations {
struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int); // 目录查找
int (*create)(struct inode *, struct dentry *, umode_t, bool); // 创建文件
int (*link)(struct dentry *, struct inode *, struct dentry *); // 硬链接
int (*unlink)(struct inode *, struct dentry *); // 删除目录项
int (*symlink)(struct inode *, struct dentry *, const char *); // 符号链接
int (*mkdir)(struct inode *, struct dentry *, umode_t); // 创建目录
int (*rmdir)(struct inode *, struct dentry *); // 删除目录
int (*rename)(struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int); // 重命名
int (*getattr)(const struct path *, struct kstat *, ...); // 获取属性
int (*setattr)(struct dentry *, struct iattr *); // 设置属性
// ...
};

file_operations 定义了已打开文件上的操作(读、写、映射等):

struct file_operations {
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 *); // 写
int (*mmap)(struct file *, struct vm_area_struct *); // 内存映射
int (*open)(struct inode *, struct file *); // 打开
int (*flush)(struct file *, fl_owner_t); // 刷新
int (*release)(struct inode *, struct file *); // 关闭
int (*fsync)(struct file *, loff_t, loff_t, int); // 同步
// ...
};
Important

inode_operationsfile_operations 的分工:前者处理命名空间操作(路径解析、创建/删除/重命名),后者处理文件内容操作(读/写/映射)。这种分离使得目录和文件可以有不同的操作集——目录的 i_op 侧重 lookup,文件的 i_fop 侧重 read/write

2.3 dentry:路径的骨架#

struct dentry 代表目录项(directory entry),是路径中每个组成部分的内核表示。当内核解析路径 /home/user/doc.txt 时,会依次查找或创建四个 dentry:/homeuserdoc.txt。每个 dentry 将一个名字(字符串)与一个 inode 关联起来。

// include/linux/dcache.h(简化)
struct dentry {
atomic_t d_lockref; // 引用计数 + 自旋锁
unsigned int d_flags; // 标志位
struct qstr d_name; // 名字(哈希字符串)
struct inode *d_inode; // 关联的 inode
struct dentry *d_parent; // 父目录项
struct list_head d_child; // 父目录的子目录项链表
struct list_head d_subdirs; // 子目录项链表
const struct dentry_operations *d_op; // dentry 操作表
struct super_block *d_sb; // 所属超级块
void *d_fsdata; // 具体文件系统私有数据
// ...
};

dentry 本身不落盘——它是内核在内存中动态创建的对象,用于加速路径解析。dentry_operations 提供了少量可选的钩子:

struct dentry_operations {
int (*d_revalidate)(struct dentry *, unsigned int); // 重新验证(网络文件系统)
int (*d_hash)(const struct dentry *, struct qstr *); // 自定义哈希
int (*d_compare)(const struct dentry *, // 名字比较
unsigned int, const char *, const struct qstr *);
int (*d_delete)(const struct dentry *); // 删除通知
void (*d_release)(struct dentry *); // 释放
// ...
};

2.4 file:打开文件的句柄#

struct file 代表进程打开的一个文件实例。与 inode 不同,file 是进程上下文相关的——同一个文件被两个进程打开,会产生两个 struct file,各自维护独立的读写偏移量和打开标志。

// include/linux/fs.h(简化)
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 等)
fmode_t f_mode; // 模式(FMODE_READ/FMODE_WRITE)
loff_t f_pos; // 当前读写偏移量
void *private_data; // 私有数据
struct address_space *f_mapping; // 页缓存映射
// ...
};

文件描述符(fd) 是用户空间对 struct file 的间接引用。每个进程的 task_struct 中有一个 struct files_struct,它维护一个 fdtable——一个 struct file * 指针数组,文件描述符就是该数组的下标。

fd (int) → fdtable.fd_array[fd] → struct file → dentry → inode
Warning

文件描述符是进程级别的资源。fork() 后子进程继承父进程的 fdtable(COW),但 exec() 默认保留所有已打开的 fd(除非设置了 O_CLOEXEC)。忘记关闭文件描述符是常见的资源泄漏来源。

三、dcache:目录项缓存#

3.1 为什么需要 dcache?#

路径解析是文件操作中最频繁的工作。每次 open("/home/user/doc.txt") 都需要从根目录开始,逐级查找 homeuserdoc.txt。如果每次都从磁盘读取目录项,性能将不可接受。dcache(Directory Entry Cache)就是为解决这一问题而设计的——它缓存最近使用的 dentry 对象,使得路径解析大部分在内存中完成。

dcache 的核心数据结构是一个哈希表,以 {父 dentry 指针, 名字哈希值} 为键,快速定位目标 dentry。同时,所有 dentry 通过 d_parentd_child/d_subdirs 组成一棵目录树,与文件系统的逻辑结构一致。

3.2 dcache 的生命周期管理#

dentry 有三种状态:

  • 正在使用(in-use):被某个 struct file 或子 dentry 引用,d_lockref.count > 0,不能被回收
  • 未使用(unused):引用计数为 0,但仍然在缓存中,保留着与 inode 的关联,可以被回收
  • 负状态(negative):关联的 inode 已不存在(文件被删除),缓存”此路径不存在”的信息,避免重复磁盘查找

当内存紧张时,内核通过 shrink_dcache_sb() 等函数回收未使用的 dentry。dcache 与 inode 缓存(icache)协同工作——当 dentry 被回收时,它指向的 inode 引用计数减少,如果降为 0,inode 也会被回收。

Note

dcache 是 Linux 文件系统性能的关键优化。实测表明,在典型工作负载下,超过 95% 的路径解析可以在 dcache 中命中,无需访问磁盘。你可以通过 cat /proc/sys/fs/dentry-state 查看 dcache 的状态,其中第一个数字是当前 dentry 总数,第三个是未使用的 dentry 数量。

四、文件打开的完整路径#

当用户态程序调用 open("/home/user/doc.txt", O_RDONLY) 时,内核经历了一个复杂的过程。逐步追踪如下:

sequenceDiagram participant U as 用户空间 participant S as sys_openat participant D as do_sys_open participant P as path_openat participant L as link_path_walk participant DC as dcache participant FS as 具体文件系统 participant I as inode U->>S: open("/home/user/doc.txt", O_RDONLY) S->>D: do_sys_open(filename, flags, mode) D->>D: get_unused_fd_flags() 分配 fd D->>P: path_openat(nd, flags) P->>L: link_path_walk("home/user/doc.txt", nd) L->>DC: 查找 "/" dentry DC-->>L: 命中,返回根 dentry L->>DC: 查找 "home" dentry DC-->>L: 命中,返回 dentry L->>DC: 查找 "user" dentry DC-->>L: 命中,返回 dentry L->>DC: 查找 "doc.txt" dentry DC-->>L: 未命中 L->>FS: ext4_lookup(parent_inode, "doc.txt") FS->>I: 从磁盘读取 inode I-->>FS: 返回 inode FS-->>L: 创建新 dentry,关联 inode L-->>P: 路径解析完成 P->>P: vfs_open(path, file) P->>FS: ext4_file_open(inode, file) FS-->>P: 初始化文件实例 P-->>D: 返回 struct file D->>D: fd_install(fd, file) 关联 fd 与 file D-->>U: 返回 fd

4.1 系统调用入口#

open() 在内核中的入口是 SYSCALL_DEFINE4(openat, ...),最终调用 do_sys_open()

// fs/open.c(简化)
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
int fd = build_open_flags(flags, mode, &op); // 解析并验证标志
if (fd)
return fd;
fd = get_unused_fd_flags(flags); // 分配一个未使用的文件描述符
if (fd >= 0) {
struct file *f = do_filp_open(dfd, filename, &op); // 执行路径解析和文件打开
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f); // 将 file 指针安装到 fdtable
}
}
return fd;
}

4.2 路径解析#

do_filp_open() 调用 path_openat(),后者启动路径解析。核心函数是 link_path_walk()(定义在 fs/namei.c),它逐级解析路径分量:

  1. 从起始目录(绝对路径为根 dentry,相对路径为当前工作目录 dentry)开始
  2. 取出路径的下一个分量(如 home
  3. 在 dcache 中查找 {当前 dentry, 分量名} —— 如果命中,直接获得目标 dentry
  4. 如果 dcache 未命中,调用 inode->i_op->lookup() 让具体文件系统从磁盘查找
  5. 文件系统找到对应的 inode 后,创建新的 dentry 并加入 dcache
  6. 检查挂载点——如果当前 dentry 是一个挂载点,跳转到挂载文件系统的根 dentry
  7. 重复步骤 2-6,直到路径解析完毕

4.3 文件打开#

路径解析完成后,vfs_open() 被调用来完成最后的打开操作:

  1. 检查权限(inode_permission()
  2. 调用 inode->i_fop->open()(如果定义了),让具体文件系统执行初始化
  3. 创建 struct file 实例,设置 f_pathf_opf_mapping 等字段
  4. struct file 与文件描述符关联(fd_install()
Tip

你可以通过 strace -e openat cat /home/user/doc.txt 观察实际打开过程。更深入地,可以通过 tracefsbpftrace 追踪内核函数调用链:bpftrace -e 'kprobe:do_filp_open { printf("opening: %s\n", str(arg1)); }'

五、read/write 的 VFS 层#

5.1 读取路径#

当用户调用 read(fd, buf, count) 时,内核的执行路径如下:

// fs/read_write.c(简化流程)
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
ksys_read(fd, buf, count)
vfs_read(file, buf, count, &pos)
→ file->f_op->read(file, buf, count, &pos) // 方式一:直接调用
new_sync_read(file, buf, count, &pos) // 方式二:通用读
→ file->f_op->read_iter(&iocb, &iter) // 调用 read_iter

现代文件系统(如 ext4)通常实现 read_iter 而非 read,这样可以利用异步 I/O 和直接 I/O 等高级特性。read_iter 内部会通过 address_space(即 inode->i_data)访问页缓存——如果数据已在缓存中,直接拷贝给用户;否则触发缺页,从磁盘读取。

5.2 写入路径#

写入路径类似,但涉及脏页标记和写回调度:

// fs/read_write.c(简化流程)
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
ksys_write(fd, buf, count)
vfs_write(file, buf, count, &pos)
→ file->f_op->write_iter(&iocb, &iter)
generic_file_write_iter()
generic_perform_write() // 逐页写入页缓存
→ 标记脏页
→ 唤醒 writeback 守护进程(如果脏页过多)

六、ext4 文件系统结构概览#

ext4 是 Linux 上最广泛使用的本地文件系统,也是理解 VFS 与具体文件系统交互的最佳案例。

6.1 磁盘布局#

ext4 将磁盘划分为一系列块组(Block Group),每个块组包含:

组件作用
超级块(Superblock)文件系统全局元信息(块大小、总块数、inode 数等)
块组描述符表(BGDT)每个块组的元信息摘要
块位图(Block Bitmap)标记块组内哪些数据块已使用
inode 位图(Inode Bitmap)标记块组内哪些 inode 已使用
inode 表(Inode Table)存储该块组所有 inode 的磁盘结构
数据块(Data Blocks)存储文件内容和目录项
| 块组0 | 块组1 | 块组2 | ...
| SB | BGDT | BB | IB | IT | Data... | SB | BGDT | ... | SB | ...
Note

ext4 使用**稀疏超级块(Sparse Superblock)**特性——并非每个块组都有超级块副本,只有块组编号为 0、1 以及 3/5/7 的幂次方的块组才保存超级块副本,节省了大量空间。

6.2 Extent 树#

ext4 相比 ext3 的重要改进之一是用 Extent 替代了传统的间接块映射。一个 Extent 描述一段连续的物理块:

// ext4 的 extent 结构(简化)
struct ext4_extent {
__le32 ee_block; // 逻辑块号(文件内偏移)
__le16 ee_len; // 长度(块数)
__le16 ee_start_hi; // 物理块号高 16 位
__le32 ee_start_lo; // 物理块号低 32 位
};

一个 inode 的 extent 以树形结构组织:inode 内嵌一个叶子节点(可容纳 4 个 extent,覆盖最多 4 × 128MB = 512MB 数据);如果文件更大,则升级为索引节点 → 叶子节点的两层结构,最大支持约 4TB 的文件。

6.3 日志机制#

ext4 支持**日志(Journal)**来保证崩溃一致性。日志模式有三种:

模式说明性能安全性
data=journal元数据和文件数据都写入日志最低最高
data=ordered(默认)元数据写入日志,数据在元数据之前写入磁盘中等
data=writeback仅元数据写入日志,数据写入顺序不保证最高最低

日志的工作流程简述:事务开始 → 将元数据(或数据)写入日志区 → 提交事务(写入提交块) → 将实际数据写入磁盘对应位置 → 释放日志空间。崩溃恢复时,重放日志中已提交但未完成的事务。

七、特殊文件系统#

Linux 内核中有几类不依赖块设备的特殊文件系统,它们的数据完全由内核动态生成,是内核向用户空间暴露内部信息的重要窗口。

7.1 procfs:进程与内核信息#

procfs 挂载在 /proc,是最古老也是最重要的特殊文件系统之一。它以文件的形式暴露进程信息和内核参数:

  • 进程信息/proc/[pid]/ 目录下的 statusmapsfd/stat
  • 系统信息/proc/cpuinfo/proc/meminfo/proc/version
  • 内核参数/proc/sys/ 下的可调参数(通过 sysctl 修改)
  • 文件系统信息/proc/filesystems/proc/mounts

procfs 的实现核心是 proc_dir_entry 结构和 proc_ops 操作表。每个 /proc 下的文件都对应一对 show/write 回调函数,读取时调用 show 动态生成内容,写入时调用 write 修改内核参数。

7.2 sysfs:内核对象模型#

sysfs 挂载在 /sys,与内核的 kobject/device/driver 模型紧密绑定。它的目录结构反映了内核对象之间的层次关系:

  • /sys/devices/:所有设备按总线/层级组织
  • /sys/class/:按功能分类的设备(如 net/block/
  • /sys/bus/:按总线类型组织(如 pci/usb/
  • /sys/kernel/:内核级参数(如 mm/debug/
  • /sys/module/:已加载的内核模块信息

sysfs 的每个属性文件通常只包含一个值,遵循”一个文件一个属性”的设计原则,与 procfs 的”一个文件多个信息”风格形成对比。

7.3 tmpfs 与 devtmpfs#

tmpfs 是一个基于内存的文件系统,数据完全驻留在物理内存(或交换区)中。它没有磁盘后端,文件系统的大小受 size= 挂载参数限制。常见用途:

  • 共享内存(shm_open() 底层使用 tmpfs)
  • /tmp/run(现代 Linux 发行版默认将它们挂载为 tmpfs)
  • 临时文件存储

devtmpfs 是 tmpfs 的变体,专门用于 /dev 目录。内核在设备驱动注册/注销时自动在 devtmpfs 中创建/删除设备节点,无需依赖用户态的 udev 守护进程来创建设备文件。

7.4 特殊文件系统对比#

文件系统挂载点数据来源可写持久化
procfs/proc内核动态生成部分可写(/proc/sys/
sysfs/sys内核对象属性部分可写
tmpfs可变内存否(重启丢失)
devtmpfs/dev内核自动创建设备节点
debugfs/sys/kernel/debug内核调试信息

八、挂载机制#

8.1 vfsmount 与 mount 结构#

文件系统挂载是 VFS 与具体文件系统建立关联的过程。内核使用 struct vfsmountstruct mount 来描述一个挂载实例:

// fs/mount.h(简化)
struct mount {
struct list_head mnt_hash; // 全局哈希链表
struct mount *mnt_parent; // 父挂载
struct dentry *mnt_mountpoint; // 挂载点 dentry
struct vfsmount mnt; // 嵌入的 vfsmount
};
struct vfsmount {
struct dentry *mnt_root; // 挂载文件系统的根 dentry
struct super_block *mnt_sb; // 挂载文件系统的超级块
int mnt_flags; // 挂载标志
};

挂载信息通过挂载哈希表组织,以 {父 vfsmount, 挂载点 dentry} 为键。当路径解析遇到一个 dentry 是挂载点时,VFS 会查找哈希表,跳转到挂载文件系统的根 dentry,继续解析。

8.2 挂载命名空间#

Linux 2.4.19 引入了挂载命名空间(Mount Namespace),允许不同进程看到不同的文件系统挂载视图。这是容器技术的基础之一:

  • 子挂载命名空间继承父命名空间的挂载点
  • 子命名空间中的挂载/卸载操作对父命名空间不可见(取决于传播类型)
  • 挂载传播类型:MS_SHARED(共享)、MS_SLAVE(从属)、MS_PRIVATE(私有)、MS_UNBINDABLE(不可绑定)
# 创建新的挂载命名空间
unshare --mount /bin/bash
# 在新命名空间中挂载,不影响宿主
mount -t tmpfs tmpfs /mnt/test

8.3 挂载流程#

mount() 系统调用的内核路径:

  1. ksys_mount()do_mount() 解析挂载参数
  2. vfs_parse_fs_string() 解析文件系统类型和选项
  3. fc = fs_context_for_mount() 创建文件系统上下文
  4. vfs_get_tree(fc) → 调用文件系统类型的 get_tree 回调,读取超级块
  5. do_new_mount(fc) → 创建 struct mount,关联超级块和挂载点
  6. 将新挂载加入全局哈希表和命名空间的挂载链表

九、动手实践#

9.1 观察 VFS 对象#

# 查看文件系统类型
df -T
# 查看文件的 inode 信息
stat /etc/passwd
# 查看目录项缓存状态
cat /proc/sys/fs/dentry-state
# 查看系统支持的文件系统类型
cat /proc/filesystems
# 查看当前挂载信息
cat /proc/mounts
# 或
mount -l

9.2 探索 ext4 文件系统#

# 查看 ext4 超级块信息
sudo dumpe2fs /dev/sda1 | head -50
# 查看 ext4 块组描述符
sudo dumpe2fs /dev/sda1 -bg
# 使用 debugfs 探索 ext4 内部结构
sudo debugfs /dev/sda1
# 在 debugfs 中:
# stat <inode号> 查看 inode 详情
# ls -l / 列出根目录
# blocks <inode号> 查看文件的块分配
# dump <文件> /tmp/out 提取文件内容
# 查看文件的 inode 号和磁盘块
ls -lai /etc/passwd
sudo hdparm --fibmap /etc/passwd

9.3 探索特殊文件系统#

# procfs:查看进程信息
cat /proc/self/status
ls /proc/self/fd/ # 查看当前进程的文件描述符
cat /proc/self/maps # 查看内存映射
# procfs:查看内核参数
cat /proc/sys/vm/dirty_ratio
sudo sysctl vm.dirty_ratio=20 # 修改内核参数
# sysfs:查看设备模型
ls /sys/class/net/
cat /sys/class/net/eth0/mtu
echo 9000 | sudo tee /sys/class/net/eth0/mtu # 修改 MTU
# tmpfs:创建和使用
mount -t tmpfs -o size=100M tmpfs /mnt/tmpfs
df -h /mnt/tmpfs
dd if=/dev/zero of=/mnt/tmpfs/test bs=1M count=50

9.4 追踪文件打开路径#

# 使用 strace 追踪 open 系统调用
strace -e openat cat /etc/hostname
# 使用 bpftrace 追踪内核路径解析
sudo bpftrace -e 'kprobe:do_filp_open { printf("path: %s\n", str(arg1)); }'
# 使用 perf 追踪 VFS 操作
sudo perf record -e 'vfs:*' -a sleep 5
sudo perf script

参考资料#

内核源码#

路径内容
fs/open.cdo_sys_open()vfs_open() 实现
fs/read_write.cvfs_read()vfs_write() 实现
fs/namei.clink_path_walk()path_lookupat() 路径解析
fs/dcache.cdcache 实现,dentry 分配/查找/回收
fs/inode.cinode 缓存(icache)实现
fs/ext4/ext4 文件系统完整实现
fs/proc/procfs 实现
fs/sysfs/sysfs 实现
fs/namespace.c挂载命名空间实现
include/linux/fs.hVFS 核心数据结构定义
include/linux/dcache.hdentry 结构定义

推荐阅读#

  • 《深入理解 Linux 内核》 第 12 章——VFS 与文件系统完整论述
  • 《Linux 内核设计与实现》 第 13 章——VFS 抽象层与具体文件系统
  • Linux 内核文档 — VFS — 官方 VFS 开发者文档
  • ext4 Disk Layout — ext4 磁盘布局的权威参考
  • Bootlin Elixir — 在线浏览内核文件系统源码

从 VFS 的统一抽象到 ext4 的磁盘布局,从 dcache 的缓存策略到挂载命名空间的隔离——理解了 VFS,你就理解了 Linux 文件系统世界的”宪法”。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

VFS 与文件系统
https://blog.souloss.com/posts/linux-internals/vfs-and-filesystems/
作者
Souloss
发布于
2024-07-03
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时