一、处理VFS对象
文件系统操作
首先,我们从标准库用来与内核通信的系统调用来研究。尽管文件操作对所有应用程序来说都属于标准功能,但是对于文件系统的操作只限于少量几个系统程序,即用于装载和卸载文件系统的mount和umount程序。
文件系统和目录树的区别
用户正常访问文件是通过目录树的路径来查找,但是从上面的表格中我们发现文件系统也有一个“路径”。原因如下:
开始的时候,设备节点不包含文件系统,此时设备节点仅提供扇区读写能力,如使用dd命令读写扇区。当使用mkfs.ext4 /dev/sda1命令将磁盘格式化之后,这块磁盘就被创建了下面这些ext4文件系统结构:
- 超级块(记录文件系统整体信息)
- inode 表(存储文件元数据)
- 数据块区域(存储实际文件内容)
此时这个/dev/sda1就被文件系统组织起来。也就是说,用户既可以从目录树访问文件,也可以从文件系统如磁盘文件系统(直接关联的磁盘设备,如/dev/sda1)访问数据。假如某个磁盘文件系统管理的是/dev/sda1磁盘,但是当我们将这个文件系统挂载到目录树的某个节点的时候,比如/mnt,那么用户就可以通过/mnt开始访问这块磁盘。
注册文件系统
文件系统的注册发生在格式化磁盘之前,内核启动时,就已经完成操作。在文件系统注册到内核时,文件系统编译为模块,或者持久编译到内核中。fs/super.c中的register_filesystem用来向内核注册文件系统。
以下结构体用来描述文件系统的结构定义:
一个文件系统不能注册两次,否则,将新的文件系统的对象至于链表末尾,这样就完成项内核注册。
1. 文件系统注册的对象
- 文件系统类型(如
ext4
):
- 内核通过
register_filesystem()
函数注册文件系统类型(如ext4
、FAT
、NTFS
等),全局仅需注册一次。- 例如:当内核支持
ext4
时,无论有多少个ext4
分区(如/dev/sda1
、/dev/sdb2
),ext4
文件系统类型仅在启动时注册一次。- 文件系统实例(如
/dev/sda1
):
- 每个磁盘设备(如
/dev/sda1
)或存储介质上的文件系统实例(如 U 盘的ext4
分区),需要通过mount
命令装载到目录树中,但无需重复注册文件系统类型。2. 为什么不能重复注册同一类型?
- 内核维护文件系统类型表:
- 内核使用全局的文件系统类型表(如
file_systems
链表)记录所有支持的文件系统类型。- 如果尝试注册已存在的类型(如
ext4
),内核会报错(如EEXIST
)。- 示例:
- 用户执行
mkfs.ext4 /dev/sda1
时,依赖已注册的ext4
文件系统类型;- 若手动调用
register_filesystem(&ext4_fs_type)
,内核会拒绝重复注册。
装载(挂载)和卸载
目录树的装载和卸载比仅仅注册文件系统复杂得多,因为后者只需要像一个链表添加对象,而前者需要对内核的内部数据结构执行很多操作。文件系统的装载由mount系统调用发起。我们需要阐明在现存目录树中装载新的文件系统必须执行的任务。还需要用于描述装载点的数据结构。
注册文件系统(Register)
- 这是静态的声明过程,类似在图书馆登记一本书的存在
- 通过
register_filesystem()
函数完成- 内核将文件系统类型(如 ext4、xfs)添加到全局链表
file_systems
- 此时文件系统还未关联任何存储设备
挂载文件系统(Mount)
- 这是动态的激活过程,类似把书从仓库放到书架上
- 通过
mount()
系统调用触发- 需要完成:
- 分配内核数据结构(vfsmount、dentry 等)
- 建立与物理存储的连接
- 更新目录树结构
- 设置访问权限和挂载标志
vfsmount结构:采用一种单一的文件系统层次结构,新的文件系统可以集成到其中,使用mount命令可以查询目录树中各种文件系统的装载情况。
vfsmount结构(文件系统层次结构,包含各种文件系统类型),具体如下:
将文件系统挂载到一个目录时,挂载点的内容被替换为即将挂载的文件系统的相对根目录的内容 ,前一个目录数据直接消失,直到新文件系统卸载才重新出现。
vfsmount结构描述一个独立文件系统的挂载信息,每个不同挂载点对应一个独立的vfsmount结构,属于同一个文件系统的所有目录和文件隶属于同一个vfsmount,该vfsmount结构对应于该文件系统顶层目录,即挂载目录。
一、生动理解
vfsmount
可以把
vfsmount
想象成文件系统的 “挂载登记卡”:
- 作用:每次把一个文件系统(如 U 盘的文件系统、光盘的文件系统)挂载到目录树某个位置(挂载点)时,内核就会创建一个
vfsmount
结构。它专门记录 “这个文件系统从哪里来”“挂载到了哪个目录” 等关键信息,就像快递点登记 “包裹从哪里来”“要放到哪个货架格子”。- 与文件系统的关联:
- 每个独立的挂载操作(如把 A 硬盘挂载到
/mnt/a
,把 B 硬盘挂载到/mnt/b
)都会生成独立的vfsmount
。内核通过这些 “登记卡” 知道:当访问/mnt/a
时,要读取 A 硬盘的文件系统;访问/mnt/b
时,要读取 B 硬盘的文件系统。- 同一个文件系统无论有多少目录,都通过同一个
vfsmount
管理。比如,把 U 盘挂载到/mnt/usb
,那么/mnt/usb
下所有文件和目录都由这个 U 盘的vfsmount
关联,内核通过它定位到 U 盘的文件系统。二、挂载点的 “覆盖” 原理
当把一个文件系统挂载到某个目录(如
/mnt
)时:
- 挂载前,
/mnt
目录下可能有自己的文件(比如原本的README.txt
)。- 挂载后,
/mnt
会 “显示” 新挂载文件系统的内容(比如光盘里的src
、libs
)。这不是原数据消失,而是被 “遮盖” 了 —— 新文件系统的内容覆盖了挂载点的访问入口。只有卸载新文件系统,原来/mnt
的内容才会重新可见。vfsmount
就是这个 “遮盖” 过程的 “管理员”,它告诉内核:“现在访问/mnt
,要读我登记的这个新文件系统的内容。”
具体mount源码如下:(新版本内核vfsmount被mount结构体代替)
文件系统之间的父子关系有上述两个成员链表表示,mnt_mounts表头是子文件系统链表的起点,而mnt_child字段则是用作该链表的链表元素。
系统中的每个vfsmount示例,通过两种途径标识,一个命名空间的所有挂载的文件系统都保存在namespace->list链表中。使用vfs的mnt_list成员作为链表元素。
超级块管理:
1. 超级块的核心作用:文件系统的 “灵魂档案”
场景类比:
想象你走进一个图书馆,想借书却发现没有任何标识、目录或管理员。这时你根本不知道书放在哪里、有多少本书、书架怎么分配。
超级块就相当于图书馆的管理员手册,它告诉内核(图书馆管理员):
- 文件系统(图书馆)的 “总容量”(总书籍数量)
- “块大小”(每个书架格子的大小)
- “inode 数量”(书籍的唯一编号总数)
- 挂载点(图书馆的入口位置)
- 其他关键参数(如权限、加密方式等)。
没有超级块会怎样?
内核无法识别文件系统,就像管理员没有手册,不知道图书馆的结构,文件系统会变成 “无法读取的乱码区”。2. 超级块的具体内容:文件系统的 “户口本”
超级块存储的核心信息包括:
- 基本属性:
- 文件系统类型(如 EXT4、NTFS)
- 块大小(如 4KB / 块)
- 总块数、已用块数、可用块数
- inode 管理:
- inode 总数、已用 inode 数、可用 inode 数
- inode 块的位置(类似书籍索引区的位置)
- 挂载信息:
- 挂载点(如
/mnt/data
)- 最后一次挂载时间、最后一次检查时间
- 错误信息:
- 文件系统是否干净卸载(避免下次启动时检查)
- 错误计数(如磁盘坏块标记)
类比:
这相当于手册里详细记录了图书馆的地址(挂载点)、每个书架的格子大小(块大小)、书籍编号范围(inode 数量)、哪些书已借出(已用块)等。3. 超级块的存储位置与备份机制
- 物理位置:
超级块通常位于文件系统的起始扇区(类似图书馆入口处的公告栏)。例如,EXT4 文件系统的超级块在第 1 块。- 备份机制:
为防止超级块损坏导致文件系统瘫痪,许多文件系统(如 EXT4)会在多个位置存储备份超级块。例如,每隔若干块就会复制一次超级块,类似图书馆在不同楼层存放管理员手册副本。
恢复场景:
当主超级块损坏时,工具(如e2fsck
)可以从备份中恢复,就像管理员丢失手册后,从其他楼层的副本中找回信息。
在挂载新的文件系统时,vfsmount并不是唯一需要在内存(由于是内存不是持久化,这也很好的说明mount挂载,系统重启后失效)中创建的结构。挂载操作开始于超级块的读取。
s_op指向一个包含了函数指针的结构体,该结构按熟悉的VFS方式,提供一个一般性的接口,用于处理超级块相关的操作。操作的实现必须由底层文件系统的代码提供。
超级块管理:file_system_type对象当中保存的read_super函数指针返回一个类型 super_block的对象。用于在内存中表示一个超级块,它是借助底层实现产生的。
1. 注册文件系统:商场的 “开业许可登记”
在操作系统里注册一个文件系统,就好比在现实中为一个商场申请开业许可。
商场场景
当你想要开一家商场时,首先要到相关部门进行登记注册,说明你要开的商场是什么类型的,比如是购物中心、批发市场还是奥特莱斯等。在这个过程中,你要提交一些关于商场的基本信息,例如商场的规模、能容纳的店铺数量、经营模式等。
操作系统场景
在操作系统中,当我们注册一个文件系统(如 ext4)时,会创建一个
file_system_type
对象。这个对象就像是商场的 “类型登记信息”,它包含了该文件系统的各种特征和操作函数指针,其中就有read_super
函数指针。这个注册过程相当于告诉操作系统:“我要使用这种类型的文件系统啦!”2. 挂载文件系统:商场选定 “开业地点”
挂载文件系统类似于商场选定一个具体的开业地点。
商场场景
商场登记好类型后,需要选择一个合适的地理位置来建造和运营。这个地点就像是文件系统的挂载点,它决定了商场在城市中的具体位置,人们可以通过这个位置找到商场并进入购物。
操作系统场景
在操作系统中,我们使用
mount
命令将文件系统挂载到一个特定的目录(挂载点)上。这一步操作触发了后续填充超级块的流程,就像商场选定地点后,开始着手准备商场内部的各项设施和管理规则。3. 调用
read_super
函数:商场的 “规划设计”当挂载文件系统时,会调用
file_system_type
对象中的read_super
函数。这个过程就像是商场的规划设计阶段。商场场景
商场选定地点后,需要聘请专业的设计师来进行规划设计。设计师会根据商场的类型(如购物中心)和场地条件,设计出商场的整体布局,包括楼层分布、店铺区域划分、通道设置等。同时,设计师还会制定一些管理规则,比如营业时间、安保措施等。
操作系统场景
read_super
函数就像是这个专业设计师,它会从存储设备(如硬盘)中读取文件系统的相关信息,并在内存中创建一个super_block
对象。这个super_block
对象就像是商场的 “规划设计蓝图”,它包含了文件系统的各种关键信息,如块大小、inode 数量、空闲块和 inode 的位图等。4. 填充超级块:商场的 “设施布置”
创建好
super_block
对象后,就需要填充其中的各项信息,这就如同商场按照规划设计蓝图进行设施布置。商场场景
商场的设计师完成规划设计后,施工团队会根据蓝图进行实际的建设和设施布置。他们会安装电梯、铺设地板、设置照明设备、划分店铺区域等。同时,还会确定商场的管理团队,制定具体的运营规则和流程。
操作系统场景
read_super
函数会根据存储设备上的文件系统信息,将各种关键数据填充到super_block
对象中。例如,它会读取文件系统的元数据,确定块的分配情况,将空闲块和已使用块的信息记录在位图中;还会统计 inode 的使用情况,将相关信息存储在super_block
中。填充完成后,super_block
对象就完整地代表了文件系统的当前状态,操作系统可以根据它来进行文件和目录的管理。
超级块super_block和挂载结构体mount的区别:
概念 | super_block | mount |
角色 | 文件系统的「元数据仓库」 | 挂载点的「运营管理中心」 |
存储内容 | 文件系统的核心配置(如块大小、inode 数量、空闲块位图等) | 挂载点路径、挂载标志、子文件系统链表等 |
生命周期 | 随文件系统存在(如磁盘分区格式化后即生成 | 随挂载操作创建,卸载时销毁 |
mount系统调用
mount系统调用的入口点是sys_mount函数,由sys_mount从用户空间复制到内核空间之后,内核将控制权转移给do_mount.
do_mount
函数的主要功能是解析用户传入的挂载相关参数,完成必要的权限检查,查找或创建相应的文件系统实例,最后将文件系统挂载到指定的挂载点上,使得用户可以通过该挂载点访问文件系统中的文件和目录。
共享子树
共享子树最核心的特征是允许挂载和卸载事件以一种自动的,可控的方式在不同的amespaces间传递(propagation)。这就意味着,在一个命名空间中挂载光盘的同时也会触发对其他namespace对同一张光盘的挂载。
在共享子树中,每个挂载点都存在一个名为传递类型(propagation type)的标记,该标记决定了一个namespace中创建或者删除的挂载点是否会传递到其他的namespaces。
共享子树有 4 种传输类型:
- MS_SHARED:该挂载点及其共享挂载、卸载事件会传递。
- MS_PRIVATE:与共享挂载相反,标记为
private
的事件不会传递到任何对等组,挂载操作默认使用此标志。 - MS_SLAVE:传输类型介于
shared
和private
之间,一个slave mount
拥有一个master
(共享对等组),且slave mount
不能将事件传递给master mount
。 - MS_UNBINDABLE:该挂载点不可绑定
在 Linux 内核中,命名空间(Namespace) 是一种强大的资源隔离机制,允许不同的进程组(如容器)看到独立的系统资源视图。结合之前讨论的共享子树(Shared Subtree)场景,我们可以用以下生活化的比喻来理解其作用:
核心作用:资源隔离与共享控制
想象有多个「平行宇宙」(命名空间),每个宇宙都有自己的「地球」(系统资源)。命名空间的作用是:
- 隔离性:每个宇宙的地球独立存在,互不干扰。
- 可控共享:通过特定规则(如共享子树的传输类型),可以让某个宇宙的「月球」(挂载点)与其他宇宙同步变化。
命名空间在挂载传播中的具体角色
1. 挂载隔离
- 默认情况下,每个命名空间有独立的挂载树。例如:
- 宇宙 A 在
/mnt
挂载了硬盘,宇宙 B 看不到这个挂载。- 宇宙 B 可以在
/mnt
挂载光驱,与宇宙 A 互不影响。2. 共享子树的「对讲机」机制
- 当两个命名空间通过
MS_SHARED
标记建立关联时:
- 宇宙 A 在
/mnt
挂载新设备,会通过「对讲机」通知宇宙 B 自动同步挂载。- 反之,宇宙 B 卸载
/mnt
时,宇宙 A 也会自动卸载。3. 控制传播方向
- MS_SLAVE:主宇宙(Master)的挂载变化会传递给从宇宙(Slave),但从宇宙的操作不会反向传播。
- 比喻:主宇宙的对讲机是「发送端」,从宇宙的对讲机是「接收端」。
命名空间的典型应用场景
1. 容器化(如 Docker)
- 每个容器运行在独立的挂载命名空间中,默认使用
MS_PRIVATE
隔离。- 通过
--mount-propagation=shared
可以让容器间共享挂载点。2. 系统服务隔离
- 不同服务(如 Web 服务器、数据库)运行在独立命名空间,避免文件系统操作相互干扰。
3. 特权分离
- 普通用户进程无法访问系统命名空间的挂载点,提升安全性。
二、 标准函数
VFS层提供的有用资源是用于读写数据的标准函数。这些操作对所有文件系统来说,在一定程度上都是相同的。如果数据所在的块是已知的,则首先查询页缓存。如果数据并未保存在其中,则向对应的块设备发出读请求。如果每个文件系统都需要实现这些操作,则会导致代码大量复制,我们应该防止这种情况的发生。
常用VFS与read/write系统调用,如vfs_read和vfs_write。
VFS(虚拟文件系统)是物理文件系统与服务之间的接口层,向下对文件系统提供标准接口,方便其他文件系统移植,向上对应用层提供标准文件操作接口,使open()、read()、write()等系统调用可以跨越各种文件系统和不同介质进行。
VFS对象以及数据结构
超级块对象 super_block
,对应已装载的文件系统;索引节点对象 inode
,对应介质上的一个文件;目录项对象 dentry
,对应一个目录项;文件对象 file
,对应由进程所打开的文件。所有定义在 linux/fs.h
。
超级块对象:用来描述整个文件系统的信息,每个具体的文件系统都有自己的超级块,所有超级块对象以双向循环链表的形式连接,超级块对象在文件系统装载时创建,保存在内存中,在文件系统卸载时它会自动删除。
索引节点对象:索引节点对象包含内核在操作文件或目录时需要的全部信息。
目录项对象:目录项对象没有对应的磁盘数据结构(三种状态:被使用、未使用、负状态)。
文件对象:文件对象表示进程已打开的文件。由 open()
系统调用创建,由 close()
系统调用删除,多个进程同时打开和操作同一对象时,存在多个对应的文件对象。
VFS层调用流程
VFS层读操作调用流程
1. 系统调用入口
- 当用户程序发起读操作时,系统调用
sys_read
被触发。2. VFS层接口调用
sys_read
会调用到VFS层的_vfs_read
接口。_vfs_read
根据文件的file_operations
结构体中的read
或read_iter
方法进行调用。3. 具体文件系统操作
- 如果文件的
file_operations
结构体中定义了read
方法,则直接调用该方法。- 如果定义了
read_iter
方法,则调用new_sync_read
函数,该函数会进一步调用read_iter
方法。- 如果两者都没有定义,则返回
EINVAL
错误。4. 通用读操作处理
new_sync_read
函数会初始化一些必要的数据结构,如iovec
、kiocb
和iov_iter
。- 然后调用
read_iter
方法进行实际的读操作。- 读操作完成后,更新文件的读取位置
*ppos
,并返回读取的字节数。5. 通用读操作实现
generic_file_read_iter
是VFS层提供的一个通用读操作实现。- 该函数会根据读取的字节数和文件的映射信息,调用
do_generic_file_read
函数进行实际的数据读取。do_generic_file_read
函数会处理数据的缓存和实际读取操作。