概述
在VFS框架下编写一个文件系统是十分方便的,只需要实现VFS需要用到的接口即可,大量磁盘文件系统被加入了进来。除此之外,很多“虚拟”的文件系统也被创造出来,为内核提供了很多重要特性。本篇来梳理一下Linux文件系统的类型,以及以ext4为例来看看一个文件系统驱动是如何被实现的。
文件系统类型
再来回顾一下之前梳理的文件系统框架:
文件系统根据底层操作的硬件,基本可分为四类:
普通文件系统
传统意义的磁盘文件系统,是文件系统驱动占比最大的部分,用于访问磁盘上的数据。
内存文件系统
底层存储介质是内存的文件系统,常见的是tmpfs和ramfs。系统中/tmp目录被挂载为tmpfs,主要用于存放临时文件,系统重启即会清空。内存文件系统还在kernel启动时起到关键作用,kernel启动时各分区及关键驱动可能还没加载,此时通常先挂载一个基于内存的根文件系统initramfs,在其中完成系统初始化工作,再将rootfs切换到真正的根文件系统磁盘。
网络文件系统
nfs可以基于网络来共享本地数据或者访问远端设备的数据。nfs并不处理文件系统的实际操作,只是提供基于rpc协议的网络客户端与服务端程序。当共享本地数据时启动服务端,将收到的网络请求转换为对共享目录的操作。当访问远端设备时启动客户端,将文件读写等操作转换为网络请求发出。
特殊文件系统
Linux中的"文件",除了代表磁盘上真实的文件,还可以是对系统及硬件资源操作访问的入口。Linux用一些特殊的文件系统来管理这些文件。例如,devtmpfs用于管理设备节点文件,proc,sysfs中的文件用于访问内核运行时的一些数据结构。
关键接口
VFS框架提供了大量接口,编写一个文件系统驱动不需要全部实现,VFS也提供一些通用的接口可以直接使用。这里列出一些关键接口及其作用。
file_system_type
struct file_system_type用于描述一个文件系统类,是最先需要定义的结构,其中最重要的是实现mount接口。
- mount() , 挂载一个该类型文件系统实例时被调用,创建super_block以及根目录dentry,并初始化。
super_operations
管理文件系统整体信息的接口,主要功能是对inode资源的管理。
- alloc_inode(),在内存中分配一个inode并初始化,通常文件系统驱动会自己定义一个更大的结构并内嵌inode。
- write_inode(),将一个inode写入磁盘。
- evict_inode(),删除一个inode。
inode_operations
管理inode之间关系的接口
- lookup(),在一个inode的子inode中查找指定名字的inode。
- mkdir(),在一个inode下创建目录inode。
- rmdir(),删除一个目录inode。
- creat(),在一个inode下创建普通文件inode。
- link(),创建硬链接。
- unlink(),删除一个inode。
file_operations
对一个打开的文件进行操作的接口,大部分接口会封装成系统调用提供给应用层使用。
- read_iter(),将磁盘数据读入iov_iter。
- write_iter(),将iov_iter数据写入磁盘。
address_space_operations
磁盘上文件数据到内存的读写操作接口。
- readpage()/readpages(),将磁盘数据读到内存。
- writepage()/writepages(),将内存数据写入磁盘。
- directio(),不经过pagecache读写数据。
EXT4
第 1 扩展文件系统(ext1)是专门针对Linux设计的文件系统,也是第一个使用虚拟文件系统(VFS)的文件系统,ext系列文件系统设计之初就考虑了与VFS的配合,使其在Linux上表现出良好的性能与可靠性,被众多发行版作为默认文件系统。ext4是最新一代扩展文件系统,通过它的实现来看看一个文件系统驱动是如何编写的。
磁盘数据结构
通常所说的磁盘文件系统,其本质是一种组织管理数据的规则,目的是在一段连续的存储空间内实现目录,文件这样的树形结构。为了后续描述其实现,这里简单介绍一下ext4的磁盘数据结构,更详细的可以参看这篇文章https://www.cnblogs.com/f-ck-need-u/p/7016077.html。
结构图如下:
ext4中用inode来表示一个“文件”,“文件”的含义与在linux中一致包含普通文件,目录等7种类型。在ext4中与一个文件相关的数据有两部分,一部分是文件真实包含的数据,还一部分是其inode数据,其中包含文件大小,属性,更新时间等参数以及指向其数据块的指针。ext4文件系统创建时已经设置了inode最大数量,每个inode分配一个唯一的inode num,用于查找inode。其中一些特殊inode的inode num是固定的,如根目录inode num=2 ,日志inode num=8 。
ext4将磁盘空间划分为GROUP来管理。其中各结构功能如下:
- superblock:记录文件系统的整体信息,包含大小,inode数量,block大小等等,在多个group中有备份。
- GDT:记录所有GROUP的信息,包含GROUP的起始block位置,包含inode num范围,可用block,inode数量等,在多个group中有备份。
- Reserved GDT:GDT的保留空间,为以后扩展准备,在多个group中有备份。
- Block Bitmap:以bitmap形式记录当前GROUP中block的使用情况。
- Inode Bitmap:以bitmap形式记录当前GROUP中inode的使用情况。
- Inode Table:顺序保存当前GROUP中所有inode数据。
- Data Block:当前GROUP的数据block。
文件查找
对于ext4中的inode,唯一的索引就是它的inode num。当要访问一个文件时,如果已经知道了inode num,先根据GDT的信息找到其所在的GROUP,再在Inode Table中根据偏移找到inode数据,再按照inode中记录的数据块指针找到要访问的数据。inode的inode num,记录在其父inode即其所在目录的inode的数据块中。
假设要查找/file/1.bin的inode,流程如下:
- 先根据inode num=2找到跟目录inode
- 根据inode中的数据块指针找到其数据块,其中保存了根目录下inode的文件名和inode num,找到file目录的inode num=i1
- 根据 i1找到file目录的inode
- 读取file目录的数据块找到1.bin 的inode num=i2
- 根据 i2 找到1.bin 的inode
关键函数
ext4文件系统看似结构复杂,但其实对于文件系统的大部分操作,基本上都可以用两个步骤反复组合来完成,
- 根据inode num查找inode。
- 查找inode数据块,并读写。
这里介绍几个关键的函数。
ext4_fill_super()
在mount时被调用,这是一个超过1000行的庞大函数,super_block的数据在这里被初始化。其中会将磁盘上SUPER BLOCK 和GDT数据读到内存,以便后续使用及更新,为之后操作提供了基础(扩展阅读:EXT4分区mount状态下也可以用tune2fs修改属性吗?)。还将根目录inode读取并保存起来,作为inode查找的起点。
__ext4_iget()
根据inode num查找inode,该函数返回的是vfs的inode结构,但此时该inode已经被内嵌到ext4_inode_info结构中,其中包含了ext4 inode数据,可由EXT4_I(inode)获取。
ext4_map_blocks()
用于查找inode的数据块,查找方式是通过一个ext4_map_blocks结构。
struct ext4_map_blocks {
ext4_fsblk_t m_pblk; //起始物理块号(返回参数)
ext4_lblk_t m_lblk; //起始逻辑块号 (传入参数)
unsigned int m_len; //逻辑块数量,同时这个参数也作为返回值保存实际映射物理块的长度
unsigned int m_flags; //块状态(返回参数)
};
该函数负责查找或者创建与文件逻辑块在磁盘上对应的物理块。大致流程是,首先找磁盘上是否有已经对应的块,若没有再根据传入flag决定是否要在磁盘上分配新的块。为兼容ext2,ext3,其中有两种数据块管理方式,ext4_ext_map_blocks(extent tree),ext4_ind_map_blocks(inode,映射)。
__ext4_new_inode()
用于分配一个ext4 inode,inode结构在文件系统格式化时已经创建好了,分配大致流程是,选择一个group在其inode table中选择一个未使用的inode,初始化inode参数并建立与其相关结构的联系,更新与该inode相关的inode,group,superblock等结构的数据,并同步到磁盘。
VFS接口实现
有了这些基础函数,最后来简要看下在ext4中VFS的接口是如何被实现的。
file_system_type
ext4_mount:调用mount_bdev()通用接口,主要初始化操作在ext4_fill_super()中实现。
super_operations
ext4_alloc_inode:分配一个ext4_inode_info结构,将其中内嵌的inode结构返回。
ext4_write_inode:若使用日志由jbd2驱动完成inode同步,否则由__ext4_get_inode_loc()找到inode数据块直接更新。
inode_operations
ext4_lookup:找到当前目录inode的数据块,根据文件名在其中找到要查找的inode num,由ext4_iget()找到inode。
ext4_mkdir:由__ext4_new_inode()新建inode,并设置其inode_operations,file_operations为目录操作方法。
ext4_cread:由__ext4_new_inode()新建inode,并设置其inode_operations,file_operations为普通文件操作方法。
ext4_unlink/ext4_rmdir:删除该inode的引用关系,将inode放入orphan list。
file_operations
ext4_file_read_iter:调用通用接口generic_file_read_iter()。
ext4_file_write_iter:调用通用接口__generic_file_write_iter()。
address_space_operations
ext4_readpage:根据偏移计算出数据在文件中的起始逻辑块及长度,由ext4_map_blocks()找到磁盘上的物理块,提交IO读取。
ext4_write_begin:在pagecache中查找或分配文件映射的内存页,并由ext4_map_blocks()找到在磁盘上对应的物理块。
ext4_write_end:标识写入的page为“脏”,更新文件大小等数据。
ext4_writepage:提交IO,将pagecache内存页写入磁盘。
至此ext4驱动的主体流程就介绍完了,所有的设计都是为了高效的实现VFS需求的接口。然而ext4的驱动是相当复杂的,有太多的细节以及特性没有涉及,实际流程还是要参看具体代码。
总结
本篇主要通过ext4,简要介绍了如何编写一个磁盘文件系统驱动,核心是实现VFS需求的各种接口,接口之下可以是对磁盘,网络,内存等等资源的管理,这使得在Linux中文件系统被极大扩展,实现了丰富多样的功能。
文件系统的代码中,往往能看着这样的流程,“优先在cache中查找,查找不到再去访问实际的资源”,这种设计显著的提高了文件系统的性能,下篇来梳理下这一部分,文件系统如何与内存子系统一起管理cache。
参考资料
https://www.ibm.com/developerworks/cn/linux/l-anatomy-ext4/
https://www.cnblogs.com/f-ck-need-u/p/7016077.html
最后
以上就是柔弱发卡为你收集整理的Linux 文件系统解析(二)fs驱动实现文件系统类型关键接口EXT4总结的全部内容,希望文章能够帮你解决Linux 文件系统解析(二)fs驱动实现文件系统类型关键接口EXT4总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复