Linux文件系统架构总结
提要
文件系统作为内核中重要组成部分,本身职责特殊(驱动模型是离不开debugfs,procfs,sysfs这些内存文件系统的)。而且从这里开始内核空间和用户空间开始有了实际的交集。所以对于理解内核必不可少,借此文章对自己所学做一个总结。
作为内核爱好者,一方面想分享自己所学的感悟。另一方面作为跨行而来的探索者,想通过本系列文章获得业内人士认可,如果有岗位需求的,请联系文尾微信。
整体架构流程
我从希望从三个根本角度来看而不是先说一大堆专业术语和代码讲解太过突兀,设想用户读一个文件的流程总共就三件大事。
1.用路径找到文件。
2.通过磁盘获取数据和pagecache缓存交互。
3.数据copy入用户指定的内存。
那我们就从这三个角度看内核是如何实现的。涉及哪些功能概念以及如何进行的初始化。
1. 文件路径
在这个阶段起到核心作用的就是struct dentry这个结构,可以简单地设想一下,一个path有/xx/xx/xx/这种路径,那么每一截/xx/都映射一个struct dentry。
struct dentry {
。。。。
struct dentry *d_parent; /* parent directory */
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
有父节点,有兄弟节点,有子节点就是一个树状结构。对应一个文件目录树。
如图所示:
图1:文件系统整体初始化,和路径结构(vfs的主要内容)
1.1 路径系统的初始化
一切的起点 vfs_caches_init(),其实路径这个大白话的原型就是vfs,vfs抽象出了所有类型文件系统的公共部分,毕竟所有文件系统都需要实现路径查询,增删改查某个路径,文件目录,以及某个具体的文件,磁盘挂载等功能。
其中这些所有操作都围绕路径这个概念为基石,所以我更愿意用路径这个概念来阐述这一块。
vfs_caches_init()本身做的事情有个:
- 申请struct dentry,inode(每个dentry有一个自己的inode保存文件系统的操作属性),file的缓存空间。以及mount相关结构的slab内存。至于这些slab内存的左右 1.2 章节会说。
- 初始化rootfs,根文件系统用于前期重要设备驱动的初始化。
- sysfs,debugfs初始化,这些都是内存文件系统没有实际存储设备支持。都是设备驱动模型的重要组成部分。
1.2 路径生成
首先看文件相关系统调用函数相关代码:
static inline long ksys_mkdir(const char __user *pathname, umode_t mode){
。。。
dentry = user_path_create(dfd, pathname, &path, lookup_flags);
}
static inline long ksys_mknod(const char __user *filename, umode_t mode,
unsigned int dev){
。。。
dentry = user_path_create(dfd, filename, &path, lookup_flags);
}
最后都指向user_path_create(),这个函数,代码总体很多很复杂(读起来掉了很多头发)但是做的事情最主要就三个:
- /xx/xx/xx比如这是路径,从第一个/开始每一段都有一个路径。逐级查找每一段路径是否存在。
- 如果不存在就alloc一段new dentry。(不管是查找还是申请新的dentry都和dcache有关,dcache就是一段hash list用于查找和储存dentry,inode,mnt的信息)
- 因为linux每级路径都有可能被挂载新的存储设备。所以dentry和path要记录挂载点,如果进入挂载点要重新进入其挂载路径进行新的寻路流程。
1.3 路径与挂载
那么前面说了大量路径查询都和挂载相关,那么我们这里就说说文件系统中的挂载到底做了哪些事情。
- 找到参数设定的的挂载地址,如果最终段地址未找到可以申请内存新建super block ,dentry 和对应inode。
- 通过在生成super block同时将块设备相关结构和super block进行关联,(只有有实际存储设备的文件系统类型才具有块设备,内存文件系统类型通过super block已经可以起到文件操作记得基本作用)
(由此可见,super block可以算是对块设备在vfs层的一种抽象。)挂载完成后,文件系统和真实设备之间的读写功能才被“激活”。
至于块设备结构体(struct block_device)的本身,主要是作用是:1.记录硬盘对应指针,2.记录文件io传输request 队列,3.记录文件传输每次的最大block size(最多多少个page的数据等等,这些数据也被赋值到struct inode)
所以struct block_device本身的作用也就是统合描述文件系统的功能,但是他自身并没有做事情,这里不做笔墨说太多。
2.磁盘数据和pagecahe交互的流程(read过程)
图2:文件体统读流程分析
2.1 第一种情况pagecache中存在需要数据
对应图2的(1),(2)步,每个文件都有自己的struct file结构,其中有个pos变量记录文件和pagecache的对应关系。(这里pagecache就是一组划分的内存页用于作为文件数据的缓存。)
pos和文件pagecache对应关系中已经有相关数据,则不需要继续读硬盘内容直接读pagecache中的数据返回即可。
2.2 第二种情况pagecache中没有需要的数据
对应图2的(3),(4),(5)步,在pagecache中找不到需要的数据时,准备向硬盘直接读取,这个过程需要考虑几点:
a. struct bio 对应一个内存页的内容读取,需要多个page的数据就需要多个bio进行submit_bio(bio) (关键函数)。
b. 当多个struct bio的地址是连续的时候可以进行合并成request queue,打包进行硬盘数据读取。
c. 因为多条queue队列被发送给硬盘读取相关的函数执行,也涉及并发问题,所以request queue也需要考虑硬盘驱动自身有多少硬件队列可以帮助并发排列。
取回数据后存入pos对应的pagecache,再从pagecache读取给read相关应用程序,方便下次复用。
2.3 硬盘驱动读写数据的过程
至于request queue被存入hctx队列之后(图2中有信息请查看),那么就进去硬盘驱动的范畴了,这一块内容比不少,后面我会开一篇文章单独说,这里简单说几点。
a. request queue最后读取数据本身和cpu无关,最后进入硬盘的dma管理器,所以具备异步并行的性质。
b. dma本身执行却根据优先级有顺序执行,所以数据读取到对应内存过程需要硬盘设立对应总线上的中断程序来做一个读写执行结束的信号。
小结
本文简单从read系统函数-》vfs-》实际读写io的过程,讲的比较简练,其实每个环节涉及大量细节代码。本篇目的就是让读者了解一个整体流程。如果有细节分析的需求的请联系本人。微信如下: