
开局先说个我自己的经历。刚工作那会儿排查一个磁盘满了但du看不到大文件的问题折腾了半天才发现是进程删掉日志后句柄没释放空间挂在/proc里下不来后来又有一次一台机器写入卡顿最后定位到是脏页回刷策略和挂载参数没调好。这两次都指向同一个东西Linux的VFS虚拟文件系统。你可以把VFS理解成内核里的文件系统翻译官——它自己几乎不存数据但所有open、read、write、stat、mount都要先经过它再由它分发给底下真正干活的ext4、XFS、NFS、procfs、tmpfs。搞运维的、写驱动的、做安全加固的、准备面试的其实都绕不开VFS。这篇就把VFS从抽象对象到实操观测、从调用链路到踩坑排查一层层拆开讲透尽量让你看完能自己动手验证而不是只记住几个名词。1. 为什么Linux非要套一层VFS1.1 没有VFS的内核会乱成什么样先从一个反直觉的问题入手Linux上有几十种文件系统本地磁盘的ext4、XFS内存里的tmpfs网络上的NFS、CIFS内核暴露状态的procfs、sysfs、debugfs还有各类设备节点、管道、socket。如果不存在统一抽象层那么每一个文件系统都要自己实现一套完整的系统调用入口cat要读ext4就得调ext4的read要读NFS就得调NFS的read。用户空间程序不可能为每种文件系统写一套分支逻辑cp、grep、tar这些工具也没法通用。所以内核必须引入一个中间层把所有文件系统的能力收敛成一套固定接口。上层系统调用只认这套接口比如read最终统一走vfs_read下层各文件系统只负责实现这套接口里自己关心的部分比如ext4实现ext4_file_read_iter。这个中间层就是VFSVirtual File System虚拟文件系统。它带来的直接好处是内核新增一种文件系统上层工具和系统调用完全不用改只要求新文件系统按约定填好那几张函数表。这就是一切皆文件这句口号能落地的技术前提——不是因为Linux文件多而是因为VFS把文件抽象成了统一的操作集合任何对象只要能实现读、写、查找这些操作就能被当作文件来用。1.2 VFS的职责边界它管什么又不管什么这里必须把边界划清楚否则很容易越学越糊。VFS负责的是这么几件事命名空间和路径解析/a/b/c怎么一步步找到目标、对象生命周期管理缓存、引用计数、释放、通用操作分发把read路由给具体文件系统、若干跨文件系统共享的缓存逻辑比如page cache、dentry cache。它不负责的是数据块在磁盘上怎么摆放、日志怎么记录、元数据怎么校验、空间怎么分配。这些全部是具体文件系统的活。用生活化的类比VFS像是一个物流总调度台它知道每个包裹该送到哪个网点、怎么编号、怎么查在途状态但它不管货车怎么装、油怎么加、路线怎么规划。网点具体文件系统才管这些。新手常见的误解是把page cache算成VFS的完全私有财产其实更准确的说法是page cache是VFS提供给所有基于块设备、愿意用通用读写路径的文件系统的公共缓存层文件系统可以选择用也可以绕过比如直接用direct I/O甚至可以自己另搞一套。理解了这条边界后面看各种现象就不会张冠李戴。1.3 选这套设计的代价与收益任何设计都有取舍。VFS这套抽象的代价是多了一层间接调用每次read都要先经过VFS再跳到具体实现函数指针跳转和结构体嵌套带来一点开销同时抽象意味着最小公倍数某些文件系统的特殊能力无法通过通用接口完全暴露只能靠专属的ioctl或者扩展操作来补。收益则是巨大的统一的权限检查inode_permission集中在VFS做、统一的缓存管理、统一的挂载语义、统一的安全钩子入口LSM就挂在VFS的关键路径上。安全产品、审计工具、加密文件系统、快照文件系统基本都是贴着VFS提供的这些扩展点做文章的。换句话说VFS不只是让文件能打开它还是整条存储栈的公共骨架所有外挂能力都得挂在这副骨架上。2. 四大核心对象sb、inode、dentry、file 逐个拆2.1 super_block一个已挂载文件系统实例的户口本struct super_block描述的是一次具体的挂载实例。注意区分两个概念struct file_system_type是文件系统类型比如ext4这个类型本身系统启动时通过register_filesystem注册进全局链表而super_block是这个类型被挂载了一次所产生的运行时对象。同一个ext4设备挂载两次比如bind mount或不同挂载点通常共享同一个sb但像tmpfs每次挂载都会生成独立的sb。sb里关键的字段s_op指向super_operations这是文件系统级别的操作表包含alloc_inode、destroy_inode、write_inode、sync_fs、statfs、evict_inode等s_root是整个文件系统树的根dentry挂载的本质就是把s_root嫁接到父文件系统的某个dentry上s_fs_info是文件系统私有的挂载信息ext4会把自己的ext4_sb_info挂在这里s_list把所有sb串成链表cat /proc/filesystems和/proc/mounts的数据就来源于这些链表。理解sb的一个好办法是把它当成这块设备被内核接管后的状态容器块设备句柄、挂载选项、统计计数、缓存策略都挂在这上面卸载时才整体回收。2.2 inode文件元数据的真正载体struct inode是VFS里最重要的对象代表一个文件实体注意它和文件名是解耦的——同一个inode可以有多个名字这就是硬链接。关键字段包括i_inoinode号、i_mode类型和权限位、i_size大小、i_nlink硬链接计数、i_uid/i_gid、三个时间戳i_atime/i_mtime/i_ctime、i_opinode_operations管lookup、create、link、unlink、mkdir这类目录和元数据操作、i_fopfile_operations管read、write、mmap这类数据操作、i_mapping指向address_spacepage cache的入口、i_count引用计数、i_dentry指向所有引用它的dentry的链表。这里有个非常经典的C语言技巧值得单独说磁盘上的inode结构和内存中的struct inode不是一回事。EXT4的磁盘inode定义和VFS的struct inode完全不同内核对不上怎么办做法是让具体文件系统自己定义一个更大的结构体比如struct ext4_inode_info把struct inode vfs_inode作为其中一个成员嵌进去然后用container_of宏从vfs_inode的地址反推出外层结构体的地址。这相当于用组合加偏移实现了继承是内核里最常用的面向对象模拟手法看懂这一手很多内核结构就通了。注意i_count是内存引用计数i_nlink是磁盘硬链接计数两者含义完全不同。文件被unlink后i_nlink变0但只要还有进程持有引用i_count不为0inode就不会释放磁盘空间也就不会回收——后面第5章会专门讲这个坑。2.3 dentry路径解析的加速器struct dentry叫目录项它把一个名字和一个inode关联起来。很多人第一次看会觉得它多余既然有了inode代表文件为什么还要dentry答案是性能。路径/usr/local/bin/python如果每次都从磁盘目录块里逐级查找名字开销会大到无法接受。dentry cache简称dcache把这些名字→inode的映射缓存在内存里热路径上一个open几乎可以完全不碰磁盘。dentry的关键字段d_name名字是一个qstr含指针和长度、d_parent父dentry、d_inode指向对应inode负dentry时为NULL、d_subdirs和d_child把树结构串起来、d_opdentry_operations比如d_revalidate、d_hash、d_compare网络文件系统靠这些做缓存一致性、d_sb、d_lockref锁和引用计数。还有两个概念必须掌握**负dentrynegative dentry**是指d_inode NULL的dentry表示这个名字之前查过确实不存在。别小看它大量程序会反复探测不存在的文件负dentry把这种无效查找也缓存下来避免每次都去读磁盘。dentry状态分为used、unused、negative三种/proc/sys/fs/dentry-state里的几个数字就是它们各自的计数。dcache内存不够时可以回收unused状态的dentry是优先回收对象这就是为什么你有时候发现目录刚访问过很快过一会儿又慢了。2.4 file站在进程视角的打开文件struct file代表一个进程打开了一个文件这件事注意它和inode的区别inode是文件本身file是打开这个动作产生的实例。同一个文件被两个进程打开会有两个struct file但共享同一个inode。fork之后父子进程会共享同一个struct file因为file的引用计数f_count加1所以共享文件偏移量而重新open一次则生成新的file偏移量独立。关键字段f_path包含挂载点和dentry注意它同时携带vfsmount信息这是路径解析能处理挂载点跨越的关键、f_op指向文件系统给的file_operations读写的真正入口、f_pos当前读写偏移、f_flagsO_RDONLY、O_NONBLOCK等、f_count引用计数、f_cred打开时的凭证权限检查基于它、private_data文件系统私有数据比如设备驱动常把自定义结构挂这里。VFS的很多骚操作就是围绕f_op做文章在open返回前替换掉file-f_op就能在不影响其他文件的前提下劫持某个文件的所有读写——这正是各类内核级监控、加密、审计模块的常见切入点第3.4节会展开。四个对象的关系可以用一张表快速对照对象代表什么生命周期关键操作表常见误区super_block一次挂载实例挂载到卸载super_operations与file_system_type混淆inode文件实体元数据缓存中引用归零可回收inode_operations / file_operations误以为inode含文件名dentry名字到inode的映射dcache缓存可回收dentry_operations误以为每个文件必有dentryfile一次打开动作open到closefile_operations误以为多进程打开共用一个file3. 一次open到read内核里到底走了哪些路3.1 挂载把一棵文件系统树嫁接到全局目录树挂载的本质是把一个super_block的根dentrys_root挂到现有目录树的某个dentry上。流程大致是系统调用进入do_mount最终到do_new_mount先调vfs_get_tree它会找到对应file_system_type的挂载方法EXT4对应ext4_mount内部走mount_bdev接着mount_bdev打开块设备调用sget或sget_fs_type去查找或新建super_block并执行文件系统的fill_super回调来读取超级块、初始化根inode最后graft_tree把新树的根接到目标挂载点上并生成struct mount挂载点信息包含mnt_mountpoint和父mount。这里有个很多人疑惑的现象挂载点目录下原有的文件消失了。原因就在于路径解析走到挂载点时VFS会检测到该dentry上挂了别的文件系统通过d_flags里的DCACHE_MOUNTED和__lookup_mnt于是自动切换到新树的根原目录内容被遮蔽但没被删除卸载后又会露出来。理解这一点排查umount失败时就多了一个视角——挂载点被占用往往不是文件被删了而是路径解析还卡在跨越点上。3.2 路径解析dcache如何省下绝大部分磁盘访问open(/usr/local/bin/python)进入path_openat核心是link_path_walk和walk_component。对路径里的每一段名字先尝试lookup_fast也就是走RCU-walk模式在dcache里查。RCU-walk是一种不加锁、只读的快速查找模式命中就能直接拿到dentry开销极低没命中或者遇到需要确认的场景就退化成ref-walk模式加锁并可能触发真正的磁盘查找。磁盘查找走lookup_slow最终调用父目录inode的i_op-lookupEXT4对应ext4_lookup它会去读目录数据块、找到目录项、加载目标inode或者判定为负dentry然后构造dentry挂进dcache。整个过程里热路径的dcache命中率通常极高strace里你甚至看不到明显的系统调用耗时——因为磁盘I/O根本不是每次open都发生。这也解释了为什么删除大量文件后重新遍历会变慢dcache被回收了第一次遍历要重建缓存第二次才快。3.3 read/write落地从vfs_read到块设备提交读的链路sys_read→ksys_read→vfs_readVFS做参数校验和权限相关检查前面open时已经检查过这里是补充检查然后调用file-f_op-read_iter。对EXT4来说就是ext4_file_read_iter通常进一步进入generic_file_read_iter在page cache里找对应页命中直接拷到用户缓冲区未命中触发filemap_read和预读最终由address_space的a_ops-readahead或readpage下发I/OEXT4走ext4_mpage_readpages把连续的页合并成bio提交给块层。写的链路sys_write→vfs_write→f_op-write_iter。对普通文件的缓冲写数据先落进page cache并把该页标记为脏系统调用就返回了——这就是为什么写文件看起来快。真正的落盘由内核回写线程现代内核是kworker配合writeback机制在后台异步完成或者被fsync、fdatasync、sync强制触发。所以**write返回成功不等于数据在磁盘上**断电可能丢最近几秒的写这是运维事故的经典来源。想搞清这一点你可以自己写个测试写一大块数据后立刻断电源再看文件完整性比看一百篇文档都管用。3.4 在内核里拦截file_operations的几种思路这一节是给做内核模块开发和学习的同学准备的属于自建实验环境下的技术探讨。想看住文件读写常见路子有这么几条各有取舍第一种kprobe/ftrace。挂在vfs_read、vfs_write或具体f_op函数上打印参数优点是无需改动原逻辑、内核一般自带支持缺点是性能开销明显、无法真正阻断调用只能观测。第二种替换file-f_op。在伪造的open比如自己实现的文件系统或通过类似overlayfs的思路叠一层里把新打开文件的file-f_op换成自己的包装表转发调用前先做处理。优点是粒度细、只影响目标文件缺点是要处理好private_data、偏移量、并发。第三种stackable文件系统。像ecryptfs那样自身是个完整文件系统把底层文件系统当作后端所有IO在中间过一道。这是最正规的做法可长期维护但开发量大。第四种LSM钩子。利用security_file_open、security_file_permission等官方安全钩子在关键决策点介入适合做访问控制类需求工程上比前几种稳。第五种修改系统调用表。听起来直接但现代内核里sys_call_table通常是只读的硬改得先处理页表写保护还容易触发内核自保护机制且随版本变化极易失效极不推荐。/* kprobe 观测 vfs_read 的最小示意仅用于自建实验环境 */ static struct kprobe kp { .symbol_name vfs_read }; static int handler_pre(struct kprobe *p, struct pt_regs *regs) { struct file *f (struct file *)regs-di; if (f f-f_path.dentry) pr_info(vfs_read: %s\n, f-f_path.dentry-d_name.name); return 0; }注意以上代码仅为原理示意不同内核版本的pt_regs参数寄存器x86_64是diARM64是regs[0]和符号可见性都不同编译前务必对照你所用内核的头文件。任何此类模块都应只在你自己的实验机器上加载做好备份别放到生产环境。选择哪条路取决于你的真实目标只是想看清楚就用kprobe要改行为就用包装或叠层文件系统要做安全策略优先考虑LSM。别一上来就想着动系统调用表那是投入产出比最差的一条路。4. 动手观测命令行里能看见的VFS痕迹4.1 从/proc和/sys读出挂载与缓存状态VFS虽在内核里但状态被大量导出到伪文件系统动手看是理解它最快的方式cat /proc/filesystems # 已注册的文件系统类型nodev表示不需要块设备 cat /proc/mounts # 当前挂载格式设备 挂载点 类型 选项 0 0 cat /proc/self/mountinfo # 更详细含mount id、父id、可选的传播属性 cat /proc/sys/fs/dentry-state # nr_dentry nr_unused age_limit ... cat /proc/sys/fs/inode-state # nr_inodes nr_free_inodes ... cat /proc/slabinfo | grep -E dentry|inode_cache # 各缓存对象占用/proc/mounts其实是/proc/self/mounts的软链内容基于当前进程的挂载命名空间。如果你用了容器技术会发现容器里的/proc/mounts和宿主机不同这就是挂载命名空间隔离的体现——VFS的挂载视图是按namespace维度的。/proc/self/mountinfo比mounts多了shared:1这类传播属性做mount --bind和mount --make-*实验时你会用到。4.2 dcache和inode缓存的观测与清理dentry-state第一列是dentry总数第二列是处于unused状态的可回收第三列age_limit是目标保留时间秒。如果你发现nr_dentry持续膨胀通常是有程序在遍历大量文件缓存暂时没收。inode-state的第三列开始是各种分配统计nr_free_inodes为0而nr_inodes接近上限时就要警惕后面讲的inode耗尽问题。想清缓存做实验sync # 先把脏页落盘避免丢数据 echo 1 /proc/sys/fs/drop_caches # 清page cache echo 2 /proc/sys/fs/drop_caches # 清dentry和inode缓存 echo 3 /proc/sys/fs/drop_caches # 全清注意drop_caches是全局动作会短暂拖慢整机性能缓存全失效后要重建生产环境不要随手敲。做性能对比实验时每次测量前用同一套清理策略结果才有可比性。4.3 用strace和bpftrace看清调用路径最简单的观测手段是strace看某个程序到底碰了哪些文件strace -f -e traceopenat,read,write,close -o trace.log ./your_program grep -c openat trace.log # 数一下打开次数想在内核态看得更深bpftrace一行就能统计vfs_read的调用分布需要内核开启对应支持bpftrace -e kprobe:vfs_read { [comm] count(); }跑几秒后按CtrlC它会按进程名输出vfs_read的调用次数排行。这个用法我常用来判断到底是谁在疯狂读盘——比看iostat更直接因为它能告诉你是哪个进程的哪类调用。如果要看的是具体文件可以把kprobe:vfs_read换成对vfs_read参数取dentry-d_name.name的聚合。这类内核态观测需要root和对应内核支持测试机上玩别在生产上长跑。4.4 数据落盘sync、fsync与脏页策略sync把整个系统的脏页刷盘fsync(fd)只刷某个文件及其必要的元数据fdatasync(fd)只刷数据必要时才刷元数据比fsync轻。数据库这类应用通常对关键提交点用fsync保证持久性代价是每次都要等I/O完成吞吐会掉。相关的调优参数sysctl vm.dirty_ratio # 脏页占总内存比例上限超过后写操作同步阻塞 sysctl vm.dirty_background_ratio # 超过此比例后台开始回刷 sysctl vm.dirty_expire_centisecs # 脏页最长存活时间单位1/100秒 sysctl vm.dirty_writeback_centisecs # 回刷线程唤醒间隔经验上写密集型的机器比如日志收集如果发现周期性卡顿多半是脏页积压后集中回刷造成的。适当降低dirty_background_ratio让回刷更平缓或者把dirty_expire_centisecs调小往往能改善抖动。但这不是银弹参数调小会牺牲部分批量合并写带来的吞吐优势得结合你的负载类型实测。我一般会先在测试环境用fio跑混合负载对比几组参数下的P99延迟再决定线上值。4.5 用procfs给VFS挂一个自己的文件想真正摸到VFS最好的方式是让内核里出现一个属于你的文件。最简单的做法是用procfs注册一个读写接口#include linux/proc_fs.h #include linux/uaccess.h static char buf[64]; static ssize_t my_read(struct file *f, char __user *u, size_t n, loff_t *off) { return simple_read_from_buffer(u, n, off, buf, strlen(buf)); } static ssize_t my_write(struct file *f, const char __user *u, size_t n, loff_t *off) { if (n sizeof(buf)) return -EINVAL; if (copy_from_user(buf, u, n)) return -EFAULT; buf[n] \0; return n; } static const struct file_operations my_fops { .owner THIS_MODULE, .read my_read, .write my_write, }; static int __init my_init(void) { proc_create(my_vfs_demo, 0666, NULL, my_fops); return 0; } static void __exit my_exit(void) { remove_proc_entry(my_vfs_demo, NULL); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE(GPL);配合一个标准的内核模块Makefile编译加载后你会看到/proc/my_vfs_demoecho hello vfs /proc/my_vfs_demo cat /proc/my_vfs_demo这个例子虽小但完整串起了VFS的核心链条file_operations被VFS调用simple_read_from_buffer帮你处理了偏移量语义copy_from_user完成了用户态和内核态的数据搬运。写一遍、调一遍、故意写错权限或者返回错误码看内核怎么反应比读十章文档理解得都深。想更进一步可以试试用debugfs或configfs或者挑战写一个能mount上去的独立文件系统。5. 踩过的坑与高频问题排查5.1 磁盘有空间却报No space left on device这是最经典的VFS相关误判。df -h显示还有几十G但创建文件就是失败。八成是inode耗尽df -i # 看 IUse% 是否接近100%inode数量在格式化时确定EXT4可以用mke2fs -N调整但事后调整麻烦如果某个目录塞了上百万个小文件inode先用光块空间再多也没用。定位哪个目录find / -xdev -type d -printf %p\n | while read d; do echo $(find $d -maxdepth 1 | wc -l) $d; done | sort -rn | head或者用du --inodes部分版本支持。解决方向是清理小文件、归档合并长期方案是迁移到支持动态inode的文件系统或者格式化时预留更多inode。5.2 文件删了空间不释放deleted but open上一章提到的i_nlink和i_count分离这里就见真章了。进程删掉文件后如果还有别的进程打开着它inode不释放、空间不回收du看不到但df显示占用。排查lsof L1 # 列出link数小于1的已打开文件 ls -l /proc/*/fd 2/dev/null | grep deleted找到持有进程后重启该进程或者让它关闭句柄空间立刻回来。紧急情况下可以清空而非删除: /path/to/big.log # 截断句柄保留但空间释放这个技巧在日志服务上极其常用——直接rm日志后服务还握着句柄磁盘不降反升用截断就避免了。但要注意多进程共享同一日志句柄时截断后偏移量可能混乱最好还是让服务自己支持reopen信号很多服务响应SIGUSR1重开日志。5.3 umount失败device is busyumount报target is busy原因通常有几类有进程的工作目录在挂载点里、有进程持有该文件系统上的打开文件、或者上层还嵌套着别的挂载。排查三板斧lsof D /mnt/point # 谁打开了里面的文件大目录会很慢 fuser -vm /mnt/point # 谁在用这个挂载点 cat /proc/self/mountinfo | grep /mnt/point # 确认是否还有嵌套挂载实在找不到进程fuser -km可以强杀占用者但生产环境别乱用。还有一个隐蔽原因某些进程的cwd指向已卸载目录的旧inode这时lsof可能也难定位可以用ls -l /proc/*/cwd | grep deleted辅助。养成习惯跑服务的目录别放在临时挂载点下面。5.4 中文文件名乱码到底是谁的锅很多人一乱码就怀疑VFS其实要分清层次。在VFS和大多数本地文件系统里文件名就是一串字节内核不附加编码语义除了少数有明确约定的文件系统。乱码通常来自两个地方一是挂载参数比如FAT/exFAT/NTFS这类跨平台文件系统需要指定iocharset和codepage不建议用默认的ascii二是终端或程序的locale和编码不匹配比如服务端是UTF-8、客户端用GBK解。排查顺序locale # 看当前编码环境 ls | iconv -f utf-8 -t gbk # 验证是不是显示层问题如果是挂载参数问题重新挂载时指定mount -o iocharsetutf8,codepage936 /dev/sdX /mnt/data关键是先判断乱码发生在存储层还是显示层用xxd看文件名字节如果字节本身正常那就是显示端的事改挂载参数没用。5.5 内核模块卸载不了Module in use卸载模块时报Module XXX is in use本质是引用计数没归零。常见原因还有文件通过该模块的file_operations打开着、还有进程占着该模块创建的文件系统挂载点、或者模块的exit函数里忘了清理proc_create创建的节点。排查lsmod | grep your_module # 第三列是引用计数 cat /proc/modules | grep your_module从模块开发角度.owner THIS_MODULE会自动维护打开文件的引用但你自己创建的对象proc节点、字符设备、文件系统需要在exit里显式remove或unregister否则计数下不来。我见过最典型的坑是把proc_create放在init里、却忘了在exit里对应remove_proc_entry结果模块永远卸不掉只能重启。现象排查命令根因处理方式No space left on devicedf -iinode耗尽清理小文件或调整inode数df满但du看不到大文件lsof L1文件被删句柄未关截断文件或重启进程umount报busylsof D、fuser -vm有进程占用挂载点迁移工作目录、关句柄文件名乱码locale、xxd编码或挂载参数不匹配调整iocharset/locale模块无法卸载lsmod引用计数未归零补齐资源释放逻辑6. 面试高频考点与我的实操体会6.1 高频问题速查VFS这块几乎是Linux岗位的必问区我把常被问到的整理成问答形式方便对着自测问VFS四大对象是什么各自职责答super_block管一次挂载实例inode管文件实体元数据dentry管名字到inode的映射与缓存file管一次打开动作。要点在于强调inode不含文件名名字在dentry上。问硬链接为什么不能跨文件系统答硬链接本质是让多个dentry指向同一个inode。跨文件系统的话inode分属不同的super_blockinode号可能冲突i_nlink的语义也跨不过去所以设计上禁止。软链接不一样它存的是路径字符串可以跨。问软链接在VFS层怎么实现答软链接文件有独立inode读它的内容就是读一段路径字符串路径解析遇到软链接会重新走一遍link_path_walk做符号链接展开并受MAXSYMLINKS通常40层限制防循环。问open一个文件究竟读了几次磁盘答取决于缓存。路径各级如果在dcache里可能零次inode没缓存要读一次数据页没缓存read时才触发。热路径常常完全不碰磁盘这也是要强调缓存概念的原因。问page cache和buffer cache有什么区别答现在内核里两者基本统一为page cacheaddress_space粒度为页。历史上buffer cache按块组织用于缓存块设备的块和文件系统元数据如今元数据也通过page cache走buffer_head仍作为块层的一部分存在但不再是独立的大缓存体系。问挂载点下面的原文件去哪了答没删被遮蔽了。路径解析到挂载点时切换到新文件系统树的根卸载后原内容恢复可见。问/proc、/sys是什么文件系统答都是伪文件系统procfs、sysfs不基于块设备/proc/filesystems里带nodev标记。它们通过实现file_operations把内核数据映射成文件是VFS抽象能力的最佳示范。6.2 一些实打实的经验我个人的体会是VFS这套东西光看结构定义会越看越晕真正开窍的瞬间发生在你同时用两条线去看一条是代码路径从sys_read往下追到具体文件系统的read_iter一条是现场现象/proc里的计数、strace里的调用、df -i和lsof L1的输出。只看代码你不知道线上会撞上什么只看现象你不知道为什么。还有个反复踩到的坑值得提醒不要迷信删除即释放。做日志清理、临时文件管理、容器镜像瘦身时一定要确认持有句柄的进程已经关闭或重启否则你删了半天空间纹丝不动。稳妥的做法是先truncate再让服务重开或者用lsof L1验证。最后给做内核模块和存储方向的同学一个建议想深入VFS别从写复杂文件系统开始先写一个能读写的proc节点再写一个能mount上去的只读伪文件系统最后再试着叠一层包装文件系统去看清file_operations的转发逻辑。每一步都用bpftrace或者ftrace验证你的函数真的被调用了。这套路径走一遍比背十条面试答案管用得多——因为你会亲眼看到VFS到底在什么时候把控制权交给了谁。