开门见山说个反直觉的事实:在Linux上,普通开发者想写一个自己的文件系统,门槛根本不在“文件系统”本身,而在“内核态”。一个文件系统要挂载进VFS,传统上意味着你要改内核、调内核、编内核,稍微踩错一个指针就是整机panic。直到FUSE出现,这个局面才被彻底打破——文件系统逻辑可以跑在用户态,像写一个普通守护进程一样去实现。
这篇是系列教程的第1章,目标我定得很明确:先把文件系统原理里和日常开发强相关的部分讲透,再把FUSE3的开发环境完整搭起来。适合两类人看:一类是想搞明白“Linux文件系统到底是怎么回事”的进阶开发者,另一类是打算用FUSE做实际项目(网络盘、加密盘、资源映射、嵌入式只读文件系统)但卡在环境上的工程师。原理部分我不按教科书那样从磁盘结构讲起,直接从VFS切入,因为这才是FUSE真正挂载的位置。
1. 文件系统在现代操作系统里的真实角色
1.1 VFS:文件系统的“总调度台”
很多人一提到文件系统就想到ext4、XFS、FAT32,觉得文件系统就是“怎么把数据存到磁盘上”。这个理解没错,但它只看到了最底层的那部分。实际上一套现代操作系统能同时挂载ext4、XFS、NFS、tmpfs、procfs这么多种文件系统,靠的是它们上面还压着一层VFS(Virtual File System,虚拟文件系统)。
VFS的作用可以类比成一个公司的总机前台:所有进程发起的文件操作——open、read、write、close、stat,首先碰到的是VFS,而不是ext4或者XFS。VFS把这堆系统调用统一翻译成结构化的请求,再根据文件所在挂载点,分发给对应的具体文件系统去处理。进程根本不用关心底下的数据到底存在本地磁盘、远端服务器、还是内存里。
这层抽象带来的价值非常大:上层应用程序写代码只需要面对open/read/write这些POSIX接口,底层存储介质无论怎么换,上层的代码一行都不用改。 你在Linux上挂载一个U盘(vfat)、一个云盘(通过FUSE/NFS)、一个内存盘(tmpfs),对应用程序来说都是“一个目录”,打开文件的方式完全一样。
1.2 四个核心对象:超级块、inode、dentry、file
VFS内部有四个核心对象,搞明白这四个对象,后面理解FUSE的工作方式基本就无障碍了。
- 超级块(super_block):可以理解成整个文件系统的“档案室总目录”。它记录了这个文件系统的总量、剩余空间、挂载状态、根目录的inode编号、文件系统类型等“全局级”信息。每个文件系统实例在挂载时都会创建一个对应的超级块对象。
- 索引节点(inode):这是一个文件或目录的“身份档案袋”。它记录了文件的元数据——大小、权限、属主、时间戳、数据块位置等。注意,inode里面不存文件名,文件名是目录项的事。
- 目录项(dentry):dentry的作用是把文件名和inode串起来。它是“档案袋上的标签”,记录了文件名到inode的映射关系。每个路径分量都会有一个dentry,比如
/home/user/a.txt会经过/、home、user、a.txt这四层dentry解析。dentry会被缓存,这就是为什么反复访问同一个路径比首次访问快很多。 - 文件对象(file):这是进程视角的东西,可以理解成“读者手里的阅览单”。记录了文件当前读写位置、打开方式(只读/只写/追加)、进程对该文件持有的引用。同一个inode可以被多个进程同时打开,各自有独立的file对象,但共享同一个inode的元数据。
用图书馆打个比方:超级块是整栋楼的总览图,inode是每一本书的登记卡,dentry是书架侧面的图书分类标签,file是你今天坐在这本书面前、正翻到第几页的状态。进程每次open一个文件,就会拿到一个新的file对象,它一路通过VFS找到dentry,再由dentry找到inode,最后由inode定位到实际的数据存储位置。
1.3 一次read()系统调用穿过哪些层
从系统调用到磁盘数据,完整路径可以简化为:
code复制进程 → 系统调用read() → VFS → 具体文件系统(ext4/FUSE)→ 块设备层(Block Layer)→ 磁盘驱动 → 物理存储
对于FUSE来说,路径在“具体文件系统”这一步就拐了个弯:VFS把请求交给FUSE内核模块,FUSE模块把请求伪装成一次设备读写,写入 /dev/fuse 这个设备文件,然后用户态的文件系统守护进程从 /dev/fuse 里读出这个请求,执行实际逻辑,再把结果写回。对上层VFS来说,FUSE内核模块就是一个普通的文件系统;对用户态进程来说,FUSE又是一组可以读写的文件描述符协议。 这就是“用户在用户态写文件系统”之所以能成立的根本原因。
注意:这套原理不是纸上谈兵。你排查一切FUSE问题时的第一反应应该是“问题出在哪一层”——应用层、用户态FUSE守护进程、/dev/fuse设备的读写、内核FUSE模块、VFS、还是底层存储。这个排查链路之后调试章节还会反复用到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么用户态文件系统需要FUSE:从内核态到用户态的破局
2.1 传统文件系统开发的“高墙”
在FUSE出现之前,想在Linux上做一个自定义文件系统,意味着你要做下面这件恐怖的事:写一个Linux内核模块,实现超级块操作、inode操作、文件操作这一大套VFS规定好的回调函数。这还不是最难的,难的是调试——内核态代码一旦出错就是oops甚至整机挂掉,没有core dump可以分析,没有gdb可以断点,printk打日志要反复重新编译内核模块、重挂载。
除了调试难,还要面对内核API版本兼容问题。Linux内核从2.x到6.x,很多VFS接口内部结构体变化非常大。一个文件系统模块在5.15内核上编译通过,放到6.1内核上可能一堆编译错误。这就导致长期以来“玩文件系统的都是内核开发老手”,普通应用层工程师根本不敢碰。
FUSE把这个墙彻底拆了。它把文件系统的大部分逻辑移到了用户态,开发者只需要写一个普通的应用程序,用C、Python、Go、Rust都行,只要这个程序能读 /dev/fuse、能按FUSE协议响应请求,它就是一个完整的文件系统。内核侧只保留一个很小的通用模块,负责把VFS请求转发给用户态。
2.2 FUSE的架构:内核模块、/dev/fuse与用户态守护进程
FUSE整体分三块:
- 内核模块(fuse.ko):负责在VFS层注册成一个文件系统驱动,并在挂载时创建一个
/dev/fuse设备节点。它内部维护了请
