最近开始研究Linux文件系统,希望通过阅读源码,了解文件系统的设计要素、控制逻辑,最终自己能够实现出一个简易的文件系统。
经历过内核源码阅读的小伙伴,相信你在最初面对如此庞大的内核源码时,一定会有种“面对茫茫大海,无从下手”的感觉,即便幸运的找到了逻辑入口,却在繁琐的函数跳转中晕头转向,无法把握住整个控制路径上的重难点。代码阅读之后,无法上升到整体设计,提炼出实现者的出发点,对代码逻辑,常常感觉迷惑,无法洞悉代码背后的原理。
鉴于此,在研究该部分代码前,我从实现的角度出发,根据文件系统的功能往下进行推敲,提炼出文件系统的重难点,有方向性地阅读代码、印证想法、解决疑问。
挖掘出技术难点,带着问题有方向性的阅读代码,这样能够做到事半功倍,同时对整体结构的理解,更加透彻
1. 文件系统是什么
首先,我们先来考虑下“文件系统是什么?”,在早期操作系统还未出现文件系统这个概念的时候,如果程序需要存储数据,那么它必须自己实现文件存储、文件管理的代码,程序本身需要与硬盘驱动器打交道。最令人头痛的是,你存储文件的地方可能被其他程序覆盖,导致数据的丢失。
为了解决这个“各自为营,重复创造”的现象,大家提出采用统一的中间层,管理应用程序与硬盘之间的文件交互。这样做的好处就是:
应用程序不再关注文件的存储与管理,能够专注于自身的应用逻辑控制
统一的中间层知道所有文件存储的信息,能够避免文件覆盖现象的出现,进行更有效的数据管理
总结来说:
文件系统是应用程序与块设备(磁盘等)之间的桥梁,是对文件进行统一管理的中间层。
对上: 向上层用户提供读写文件的操作接口
对下: 将文件在磁盘上进行存储及有效的管理
2. 文件系统涉及什么
2.1 文件系统挂载
在Linux环境下,磁盘作为一种特殊的文件(Linux系统中一切皆文件),允许用户进行读写操作,但是如果我们想要正常使用,在其上创建文件夹,文件等,则必须经过以下几道工序:
采用某种文件系统格式化磁盘
mkfs -t ext3 /dev/sdb
为磁盘创建挂载点
mkdir /sdb_dir
挂载磁盘到指定目录下
mount /dev/sdb /sdb_dir
修改/ext/fstab配置文件,设置开机自启
/dev/sdb /sdb_dir ext3 defaults 0 0
我们可以将上面的步骤大体分为两步:
(1)磁盘格式化 : 写入超块信息,按照特有的数据布局,对磁盘进行格式化
(2)文件目录入口: 在整个系统文件目录上,为其寻一入口,以后由此进入
2.2 技术点初总结
从上面的步骤中,我们可以发现磁盘无法直接使用,首先需要经过某种文件系统的格式化,而这个格式化过程,包括注册超块信息,将磁盘划分成数据块进行管理,也就是说:文件是由多个数据块联接在一起进行表示。从这里出发,我们来思考一下,文件系统可能涉及哪些技术点:
1. 文件存储方式
应用程序直接打交道的是文件,文件是采用何种方式持久化在磁盘上呢?
如果采用数据分块的方式,数据块如何有效组织在一起,表示成一个文件?
2. 读、写、定位的实现
如何快速定位文件中一个数据块的位置?比如要查看文件某个位置的数据。
3. 元数据的管理
元数据有哪些?目录树,空闲块的维护?
4. 数据一致性保证
内存的数据与硬盘上的数据如何来往? 何时将内存中的数据固化到磁盘,以防断电等造成的数据丢失。
上面我是想到啥,说到啥,整体感觉,有些凌乱,可能会遗漏某些技术点。接下来,从文件系统的基本功能“读写文件”出发,我们来尝试挖掘出一些有用的技术点。
2.3 读写流程分析
在这里,我们尝试分析下读写逻辑,整理其控制路径,当然这里只是一个粗糙的版本,毕竟此时我们还未读代码,只是猜想其过程,同时暂且不讨论Page Cache,均采用Direct IO的方式:
(1)根据文件路径,查找目录树,获得当前文件目录项
(2)通过文件目录项,可以获得该文件起始块,文件大小,权限等信息
(3)通过文件起始块,文件偏移,以某种方式,得到目标数据块位置 (这里需要研读下如何快速定位某数据块)
(4)读整个目标数据块,数据缓存于内存当中 (是否将整个目标数据块读出来,这里不确定,有待源码印证)
(5)拷贝数据到读请求的buffer当中
(1)根据文件路径,查找目录树,获得当前文件目录项
(2)通过文件目录项,可以获得该文件起始块,文件大小,权限等信息
(3)通过文件大小,如果发现文件偏移超过文件大小,则通过“空闲块管理”获取新的数据块位置。否则,通过文件起始块,文件偏移,以某种方式,得到目标数据块位置;
(4)将数据buffer中的数据写入对应磁盘位置(这里可能使用写buffer,不知如何实现的)
2.3 技术点总结
通过上面的控制逻辑推敲,我们对文件系统想必有更加清晰的认识了,这次,我们再来总结总结,可能涉及的知识点:
正常读写流程
应用读写请求(文件描述符,位置,长度,数据buffer)
读:文件系统如何填充到buffer当中,在内核态到用户态buffer,是否会有一次拷贝
写:将buffer当中的数据以IO的方式,发送到下层
目标:
1. 读写经过的路径:系统调用 --> 文件系统 --> 下层调用接口
2. 元数据之间的交互:目录树,空闲块的使用
3. buffer中数据的来龙去脉,能观察到经过了多少次拷贝
磁盘数据固化
数据分布:超级块?目录树?空闲块?数据块? 这些如何记录在案
数据固化:数据一致性保证,目录树这些在内存中的元数据,何时固化到磁盘上?采用什么方式,保证数据在突然断电的情况下,不会丢失
目录树
查找文件的入口地址,希望维护快,查找快
数据结构:目录树采用何种数据结构进行维护:BTree?红黑树?
磁盘固化:关机之后, 如何在硬盘上进行存储?
加载方式:运行时,是将所有的目录信息均加载到内存当中,还是按需加载,缓存频繁的目录数据块?
空闲块表
记录哪些数据块还未被使用
数据结构:采用何种数据结构进行维护:Bitmap?BTree?红黑树?
猜测:构造排序二叉树(红黑树啥的),维护关键字<空闲长度,位置>,这样就能快速找到满足长度的一段联系空闲块??
磁盘固化:关机之后, 如何在硬盘上进行存储?
通过上面这些,让我们清楚认识哪些问题需要去注意,能够有方向性的去阅读代码。小伙伴们,让我们带着这些问题,去探索,阅读代码吧!!
当然每个人的关注点可能不一样,在此分享自己源码阅读的方式,希望抛砖引玉,大家相互交流。
如果你在工作学习中存在什么技术难题,请关注大数据架构师公众号:xinsz08,我们会努力帮你解答,每天1篇必读运维大牛亲笔绝密干货贴,1000万IT人的干货资源库。