File system drivers
实验室目标:
- 获取有关Linux中虚拟文件系统(VFS)的知识,并了解有关“inode”,“dentry”,“file”,超级块和数据块的概念
- 了解在 VFS 中挂载文件系统的过程
- 了解各种文件系统类型,并了解具有物理支持的文件系统(在磁盘上)和没有物理支持的文件系统之间的差异
Virtual File System (VFS)
虚拟文件系统(也称为 VFS)是内核的一个组件,用于处理与文件和文件系统相关的所有系统调用
- VFS 是用户和特定文件系统之间的通用接口
- VFS 的抽象简化了文件系统的实现,并提供了多个文件系统的集成
- 文件系统的实现就通过使用 VFS 提供的 API 来完成,通用硬件和 I/O 子系统通信部分由 VFS 处理
从功能的角度来看,文件系统可以分为:
- 磁盘文件系统(ext3, ext4, xfs, fat, ntfs …… )
- 网络文件系统(nfs, smbfs/cifs, ncp …… )
- 虚拟文件系统(procfs, sysfs, sockfs, pipefs …… )
Linux 内核将 VFS 用于目录和文件的层次结构(树),使用挂载操作将新的文件系统添加为 VFS 子树
VFS 可以使用普通文件作为虚拟块设备,因此可以在普通文件上挂载磁盘文件系统,这样,可以创建文件系统堆栈
VFS 的基本思想是提供一个可以表示来自任何文件系统的文件的单个文件模型,文件系统驱动程序负责引入公分母,这样,内核就可以创建包含整个系统的单个目录结构,将有一个文件系统将成为根,其余的将挂载在其各个目录中
The general file system model
通用文件系统模型(任何实现的文件系统都需要简化为通用文件系统模型)由几个明确定义的实体组成:
- 超级块 superblock
- 超级块存储已挂载文件系统所需的信息:
- 输入和块位置
- 文件系统块大小
- 最大文件名长度
- 最大文件大小
- 根输入节点的位置
- 对于磁盘文件系统,超级块在磁盘的第一个块中有一个对应项(文件系统控制块)
- 超级块存储已挂载文件系统所需的信息:
- 索引结点 inode
- 保留有关一般意义上的文件的信息:常规文件,目录,特殊文件 (pipe,fifo),块设备,字符设备,链接,或任何可以抽象为文件的内容
- 一个索引结点存储信息:
- 文件类型
- 文件大小
- 访问权限
- 访问或修改时间
- 磁盘上数据的位置(指向包含数据的磁盘块的指针)
- 像超级块一样,每个 inodes 都有一个磁盘对应物,磁盘上的索引节点通常被分组到一个专门的区域(inode 区域,与数据块区域分开),在某些文件系统中,inode 的等效项分布在文件系统结构(FAT)中
- 文件 file
- 文件是最接近用户的文件系统模型的组件,该结构仅作为 VFS 实体存在于内存中,并且在磁盘上没有物理对应项
- 文件对象表示进程已打开的文件,维护以下信息:
- 文件光标位置
- 文件打开权限
- 指向关联 inode 的指针(最终为其索引)
- 目录项 dentry
- 目录(目录条目)将索引节点与文件名相关联
- 通常,dentry 结构包含两个字段:
- 标识 inode 的整数
- 表示其名称的字符串
这些实体是文件系统元数据(它们包含有关数据或其他元数据的信息),其中需要注意的就是 inode
和 file
从文件系统的角度来看,inode
表示文件:
inode
的属性是与文件关联的大小,权限,时间inode
唯一标识文件系统中的文件
从用户的角度来看,file
表示文件:
file
的属性是inode
,文件名,文件打开属性,文件位置- 所有打开的文件都有与之关联的
file
结构体
Register and unregister filesystems
在单个系统上,不太可能有超过 5-6 个文件系统
因此,文件系统(或者更准确地说,文件系统类型)作为模块实现,并且可以随时加载或卸载
- 描述特定文件系统的结构是
file_system_type
:
1 | struct file_system_type { |
为了能够动态加载/卸载文件系统模块,需要文件系统注册/注销的 API
将文件系统注册到内核中的操作,通常在模块初始化函数中执行,为了注册文件系统,需要完成如下的工作:
- 填充
file_system_type
结构体(至少填充:name
mount
kill_sb
fs_flags
) - 调用
register_filesystem
函数
卸载模块时,必须通过调用函数 unregister_filesystem
来注销文件系统
注册操作系统的案例如下:
1 | static struct file_system_type ramfs_fs_type = { |
挂载文件系统时,内核会调用 file_system_type->mount
,该函数会进行一组初始化并返回表示挂载点目录的 dentry 结构,最简单的做法是在 mount
中使用如下 API:
1 | struct dentry *mount_bdev(struct file_system_type *fs_type, |
- 这些函数会获取一个指针,该指针指向将在超级块初始化后调用的函数,以完成驱动程序的初始化
卸载文件系统时,内核调用 kill_sb
,它将会执行清理操作并调用以下 API 中的一个:
1 | void kill_block_super(struct super_block *sb); /* 卸载块设备上的文件系统 */ |
Superblock in VFS
超级块既作为物理实体存在(磁盘上的实体),也作为 VFS 实体存在(结构体 super_block
),超级块仅包含信息,用于从磁盘写入和读取元数据
超级块操作由以下结构描述:
1 | struct super_operations { |
有一些重要的 API 可以使用 super_operations
:
1 | struct buffer_head *__bread(struct block_device *bdev, sector_t block, unsigned size); /* 读取结构block_device中具有给定块号block和给定大小size的块buffer_head,如果成功,则返回指向buffer_head结构的指针,否则返回NULL */ |
填充超级块的一个案例如下:
1 |
|
- 内核提供了通用函数来实现文件系统结构的操作
- 例如,上述代码中使用的
generic_delete_inode
和simple_statfs
(一般都以generic
或者simple
开头)
Buffer cache
缓冲区缓存是一个内核子系统,用于处理来自块设备的缓存(读取和写入)块,缓冲区缓存使用的基本实体是 buffer_head
结构体:
1 | struct buffer_head { |
函数和有用的宏:
1 | unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size); /* 查找内存区域中的第一个零位(size参数表示搜索区域中的位数) */ |
The Inode Structure
索引节点 inode 是 UNIX 文件系统的重要组成部分,同时也是 VFS 的重要组成部分
索引节点是元数据(它具有有关信息的信息):
- 索引节点唯一标识磁盘上的文件并保存有关该文件的信息(uid、gid、访问权限、访问时间、指向数据块的指针等)
- 索引节点是指磁盘上的文件,一个 inode 可以关联任意数量的
file
结构(多个进程可以打开同一个文件,或者一个进程可以多次打开同一个文件) - 与 VFS 中的其他结构一样,它是一种通用结构,它涵盖了所有受支持的文件类型的选项,甚至包括那些没有关联磁盘实体(如 FAT)的文件类型
1 | struct inode { |
相关 API 如下:
1 | struct inode *new_inode(struct super_block *sb); /* 创建一个新的inode,初始化字段i_nlink,i_blkbits,i_sbi_dev(设置为'1') */ |
创建一个 inode:
- 通常,此函数将调用
iget_locked
从 VFS 获取 inode 结构,如果 inode 是新创建的,则需要从磁盘读取对应的超级块(使用 sb_bread)并填写有用的信息 - 例如文件系统 minix 的
minix_iget
函数 :
1 | struct inode *minix_iget(struct super_block *sb, unsigned long ino) |
minix_iget
会先调用iget_locked
来获取具有给定编号的 inode- 如果没有成功获取,程序将调用
V1_minix_iget
,进而调用minix_V1_raw_inode
来从磁盘读取输入,然后使用读取信息完成 inode
The File Structure
文件结构对应于进程打开的文件,仅存在于内存中,与 inode 相关联
1 | struct file { |
- 文件系统的文件操作
file->f_op
是使用inode->i_fop
字段初始化的,以便后续系统调用使用存储在file->f_op
中的值 - 结构体
file
中还有一个有意思的条目address_space
,值得单独分析一下(其实这个条目是由inode->i_data
进行初始化的)
Address space operations
进程的地址空间和文件之间有着密切的联系:
- 程序的执行几乎完全是通过将文件映射到进程地址空间来完成的(例如
execve
) - 由于此方法运行良好且非常通用,因此也可用于常规系统调用,如读取和写入
描述地址空间的结构是 address_space
(也被称为地址空间描述符),并且使用它的操作由结构体 address_space_operations
描述,要初始化地址空间操作,必须填写 inode->i_mapping->a_ops
结构体 address_space
用于管理 “索引结点inode” 到 “内存页面-page” 的映射:
- 一个文件对应一个
address_space
结构 - 一个
address_space
与一个偏移量能够确定page cache
或swap cache
中的一个页面 - 结构体
address_space
的条目如下:
1 | struct address_space { |
The Dentry Structure
目录项 Dentry 的主要任务是在 inode 和文件名之间建立链接,该结构的重要字段如下所示:
1 | struct dentry { |
- 内核使用 Dentry 来构建并管理文件系统的目录树
- 目录项在内核中起到了连接不同的文件对象 inode 的作用,进而起到了维护文件系统目录树的作用
Bitmap operations
使用文件系统时,管理信息(哪个 block 是空闲或忙碌,哪个 inode 是空闲或忙碌)使用位图 Bitmap 存储,为此,我们经常需要使用位运算,此类操作包括:
1 | unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size); /* 在bitmap指定范围内找到第一个zero bit的位置 */ |
下面列出了一些用法示例:
1 | unsigned int map; |
Exercises
要解决练习,您需要执行以下步骤:
- 从模板准备 skeletons
- 构建模块
- 将模块复制到虚拟机
- 启动 VM 并在 VM 中测试模块
1 | make clean |
1.myfs 完整代码:
首先,我们计划熟悉 Linux 内核和虚拟文件系统 (VFS) 组件公开的界面:
- 设计并使用一个简单的虚拟文件系统(即没有物理磁盘支持)
- 文件系统称为 myfs
1 | /* |
- 第一次写文件系统驱动,很多东西都不懂,所以很大程度上参考了答案
- 感觉我自己写的时候就是 API 操作不熟悉,在网上不一定能找到正确的 API,有些 API 有特殊的运用场景,不能随便使用
- 根据传入参数和返回值可以判断一些 API 是否符合使用场景,但后来懒得一个一个试就直接看答案了
2.minfs 完整代码:
1 | /* |
- 结果:
1 | root@qemux86:~/skels/filesystems/minfs/user |
- 感觉本实验其实就主要完成了两个工作:
register_filesystem(&minfs_fs_type)
和unregister_filesystem(&minfs_fs_type)
- 其他的操作都是对上面这两个操作的完善
- 新注册文件系统只有一个操作是需要由我们完成的:
minfs_mount
1 | static struct file_system_type minfs_fs_type = { |
- 而在
minfs_mount
我们又只需要完成用于填充超级块的函数minfs_fill_super
- 这一部分和模板差不多,使用
sb_bread
读出超级块,效验magic number
并且把超级块的信息填入minfs_sb_info
,我们需要完成minfs_ops
中的函数:
1 | static const struct super_operations minfs_ops = { |
- 另外程序用于读取 inode 的
minfs_iget
函数需要实现 - 在
minfs_iget
中:- 先是使用
iget_locked(s, ino)
从挂载的文件系统获取 inode - 然后就是对 inode 的初始化,分配
address_space_operations
- 再根据 inode 类型为其分配对应的
inode_operations
和file_operations
- 最后返回 inode
- 先是使用
1 | static const struct file_operations minfs_dir_operations = { |
- 其中又需要我们实现的函数有:
minfs_create
minfs_lookup
minfs_readdir
- 借助参考答案和多次试错,感觉大体的流程清楚了,不过细节还需要打磨