大家好,这里是物联网心球。
作为一个Linux开发者,我们经常会跟文件打交道。虽然我们经常在使用文件,但是对文件的理解却不深刻,只停留在表面。笔者一直以来都很好奇,当我们执行文件操作时,内核和硬件设备都在做些什么?本文我们以ext4文件系统为例,来讲解Linux文件的底层实现原理。
1.ext4文件系统
磁盘一个大容量的存储设备,为了更高效地使用磁盘的存储空间,我们需要将磁盘划分成块(如4KB),块是磁盘存储数据的最小单位,Linux系统以块为单位来读写磁盘。

图1 ext4文件系统
ext4文件系统磁盘布局如图1所示。
当我们拿到一块新的磁盘时,我们并不能直接使用它。新的磁盘只有先分区和格式化才能使用,分区是将物理磁盘划分为逻辑区域,格式化是为分区创建文件系统,磁盘有了文件系统才能够被Linux系统识别,Linux系统才能正确访问磁盘文件。
Linux系统中常见的文件系统有:ext4、XFS、Btrfs、ZFS、F2FS等。其中ext4文件系统使用最为广泛。
ext4文件系统将分区划分为多个块组,块组默认大小为128MB(包含32768个4KB块),块组结构如图2所示。

图2 ext4文件系统块组
块组各部分解析如下:
- 超级块(Super Block):超级块存储了文件系统的关键参数,如块大小、inode数量、文件系统大小等元数据,对应数据结构为struct ext4_super_block。
- 组描述符表(GDT):记录每个块组的使用情况,对应数据结构为struct ext4_group_desc。
- 预留GDT块:预留空间,用于文件系统未来扩展。
- inode位图:标记哪些inode已使用(1表示使用,0表示空闲)。
- 数据位图:标记哪些数据块已使用(1表示使用,0表示空闲)。
- inode表:存储所有struct ext4_inode结构,ext4_inode表示一个文件或目录。
- 数据块:存储文件或目录内容的区域。
目录中的内容记录在块中,并通过HTree树来管理。文件的内容也同样记录在块中,并通过extents树来管理。限于篇幅,这部分内容本文不展开讲解。
2.Linux文件树
Linux系统以文件树来管理文件,在Linux用户眼中,文件树样式如图3所示。

图3 Linux用户眼中的文件树
Linux文件树是以根目录(/)为起点的单根树形目录结构,所有文件和子目录都从根目录延伸,形成一个层次化的文件系统组织。树形结构使得用户能够方便地浏览、查找和管理文件系统中的资源。同时,树形结构也很方便挂载新文件系统。
对于Linux内核来说,文件树由各种挂载实例(struct mount)组成,如图4所示。

