1. 背景
在 Linux Block Driver - 2 中,我们在 Sampleblk 驱动创建了 Ext4 文件系统,并做了一个简单的 fio 测试。
本文将继续之前的实验,围绕这个简单的 fio 测试,探究 Linux 块设备驱动和文件 IO 的运作机制。除非特别指明,本文中所有 Linux 内核源码引用都基于 4.6.0。其它内核版本可能会有较大差异。若需对 Sampleblk 块驱动实现有所了解,请参考 Linux Block Driver - 1。
2. 准备
阅读本文前,可能需要如下准备工作,
- 了解 Linux Block Driver - 2 中所提的 fio 测试的环境准备,命令运行,还有性能分析方法。
- 了解 Flamegraph 如何解读。
在上篇文章中,通过使用 Flamegraph,我们把 fio 测试中做的 perf profiling 的结果可视化,生成了如下火焰图,
在新窗口中打开图片,我们可以看到,火焰图不但可以帮助我们理解 CPU 全局资源的占用情况,而且还能进一步分析到微观和细节。例如局部的热锁,父子函数的调用关系,和所占 CPU 时间比例。关于进一步的 Flamegraph 的介绍和资料,请参考 Brenden Gregg 的 Flamegraph 相关资源。
本文中,该火焰图将成为我们粗略了解该 fio 测试在 Linux 4.6.0 内核涉及到的文件 IO 内部实现的主要工具。
3. 深入理解文件 IO
在上一篇文章中,我们发现,尽管测试的主要时间都发生在 write
系统调用上。但如果查看单次系统调用的时间,fadvise64
远远超过了 write
。为什么 write
和 fadvise64
调用的执行时间差异如此之大?如果对 Linux buffer IO 机制,文件系统 page cache 的工作原理有基本概念的话,这个问题并不难理解,
Page cache 加速了文件读写操作
一般而言,
write
系统调用虽然是同步 IO,但 IO 数据是写入 page cache 就立即返回的,因此实际开销是写内存操作,而且只写入 page cache 里,不会对块设备发起 IO 操作。应用如果需要保证数据写到磁盘上,必需在write
调用之后调用fsync
来保证文件数据在fsync
返回前把数据从 page cache 甚至硬盘的 cache 写入到磁盘介质。Flush page cache 会带来额外的开销
虽然 page cache 加速了文件系统的读写操作,但一旦需要 flush page cache,将集中产生大量的磁盘 IO 操作。磁盘 IO 操作比写 page cache 要慢很多。因此,flush page cache 非常费时而且影响性能。
由于 Linux 内核提供了强大的动态追踪 (Dynamic Trace) 能力,现在我们可以通过内核的 trace 工具来了解 write
和 fadvise64
调用的执行时间差异。
3.1 使用 Ftrace
Linux strace
只能追踪系统调用界面这层的信息。要追踪系统调用内部的机制,就需要借助 Linux 内核的 trace 工具了。Ftrace 就是非常简单易用的追踪系统调用内部实现的工具。
不过,Ftrace 的 UI 是基于 linux debugfs 的。操作起来有些繁琐。因此,我们用 Brendan Gregg 写的 funcgraph 来简化我们对 Ftrace 的使用。这个工具是基于 Ftrace 的用 bash 和 awk 写的脚本,非常容易理解和使用。关于 Brendan Gregg 的 perf-tools 的使用,请阅读 Ftrace: The hidden light switch 这篇文章。此外,Linux 源码树里的 Documentation/trace/ftrace.txt 就是极好的 Ftrace 入门材料。
3.2 open
运行 Linux Block Driver - 2 中的 fio 测试时,用 funcgraph
可以获取 open
系统调用的内核函数的函数图 (function graph),
$ sudo ./funcgraph -d 1 -p 95069 SyS_open
详细的 open
系统调用函数图日志可以查看 这里。
仔细察看函数图日志就会发现,open
系统调用打开普通文件时,并没有调用块设备驱动的代码,而只涉及到下面两个层次的处理,
VFS 层。
VFS 层的
open
系统调用代码为进程分配 fd,根据文件名查找元数据,为文件分配和初始化struct file
。在这一层的元数据查找、读取,以及文件的打开都会调用到底层具体文件系统的回调函数协助完成。最后在系统调用返回前,把 fd,和struct file
装配到进程的struct task_struct
上。要强调的是,
struct file
是文件 IO 中最核心的数据结构之一,其内部包含了如下关键信息,文件路径结构。即
f_path
成员,其类型为struct path
。内核可以直接得到
vfsmount
和dentry
,即可唯一确定一个文件的位置。这个
vfsmount
和dentry
的初始化由do_last
函数来搞定。如果文件以O_CREAT
方式打开,则最终调用lookup_open
来搞定。若无O_CREAT
标志,则由lookup_fast
来搞定。最终,dentry
要么已经有对应的inode
,这时dentry
要么在 dentry cache 里,要么需要调用lookup
方法 (Ext4 对应的ext4_lookup
方法) 从磁盘上读取出来。还有一种情况,即文件是
O_CREAT
方式打开,文件虽然不存在,也会由lookup_open
调用vfs_create
来为这个dentry
创建对应的inode
。而文件创建则需要借助具体文件系统的create
方法,对 Ext4 来说,就是ext4_create
。文件操作表结构。即
f_op
成员,其类型为struct file_operations
。文件 IO 相关的方法都定义在该结构里。文件系统通过实现该方法来完成对文件 IO 的具体支持。
当
vfsmount
和dentry
都被正确得到后,就会通过vfs_open
调用do_dentry_open
来初始化文件操作表。最终,实际上struct file
的f_op
成员来自于其对应dentry
的对应inode
的i_fop
成员。而我们知道,inode
都是调用具体文件系统的方法来分配的。例如,对 Ext4 来说,f_op
成员最终被ext4_lookup
或ext4_create
初始化成了ext4_file_operations
。文件地址空间结构。即
f_mapping
成员,其类型为struct address_space
。地址空间结构内部包括了文件内存映射的内存页面,和其对应的地址空间操作表
a_ops
(类型为struct address_space_operations
),都在其中。与
f_op
成员类似,