本来是想把整个 Linux IO 栈都大概整理一遍,限于工作繁忙,也只是把 VFS 往下一点的流程粗略翻了遍
下面会做一些简单的总结,由于说来话长,我不打算把每一处都说的特别详尽
毕竟(优质的)代码才是最好的文档

源码和注释
原始的记录都在这:RTFSC/linux/fs at master · Caturra000/RTFSC · GitHub
分类不一定很准确,像 block mapping 和 elevator 我也放到 fs 下了
mount
在 mount 前,内核其实已经注册好对应的文件系统信息 file_system_type
因此给定一个字符串字面值 fstype 也可以在全局的 file_systems 链表里遍历得到 file_system_type
这个 file_system_type 关键用途就是 mount 回调(type->mount),通过这个回调可以得到 vfsmount
这个 mount 回调一般会干这些事情:
- 通过
dev_name(/dev/xx)得到block_device - 通过
test和set回调得到super_block,比较设备的参数来得知有没有对应的super_block,没有就构造:super_block的构造有具体文件系统的fill_super给出,并插入到全局super_blocks链表 super_block实例已经有(struct dentry*)s_root,这是一个以后放入vfsmount的dentry
vfsmount 可以认为是三元组 <dentry*, super_block*, flags>:
dentry是个庞大的话题,在这里可以相当于一个组件,可在以后用于寻找挂载后的root分量super_block是具体文件系统和虚拟文件系统的开端,可以通过它来构造出整个庞大的流程flags就是用于一些特殊的策略,这是多数系统调用都用的套路
得到的 vfsmount 就插入到全局目录树(namespace’s mount tree)中,也就是说,挂载点维护于一棵树中
open
open 以一句话来说就是通过字符串求出打开文件实例,并返回给用户空间一个与打开文件实例相关联的 fd
说着容易,但其实内部实现特别恶心,这里简单描述一下不带 O_CREAT 的 open 流程:
- 构造
open_flags,这些都是影响open流程的状态机参数 - 从用户空间拷贝
filename到内核空间,其内核实例就是struct filename - 分配
fd - 初始化表示路径查找上下文的
struct nameidata,这是存储多个分量查找/解析过程的临时类型 - 启用 rcu-walk,先说疗效,就是得到打开文件
struct file fsnotifyfd和file关联
第 5 点是关键操作
由于要处理众多问题,比如:
open时的mode和flag- 跟踪符号链接
.以及..和//////////- 根本就没有目录
mount路径跳转
等等麻烦问题,而且还要用上 RCU 操作,因此变得非常复杂
这里展开说一下第 5 点最为简略的过程
- 获得空分配的 file 实例
- 初始化查找路径,就是构造 nd,更细点就是构造
<root, path, inode>三元组,携带上前面提到的 open_flags 状态机 - 循环处理分量,或者说名字解析,换到代码来看就是从一个
filename实例(靠 nd 上下文的更新)转为最终分量对应的dentry- 这个找
dentry的过程是最重要的优化了,内核实现分 2 种方法:fast lookup和slow lookup fast lookup:dentry是放在dcache(大概这名字)一个大的缓存块里面,因此可以尝试 hash(以最原始的 name 为 key 做 hash)直接得到已经在 cache 里的dentry,找不到再看 5.3.3
- 这个找
- 打开文件的
vfs_open执行,前面得到的信息想办法填充到打开文件file实例中,这里也会回调具体文件系统的f_op->open,干的事情也应该是填充 file 实例
read
read 其实是我 VFS 入门时第一个看的实现流程
read 需要用到上述 open 给出的 fd,在 open 阶段,内核为每一个 fd 都对应地构造一个内部使用的打开文件示例 struct file
而 read 会进一步封装为 struct fd,其实也没差,fd.file 就是 struct file
开始阅读前,先给出一些前置技能:
(struct file_operations*) f_op:存放于file中的一个字段,由具体文件系统提供给 VFS,read()流程用到它内部的f_op->read_iter函数指针执行读流程(struct address_space*) f_mapping:可以认为是page cache,IO 的基本优化手段,内部数据结构是radix tree/xarray(视内核版本而定),用于维护pagesstruct page:mm模块下非常复杂的结构体,我们只需要知道fs模块下需要关注的点就可以了:比如前面提到的page cache,就是用来维护若干个page用于加速寻址,但是相邻的page之间也是可以通过链表来寻址完成,也就是说,如果是page cache的话会有两种数据结构来同时维护;而page本身就存放着read()所期望获取到的数据,具体以后细说
整体流程如下:
- 获得参数
fd对应的(struct fd) f/(struct file) f.file - 获得当前的
(loff_t) pos,用于以后得到数据的字节大小后更新偏移 - 调用具体文件系统注册的
f_op->read_iter- 获取之前的
read ahead预读信息 - 往
page cache查找page(包含 read page 或者 get cached-page) - cache this page
- page up-to-date?
- copy to user
- loop, goto step 3.1
- 获取之前的
- 步骤 3 会得到整体的字节大小,依此更新
pos
这里需要补充步骤 2:
- read page 认为是
page cache里找不到page执行的过程,而 get cached-page 就是一个 cache hit 的过程 - 不管是 read page 还是 get cached-page,都需要执行 read ahead 预读
- 如果被 read ahead 流程认为需要真的往外存去读,则会执行
readpage/readpages(我后面再细说),否则,可以返回了
PS. 关于预读可以参考我以往的文章(浅谈 Linux Kernel 的预读算法)
readpage / readpages 的差异在于你要读单个 page 还是多个 page,接口来自于 (struct address_space_operations*) a_ops->readpage[s],至于为什么要写两个接口,那自然是 kernel 认为,readpages 比 for 循环多次 readpage 更具有优化的潜能,比如合并一些操作
很显然它们来自于具体文件系统给出的回调
其中 readpages 的一个通用实现为 mpage_readpages()
从 mpage_readpages() 开始,会逐步接触到 struct bio 和 struct buffer_head,历史上它们和 page / page cache 是有各种微妙的关系
先给出两个结构体的整体印象:
bio:IO 的基本单位,一次 IO 就是一个bio(submit_bio()),它是允许 scatter-IO 的(通过struct bio_vec)buffer_head:用于提取 block 映射关系(通过get_block())、跟踪页的状态(map_bh())、封装bio的提交(submit_bh(),出于历史原因,最终也会走到submit_bio())
而 readpages 流程如下:
- 假定待读入的页面集合为
pages,对于每一个page执行循环 step 2 - 把
page插入到f_mapping和page->lru中(对于 page cache 虽然是插入了 page,但其实只是填充了page自身关于index的信息) - 把
page转为按block处理,整个readpages会尽可能贪心地只用一个bio来处理,一旦发生confused现象(如不可合并、page状态不符合预期等),就把当前循环流程提交 IO 上去,更换bio。其过程需要一个buffer_head通过get_block回调来获取实际的b_blocknr - 完成循环,如有剩余
bio,再做出一次提交
提交 bh/bio 通过 generic_make_request() 转换为 request,这里会进入到 elevator 层
elevator 同样需要各种回调来描述电梯调度算法,比如 deadline,目的就是为了符合预期 IO 行为,对延迟和吞吐的一些需求做调整
// TODO 这里还没说到具体文件系统的 get_block 行为,以及 vfs inode 分配,super_block 到 inode 和 bh->b_data 的关联等等,我打一会游戏再更新吧
域名迁移,网址会自动跳转
如果没有响应,请点击这里