Linux 块设备驱动 (3)

本文深入探讨Linux块设备驱动和文件IO的工作机制,通过ftrace分析open、write、close系统调用,特别关注fadvise64的POSIX_FADV_DONTNEED如何回写页缓存和清除页缓存。通过对实验环境的fio测试,展示了函数图和火焰图,揭示了文件操作的时间差异和页缓存管理的复杂流程。
摘要由CSDN通过智能技术生成

1. 背景

Linux Block Driver - 2 中,我们在 Sampleblk 驱动创建了 Ext4 文件系统,并做了一个简单的 fio 测试。

本文将继续之前的实验,围绕这个简单的 fio 测试,探究 Linux 块设备驱动和文件 IO 的运作机制。除非特别指明,本文中所有 Linux 内核源码引用都基于 4.6.0。其它内核版本可能会有较大差异。若需对 Sampleblk 块驱动实现有所了解,请参考 Linux Block Driver - 1

2. 准备

阅读本文前,可能需要如下准备工作,

在上篇文章中,通过使用 Flamegraph,我们把 fio 测试中做的 perf profiling 的结果可视化,生成了如下火焰图,

在新窗口中打开图片,我们可以看到,火焰图不但可以帮助我们理解 CPU 全局资源的占用情况,而且还能进一步分析到微观和细节。例如局部的热锁,父子函数的调用关系,和所占 CPU 时间比例。关于进一步的 Flamegraph 的介绍和资料,请参考 Brenden Gregg 的 Flamegraph 相关资源

本文中,该火焰图将成为我们粗略了解该 fio 测试在 Linux 4.6.0 内核涉及到的文件 IO 内部实现的主要工具。

3. 深入理解文件 IO

在上一篇文章中,我们发现,尽管测试的主要时间都发生在 write 系统调用上。但如果查看单次系统调用的时间,fadvise64 远远超过了 write。为什么 writefadvise64 调用的执行时间差异如此之大?如果对 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 工具来了解 writefadvise64 调用的执行时间差异。

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 系统调用打开普通文件时,并没有调用块设备驱动的代码,而只涉及到下面两个层次的处理,

  1. VFS 层。

    VFS 层的 open 系统调用代码为进程分配 fd,根据文件名查找元数据,为文件分配和初始化 struct file。在这一层的元数据查找、读取,以及文件的打开都会调用到底层具体文件系统的回调函数协助完成。最后在系统调用返回前,把 fd,和 struct file 装配到进程的 struct task_struct 上。

    要强调的是,struct file 是文件 IO 中最核心的数据结构之一,其内部包含了如下关键信息,

    • 文件路径结构。即 f_path 成员,其类型为 struct path

      内核可以直接得到 vfsmountdentry,即可唯一确定一个文件的位置。

      这个 vfsmountdentry 的初始化由 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 的具体支持。

      vfsmountdentry 都被正确得到后,就会通过 vfs_open 调用 do_dentry_open 来初始化文件操作表。最终,实际上 struct filef_op 成员来自于其对应 dentry 的对应 inodei_fop 成员。而我们知道,inode 都是调用具体文件系统的方法来分配的。例如,对 Ext4 来说,f_op 成员最终被 ext4_lookupext4_create 初始化成了 ext4_file_operations

    • 文件地址空间结构。即 f_mapping 成员,其类型为 struct address_space

      地址空间结构内部包括了文件内存映射的内存页面,和其对应的地址空间操作表 a_ops (类型为 struct address_space_operations),都在其中。

      f_op 成员类似,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值