编写简单文件系统
1. 前言
文件系统是操作系统向用户提供一套存取数据的抽象数据结构,方便用户管理一组数据。文件系统在Linux操作系统中的位置在下图红框中标出,如Ext2、Ext4等。而在windows中现在常用的文件系统为NTFS、exFAT等,想必大家在格式化U盘、硬盘的时候就经常见到了。
为什么要用文件系统来存取数据呢?是为了图个方便。试想如果没有文件系统,放置在存储介质(硬盘)中的数据将是一个庞大的数据主体,无法分辨一个数据从哪里停止,下一个数据又从哪里开始。通过将数据分为一块一块的,并为每一块都赋予一个名字,数据将会很容易隔离和确定。当然这都是在逻辑上去划分。既然是在逻辑上划分,那总得有个依据,将划分的结果落实下来。这时候我们就需要创建一系列的数据结构(包含数据和对此数据的一系列操作),来表示我们划分的逻辑,这就是文件系统。
2. 如何编写文件系统
编写文件系统涉及一些基本数据结构。需要建立一个结构,4个操作表,如下所示。
- 文件系统类型结构(file_system_type);
- 超级块操作表(super_operations);
- 索引结点操作表(inode_operations>;
- 页缓冲区表(address_space_operations);
- 文件操作表(file_operations)。
以上基本数据结构和操作函数,贯穿了整个文件系统的主要过程,下面具体分析这几个结构和文件系统实现的要点。
一个通常意义上的文件系统驱动可以单独被编译成模块动态加载,也可以被直接编译到内核中,为了调试的方便,本文中的文件系统采用动态加载的方式实现。实现一个文件系统必须遵照内核的一些“规则”,以下我将以递进的顺序阐述文件系统的实现过程。
文件系统既然基于可加载内核模块,自然也需要实现module_init以及mocule_exit,就从module_init函数开始入手。
首先,必须建立一个文件系统类型(file_system_type)来描述文件系统,它含有文件系统的名称、类型标志以及get_sb()等操作。当安装文件系统时,系统会对该文件系统进行注册,即填充file_system_type结构,然后调用get_sb()函数来建立该文件系统的超级块。
对于特定的文件系统, 该文件系统的所有的superblock 都存在于file_sytem_type中的fs_supers链表中,而所有的文件系统,都存在于file_systems链表中。通过调用register_filesystem接口来注册文件系统,将一个新的文件系统类型加入到链表中。
int register_filesystem(struct file_system_type * fs)
注册成功以后,需要对文件系统进行挂载,因为是基于内存的文件系统,没有实际的磁盘,无法使用命令进行挂载,所以在模块初始化的时候使用内核函数kern_mount进行挂载。挂载主要完成的任务是调用file_system_type中的 mount方法,通过该方法获取该文件系统的根目录dentry,同时也获取super_block.。file_system_type的mount方法kernel也提供了已经实现的函数:mount_single,mount_pseudo等。
接下来创建若干文件和目录,用于后面进行读写操作。创建文件和目录会在向内核申请inode、dentry结构体,并且对其中的主要成员变量进行初始化。
最后就是实现文件的打开、读、写操作。通过使用内核中提供的环形缓冲区kfifo,实现对文件的读写操作。
3. 初始化内核模块
文件系统内核模块一共做了三件事情,首先是使用内核函数register_filesystem注册自定义文件系统的类型到内核,也就是把file_system_type结构体注册到内核。
其中包含了执行mount时候的回调函数myfs_get_sb。
然后通过kern_mount函数安装一次。最后创建两个文件夹,每个文件夹里面创建两个文件。
为什么要在内核模块初始化的时候mount?这是基于内存的文件系统的特殊之处,这种没有具体设备的文件系统不能直接通过mount命令安装,而是先由系统内核在内核模块初始化时自动的通过一个函数kern_mount安装一次,这个时候文件系统已经安装到了/proc节点下。使用cat /proc/filesystems查看:
然后再使用mount命令进行重安装,指定挂载的目录。
kern_mount安装成功以后就会返回一个vfsmount结构体,该结构体与mount结构体结合作为一个设备和目录节点的连接件。定义如下:
mnt_root指针指向所安装设备的dentry数据结构,mnt_sb指向所安装设备的超级块数据结构。
mnt_mountpoint指向安装点的dentry数据结构,这样就在安装目录和设备根目录二者直接搭起了一座桥梁。
内核模块初始化代码如下:
4. 创建目录和文件
创建目录的函数如下:
该函数指定了创建的是目录和用户权限,然后调用了myfs_creat_file。
data表示inode的私有数据,fops为文件操作集,这里创建的是目录赋值为空。
紧接着调用myfs_creat_by_name函数创建目录或者普通文件。
该函数主要判断是否指定了父目录,如果没有就使用文件系统根目录,然后根据名字去查找是否已经存在该目录项,找不到就创建一个目录项,然后根据传进来的标志判断该目录项是指向普通文件还是目录。分别调用myfs_mkdir和myfs_creat函数。接下来看myfs_mkdir。
该函数调用myfs_mknod把创建的inode和dentry连接起来,并且增加根目录引用计数。
myfs_mknod调用myfs_get_inode创建inode,并且关联inode和dentry。
myfs_get_inode完成了创建并且初始化一个inode的功能。
至此,创建目录的流程就结束了,创建文件同理,只不过传入的标志不同而已。
5. 实现文件操作集
前面使用myfs_creat_file创建普通文件,会传入一个myfs_file_operations结构体,里面定义了对该文件系统中文件的操作函数。
三个操作函数的实现如下:
由于没有具体的设备,读写借用了内存中的环形缓冲区kfifo。