本来是想把整个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
fsnotify
fd
和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
(视内核版本而定),用于维护pages
struct 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
同样需要各种回调来描述电梯调度算法,比如noop
,目的就是为了符合预期IO
行为,对延迟和吞吐的一些需求做调整
// TODO 这里还没说到具体文件系统的get_block
行为,以及vfs inode
分配,super_block
到inode
和bh->b_data
的关联等等,我打一会游戏再更新吧