图4 Linux内核眼中的文件树
在学习VFS(Virtual File System,虚拟文件系统)时,我们经常会围绕dentry(目录项)来理解Linux文件树。这种方式会让我们产生一个误解:Linux系统中所有的文件(相同或不同文件系统中的文件)都是通过dentry连接起来的。事实真是这样吗?接下来我们来揭晓答案。
struct mount结构体定义如下(简化版):
struct mount {
struct hlist_node mnt_hash;
struct mount *mnt_parent;
struct dentry *mnt_mountpoint;
struct vfsmount mnt;
union {
struct rcu_head mnt_rcu;
struct llist_node mnt_llist;
};
const char *mnt_devname;
......
};
关键字段解析如下:
-
mnt_hash:将struct mount实例链接到全局散列表mount_hashtable中,用于快速查找挂载点。
-
mnt_parent:指向父挂载实例,形成挂载树的父子关系。 -
mnt_mountpoint:指向挂载点的目录项(dentry),表示该挂载实例的挂载位置。 -
mnt:嵌入的vfsmount结构体实例,struct vsfmount结构体定义如下:
cppstruct vfsmount { struct dentry *mnt_root; struct super_block *mnt_sb; int mnt_flags; struct mnt_idmap *mnt_idmap; }; -
mnt_root:指向挂载文件系统的根目录项(dentry),是访问整个文件系统的起点。当用户访问挂载点时,内核通过此指针定位到文件系统的根目录。
-
mnt_sb:指向文件系统的超级块(如ext4),包含文件系统的元数据信息。 -
mnt_devname:存储设备名称(如
/dev/sda1),标识挂载的物理设备。
从图4可知,Linux系统中每个挂载实例都是独立的,每个挂载实例内部都有一个私有文件树(内部文件树),挂载实例的mnt_mountpoint成员为私有文件树的根目录(dentry)。挂载实例之间呈父子关系,Linux系统根目录(/)所处的挂载实例为祖先挂载实例,挂载实例之间通过挂载点(父挂载实例中的一个目录(dentry))连接。
注意:各个挂载实例的私有文件树并没有直接通过dentry关联,私有文件树通过挂载实例和挂载点间接关联。
私有文件树由dentry构成,struct dentry结构体定义如下(简化版):
struct dentry {
struct hlist_bl_node d_hash;
struct dentry *d_parent;
struct qstr d_name;
struct inode *d_inode;
const struct dentry_operations *d_op;
struct super_block *d_sb;
struct list_head d_child;
struct list_head d_subdirs;
......
};
- d_hash:
哈希表节点,用于快速查找目录项,加速路径解析。 - d_parent:
指向父目录的目录项,形成文件系统层次结构。 d_name:存储目录项名称(文件名或目录名)。d_inode:指向关联的inode,表示该目录项对应的文件或目录,``NULL``表示负目录项(不存在的文件)。d_op:指向目录项操作函数表。d_sb:指向文件系统超级块。d_child:同父目录下的兄弟节点链表。d_subdirs:子目录链表,指向该目录下的所有子目录。
通过d_parent和d_subdirs成员就能够构建文件系统树形结构。
为了方便查找挂载实例,Linux系统会通过挂载点文件路径和挂载实例计算哈希值,再将挂载实例插入全局挂载哈希表(mount_hashtable)。
3.挂载ext4文件系统
挂载是指将硬件设备的文件系统和Linux系统中的文件系统,通过指定目录(挂载点)进行关联。
图5 ext4文件系统挂载
Linux挂载文件系统过程如图5所示,我们可以用一句话来概括Linux挂载操作:内核创建并初始化一个新挂载实例,并将新挂载实例关联父挂载实例。
用户程序执行挂载命令(mount)后,内核需要根据mount命令传入的设备文件名(如/dev/sda1)打开块设备,并读取ext4文件系统超级块(struct ext4_super_block),为了兼容VFS,内核需要创建一个struct super_block对象并包含ext4超级块。同时,内核也会打开ext4文件系统根目录,作为私有文件树的根目录。
接着,内核开始执行挂载操作,内核会创建一个新挂载实例并初始化,初始化工作主要包括:
-
设置超级块:新挂载实例mnt_sb成员指向super_block对象,挂载实例和超级块进行绑定。
-
设置根目录:读取ext4文件系统根目录信息,创建dentry和inode,dentry关联inode并记录在新挂载实例mnt_root成员。
-
设置父挂载实例:新挂载实例mnt_parent指向父挂载实例,二者呈父子关系。
-
设置挂载点:新挂载实例mnt_mountpoint指向挂载点dentry,新挂载实例和挂载点进行绑定。
-
新挂载实例插入挂载哈希表:通过挂载点文件路径和挂载实例计算哈希值,将新挂载实例插入哈希表。
完成上述工作后,ext4文件系统就成功挂载至Linux文件树了。注意,ext4文件系统文件树并没有直接关联Linux文件树。当我们需要访问ext4文件系统时,先要查找挂载哈希表找到挂载实例,再基于挂载实例根目录访问文件系统。
4. 文件路径解析
文件系统挂载至Linux文件树后,我们就能够通过Linux文件树访问文件系统中的文件了。前面做的所有工作,就是为了我们能够顺利地访问文件系统中的文件。
图6 Linux打开文件过程
访问文件需要进行文件路径解析,我们以打开文件为例来讲解该过程,如图6所示。
用户程序调用open函数创建和打开文件时,需要传入文件路径,文件路径就像一个地图,能够帮助我们在复杂的文件树中找到目的文件。
文件路径可以是绝对路径和相对路径,二者的区别在于文件路径解析的起点(dentry)不一样。内核解析文件路径时,需要借助struct path结构动态记录当前目录的dentry以及其所处的挂载实例。每解析一层目录都会更新一次path。解析过程中如果碰到挂载点,需要根据挂载点文件路径计算哈希值查找挂载哈希表获取子挂载实例,path结构的mnt成员更新为子挂载实例,dentry成员更新为子挂载实例mnt_root成员(根目录dentry)。更新完后,从新挂载实例根目录继续解析文件路径。
当解析至目标文件(文件或目录)的父目录dentry后,文件路径解析工作基本就完成了。创建和打开文件的工作将由父目录的inode完成。内核会查询父目录是否存在目标文件(test.txt)。如果目标文件已经存在,则不会创建文件;否则,内核会创建并打开目标文件,创建和打开文件可以理解为:创建一个新的dentry和inode,dentry关联inode并插入文件树。当然,内核需要将这些信息同步至ext4文件系统,保持二者之间信息对称。
完成上述工作,用户程序还不能读写文件,用户程序读写文件需要用到文件描述符(fd)。内核会创建一个struct file对象,并将inode中的关键信息复制到file对象。然后申请一个未使用的fd,新fd关联file对象并记录在进程已打开文件表。最后,内核返回fd给用户程序,用户程序通过fd就能读写文件了。
总结:
Linux一切皆文件,Linux文件系统架构非常复杂,希望本文能够帮助你进一步了解Linux文件。