1. 背景

本系列文章整体脉络回顾,

  • Linux Block Driver - 1介绍了一个只有 200 行源码的 Sampleblk 块驱动的实现。
  • Linux Block Driver - 2中,在 Sampleblk 驱动创建了 Ext4 文件系统,并做了一个fio顺序写测试。 测试中我们利用 Linux 的各种跟踪工具,对这个fio测试做了一个性能个性化分析。
  • Linux Block Driver - 3中,利用 Linux 跟踪工具和 Flamegraph 来对文件系统层面上的文件 IO 内部实现,有了一个概括性的了解。
  • Linux Block Driver - 4里,在之前同样的fio顺序写测试下,分析 Sampleblk 块设备的 IO 性能特征,大小,延迟,统计分布,IOPS,吞吐等。

本文将继续之前的实验,围绕这个简单的fio测试,探究 Linux 块设备驱动的运作机制。除非特别指明,本文中所有 Linux 内核源码引用都基于 4.6.0。其它内核版本可能会有较大差异。

2. 准备

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

本文将在与前文完全相同fio测试负载下,使用blktrace在块设备层面对该测试做进一步的分析。

3. 使用 blktrace

blktrace(8)是非常方便的跟踪块设备 IO 的工具。我们可以利用这个工具来分析前几篇文章中的fio测试时的块设备 IO 情况。

首先,在fio运行时,运行blktrace来记录指定块设备上的 IO 操作,

$ sudo blktrace /dev/sampleblk1
[sudo] password for yango:

^C=== sampleblk1 ===
  CPU  0:              1168040 events,    54752 KiB data
    Total:               1168040 events (dropped 0),    54752 KiB data

退出跟踪后,IO 操作的都被记录在日志文件里。可以使用 blkparse(1)命令来解析和查看这些 IO 操作的记录。 虽然 blkparse(1) 手册给出了每个 IO 操作里的具体跟踪动作 (Trace Action) 字符的含义,但下面的表格,更近一步地包含了下面的信息,

  • Trace Action 之间的时间顺序
  • 每个blkparse的 Trace Action 对应的 Linux block tracepoints 的名字,和内核对应的 trace 函数。
  • Trace Action 是否对块设备性能有正面或者负面的影响
  • Trace Action 的额外说明,这个比 blkparse(1) 手册里的描述更贴近 Linux 实现
Order Action Linux block tracepoints Kernel trace function Perf impact Description
1 Q block:block_bio_queue trace_block_bio_queue Neutral Intent to queue a bio on a given reqeust_queue. No real requests exists yet.
2 B block:block_bio_bounce trace_block_bio_bounce Negative Pages in bio has copied to bounce buffer to avoid hardware (DMA) limits.
3 X block:block_split trace_block_split Negative Split a bio with smaller pieces due to underlying block device’s limits.
4 M block:block_bio_backmerge trace_block_bio_backmerge Positive A previously inserted request exists that ends on the boundary of where this bio begins, so IO scheduler merges them.
5 F block:block_bio_frontmerge trace_block_bio_frontmerge Positive Same as the back merge, except this i/o ends where a previously inserted requests starts.
6 S block:block_sleeprq trace_block_sleeprq Negative No available request structures were available (eg. memory pressure), so the issuer has to wait for one to be freed.
7 G block:block_getrq trace_block_getrq Neutral Allocated a free request struct successfully.
8 P block:block_plug trace_block_plug Positive I/O isn’t immediately dispatched to request_queue, instead it is held back by current process IO plug list.
9 I block:block_rq_insert trace_block_rq_insert Neutral A request is sent to the IO scheduler internal queue and later service by the driver.
10 U block:block_unplug trace_block_unplug Positive Flush queued IO request to device request_queue, could be triggered by timeout or intentionally function call.
11 A block:block_rq_remap trace_block_rq_remap Neutral Only used by stackable devices, for example, DM(Device Mapper) and raid driver.
12 D block:block_rq_issue trace_block_rq_issue Neutral Device driver code is picking up the request
13 C block:block_rq_complete trace_block_rq_complete Neutral A previously issued request has been completed. The output will detail the sector and size of that request.

如下例,我们可以利用 grep 命令,过滤blkparse解析出来的所有有关 IO 完成动作 (C Action) 的 IO 记录,

$ blkparse sampleblk1.blktrace.0   | grep C | head -n20
253,1    0       71     0.000091017 76455  C   W 2488 + 255 [0]
253,1    0       73     0.000108071 76455  C   W 2743 + 255 [0]
253,1    0       75     0.000123489 76455  C   W 2998 + 255 [0]
253,1    0       77     0.000139005 76455  C   W 3253 + 255 [0]
253,1    0       79     0.000154437 76455  C   W 3508 + 255 [0]
253,1    0       81     0.000169913 76455  C   W 3763 + 255 [0]
253,1    0       83     0.000185682 76455  C   W 4018 + 255 [0]
253,1    0       85     0.000201777 76455  C   W 4273 + 255 [0]
253,1    0       87     0.000202998 76455  C   W 4528 + 8 [0]
253,1    0       89     0.000267387 76455  C   W 4536 + 255 [0]
253,1    0       91     0.000283523 76455  C   W 4791 + 255 [0]
253,1    0       93     0.000299077 76455  C   W 5046 + 255 [0]
253,1    0       95     0.000314889 76455  C   W 5301 + 255 [0]
253,1    0       97     0.000330389 76455  C   W 5556 + 255 [0]
253,1    0       99     0.000345746 76455  C   W 5811 + 255 [0]
253,1    0      101     0.000361125 76455  C   W 6066 + 255 [0]
253,1    0      108     0.000378428 76455  C   W 6321 + 255 [0]
253,1    0      110     0.000379581 76455  C   W 6576 + 8 [0]

例如,上面的输出中,第一条记录的含义是,

它是序号为 71 的 IO 完成 (C) 操作。是进程号为 76455 的进程,在 CPU 0,对主次设备号 253,1 的块设备,发起的起始地址为 2488,长度为 255 个扇区的写 (W) 操作。 该 IO 完成 (C)时被记录,当时的时间戳是 0.000091017,是精确到纳秒级的时间戳。利用 IO 操作的时间戳,我们就可以计算两个 IO 操作之间的具体延迟数据。

上面的例子中,可以看到,前 20 条跟踪记录,恰好是一共 4096 字节的数据。本文中fio测试是 buffer IO 测试,因此,块 IO 是出现在fadvise64使用 POSIX_FADV_DONTNEED 来 flush 文件系统页缓存时的。 这时,文件系统对块设备发送的 IO 是基于 4K 页面的大小。而这些 4K 的页面,在块设备层被拆分成如上 20 个更小的块 IO 请求来发送。

4. IO 流程分析

在 blktrace 的每条记录里,都包含 IO 操作的起始扇区地址。因此,利用该起始扇区地址,可以找到针对这个地址的完整的 IO 操作过程。 前面的例子里,如果我们想找到所有起始扇区为 2488 的 IO 操作,则可以用如下办法,

$ blkparse sampleblk1.blktrace.0   | grep 2488 | head -n6
253,1    0        1     0.000000000 76455  Q   W 2488 + 2048 [fio]
253,1    0        2     0.000001750 76455  X   W 2488 / 2743 [fio]
253,1    0        4     0.000003147 76455  G   W 2488 + 255 [fio]
253,1    0       53     0.000072101 76455  I   W 2488 + 255 [fio]
253,1    0       70     0.000075621 76455  D   W 2488 + 255 [fio]
253,1    0       71     0.000091017 76455  C   W 2488 + 255 [0]

可以直观的看出,这个fio测试对起始扇区 2488 发起的 IO 操作经历了以下历程,

Q -> X -> G -> I -> D -> C

如果对照前面的 blkparse(1) 的 Trace Action 的说明表格,我们就可以很容易理解,内核在块设备层对该起始扇区做的所有 IO 操作的时序。

下面,就针对同一个起始扇区号为 2488 的 IO 操作所经历的历程,对 Linux 块 IO 流程做简要说明。

4.1 Q - bio 排队

本文中的fio测试程序由于是同步的 buffer IO 的写入,因此,在write系统调用返回时,fio的数据其实并没有真正写在块设备上,而是写到了文件系统的 page cache 里。 如前面几篇文章所述,最终的块设备的 IO 触发,是由fadvise64flush 脏的 page cache 引起的。

因此,fadvise64的系统调用会调用到 Ext4 文件系统的 page cache 写入函数,然后由 Ext4 将内存页封装成bio来提交给块设备。 和大多数块设备的提交函数一样,函数的入口是submit_bio,该函数会调用generic_make_request,随后代码进入到generic_make_request_checksbio进行检查。 在该函数结尾,通过了检查后,内核代码调用了trace_block_bio_queue来报告自己意图将bio发送到设备的队列里。 需要注意的是,此时,bio只是打算要被插入到队列里,而不是已经放在队列里。而且,这时提交的biobio->bi_next是 NULL 值,并未形成链表。

Q 操作对应的具体代码路径,请参考perf 命令对 block:block_bio_queue 的跟踪结果

100.00%   100.00%  fio      [kernel.vmlinux]  [k] generic_make_request_checks
            |
            ---generic_make_request_checks
               generic_make_request
               |
               |--88.24%-- blk_queue_split
               |          blk_queue_bio
               |          generic_make_request
               |          submit_bio
               |          ext4_io_submit
               |          |
               |          |--56.38%-- ext4_writepages
               |          |          do_writepages
               |          |          __filemap_fdatawrite_range
               |          |          sys_fadvise64
               |          |          do_syscall_64
               |          |          return_from_SYSCALL_64
               |          |          posix_fadvise64
               |          |          0
               |          |
               |           --43.62%-- ext4_bio_write_page
               |                     mpage_submit_page
               |                     mpage_process_page_bufs
               |                     mpage_prepare_extent_to_map
               |                     ext4_writepages
               |                     do_writepages
               |                     __filemap_fdatawrite_range
               |                     sys_fadvise64
               |                     do_syscall_64
               |                     return_from_SYSCALL_64
               |                     posix_fadvise64
               |                     0
               |
                --11.76%-- submit_bio
                          ext4_io_submit
                          |
                          |--58.95%-- ext4_writepages
                          |          do_writepages
                          |          __filemap_fdatawrite_range
                          |          sys_fadvise64
                          |          do_syscall_64
                          |          return_from_SYSCALL_64
                          |          posix_fadvise64
                          |          0
                          |
                           --41.05%-- ext4_bio_write_page
                                     mpage_submit_page
                                     mpage_process_page_bufs
                                     mpage_prepare_extent_to_map
                                     ext4_writepages
                                     do_writepages
                                     __filemap_fdatawrite_range
                                     sys_fadvise64
                                     do_syscall_64
                                     return_from_SYSCALL_64
                                     posix_fadvise64
                                     0

4.2 X - bio 拆分

文件系统提交bio时,generic_make_request会调用blk_queue_biobio缓存到设备请求队列 (request_queue) 里。 而在缓存bio之前,blk_queue_bio会调用blk_queue_split,此函数根据块设备的请求队列设置的limits.max_sectorslimits.max_segments属性,来对超出自己处理能力的大bio进行拆分。

而这里请求队列的limits.max_sectorslimits.max_segments属性,则是由块设备驱动程序在初始化时,根据自己的处理能力设置的。 当bio拆分频繁发生时,这时 IO 操作的性能会受到影响,因此,blktrace结果中的 X 操作,需要做进一步分析,来搞清楚 Sampleblk 驱动如何设置请求队列属性,进而影响到bio拆分的。

X 操作对应的具体代码路径,请参考perf 命令对 block:block_split 的跟踪结果

100.00%   100.00%  fio      [kernel.vmlinux]  [k] blk_queue_split
            |
            ---blk_queue_split
               blk_queue_bio
               generic_make_request
               submit_bio
               ext4_io_submit
               |
               |--55.73%-- ext4_writepages
               |          do_writepages
               |          __filemap_fdatawrite_range
               |          sys_fadvise64
               |          do_syscall_64
               |          return_from_SYSCALL_64
               |          posix_fadvise64
               |          0
               |
                --44.27%-- ext4_bio_write_page
                          mpage_submit_page
                          mpage_process_page_bufs
                          mpage_prepare_extent_to_map
                          ext4_writepages
                          do_writepages
                          __filemap_fdatawrite_range
                          sys_fadvise64
                          do_syscall_64
                          return_from_SYSCALL_64
                          posix_fadvise64
                          0

4.3 M - 合并 IO 请求

如前所述,文件系统向通用块层提交 IO 请求时,使用的是struct bio结构,并且bio->bi_next是 NULL 值,并未形成链表。 在blk_queue_bio代码中,这个被提交的bio的缓存处理存在以下几种情况,

  • 如果当前进程 IO 处于 Plug 状态,那么尝试将bio合并到当前进程的 plugged list 里,即current->plug.list里。
  • 如果当前进程 IO 处于 Unplug 状态,那么尝试利用 IO 调度器的代码找到合适的 IOrequest,并将bio合并到该request中。
  • 如果无法将bio合并到已经存在的 IOrequest结构里,那么就进入到单独为该bio分配空闲 IOrequest的逻辑里。

不论是 plugged list 还是 IO scheduler 的 IO 合并,都分为向前合并和向后合并两种情况,

  • ELEVATOR_BACK_MERGE 由bio_attempt_back_merge完成
  • ELEVATOR_FRONT_MERGE 由bio_attempt_front_merge完成

细心的读者会发现,前面fio测试对起始扇区 2488 发起的下面顺序的 IO 操作里,并未包含 M 操作,

Q -> X -> G -> I -> D -> C

但是,整个fio测试过程中,还是有部分 IO 被合并了,因为我们并没有用blktrace捕捉全部 IO 操作,因此没有跟踪到这些合并操作。 当合并操作发生时,其时序如下,

Q -> X -> M -> G -> I -> D -> C

如果用perf命令去跟踪 block:block_bio_backmerge 和 block:block_bio_frontmerge 的事件,会发现都是向后合并操作,测试全程没有向前合并操作。 这是由于本例中的fio测试是文件顺序写 IO,因此都是向后合并这种情况,所以只有 M 操作,而不会有 F 操作。

M 操作对应的具体代码路径,请参考perf 命令对 block:block_bio_backmerge 的跟踪结果

100.00%   100.00%  fio      [kernel.vmlinux]  [k] bio_attempt_back_merge
            |
            ---bio_attempt_back_merge
               blk_attempt_plug_merge
               blk_queue_bio
               generic_make_request
               submit_bio
               ext4_io_submit
               |
               |--94.23%-- ext4_writepages
               |          do_writepages
               |          __filemap_fdatawrite_range
               |          sys_fadvise64
               |          do_syscall_64
               |          return_from_SYSCALL_64
               |          posix_fadvise64
               |          0
               |
                --5.77%-- ext4_bio_write_page
                          mpage_submit_page
                          mpage_process_page_bufs
                          mpage_prepare_extent_to_map
                          ext4_writepages
                          do_writepages
                          __filemap_fdatawrite_range
                          sys_fadvise64
                          do_syscall_64
                          return_from_SYSCALL_64
                          posix_fadvise64
                          0

4.4 G - 分配 IO 请求

如前面小结所述,在blk_queue_bio代码中,若无法合并bio到已存在的 IOrequest里, 该函数会为bio分配一个 IO 请求结构,即struct request

G 操作对应的具体代码路径,请参考perf 命令对 block:block_getrq 的跟踪结果

100.00%   100.00%  fio      [kernel.vmlinux]  [k] get_request
            |
            ---get_request
               blk_queue_bio
               generic_make_request
               submit_bio
               ext4_io_submit
               |
               |--54.41%-- ext4_writepages
               |          do_writepages
               |          __filemap_fdatawrite_range
               |          sys_fadvise64
               |          do_syscall_64
               |          return_from_SYSCALL_64
               |          posix_fadvise64
               |          0
               |
                --45.59%-- ext4_bio_write_page
                          mpage_submit_page
                          mpage_process_page_bufs
                          mpage_prepare_extent_to_map
                          ext4_writepages
                          do_writepages
                          __filemap_fdatawrite_range
                          sys_fadvise64
                          do_syscall_64
                          return_from_SYSCALL_64
                          posix_fadvise64
                          0

4.5 I - 请求插入队列

如前面小结所述,在blk_queue_bio代码中,当已经为不能合并的bio分配了request,下一步则有如下两种可能,

  • 如果当前进程 IO 已经被 Plug,这个新的request将会被加到当前进程的plug->list里来。
  • 如果当前进程的 IO 已经或者马上处于 unplug 状态,那么request将被插入到 IO 调度器的内部队列里。

blk_queue_bio会通过触发 Unplug 操作,最终调用__elv_add_request函数负责将request插入到 IO 调度器内部队列,其中牵涉到下面两种情况,

  • ELEVATOR_INSERT_SORT_MERGE

    request合并到 IO 调度器队列已存在的request里,并释放新分配的request

  • ELEVATOR_INSERT_SORT

    request插入到 IO 调度器经过排序的队列里。例如,将request插入到 deadline 调度器排序过的红黑树里。

I 操作对应的具体代码路径,请参考perf 命令对 block:block_rq_insert 的跟踪结果

100.00%   100.00%  fio      [kernel.vmlinux]  [k] __elv_add_request
            |
            ---__elv_add_request
               blk_flush_plug_list
               |
               |--74.74%-- blk_queue_bio
               |          generic_make_request
               |          submit_bio
               |          ext4_io_submit
               |          ext4_writepages
               |          do_writepages
               |          __filemap_fdatawrite_range
               |          sys_fadvise64
               |          do_syscall_64
               |          return_from_SYSCALL_64
               |          posix_fadvise64
               |          0
               |
                --25.26%-- blk_finish_plug
                          ext4_writepages
                          do_writepages
                          __filemap_fdatawrite_range
                          sys_fadvise64
                          do_syscall_64
                          return_from_SYSCALL_64
                          posix_fadvise64
                          0

4.6 D - 发起 IO 请求

有两种常见的触发 Unplug IO 的时机,

  • 文件系统通过调用blk_finish_plug显式地触发
  • blk_queue_bio检测到当前进程plug->list的请求数目超过了 BLK_MAX_REQUEST_COUNT

当 Unplug 发生时,__blk_run_queue最终会被调用,然后块驱动程序的策略函数就会被调用,进而进入块设备 IO 流程。本例中,sampleblk 驱动的策略函数sampleblk_request开始被调用,

D 操作对应的具体代码路径,请参考perf 命令对 block:block_rq_issue 的跟踪结果

100.00%   100.00%  fio      [kernel.vmlinux]  [k] blk_peek_request
            |
            ---blk_peek_request
               blk_fetch_request
               sampleblk_request
               __blk_run_queue
               queue_unplugged
               blk_flush_plug_list
               |
               |--72.41%-- blk_queue_bio
               |          generic_make_request
               |          submit_bio
               |          ext4_io_submit
               |          ext4_writepages
               |          do_writepages
               |          __filemap_fdatawrite_range
               |          sys_fadvise64
               |          do_syscall_64
               |          return_from_SYSCALL_64
               |          posix_fadvise64
               |          0
               |
                --27.59%-- blk_finish_plug
                          ext4_writepages
                          do_writepages
                          __filemap_fdatawrite_range
                          sys_fadvise64
                          do_syscall_64
                          return_from_SYSCALL_64
                          posix_fadvise64
                          0

4.7 C - bio 完成

块驱动在处理完 IO 请求后,可以通过调用blk_end_request_all来通知通用块层 IO 操作完成。

通知通用块层完成的函数还有blk_end_request。两者的区别主要是,blk_end_request是为 partial complete 设计实现的,但是blk_end_request_all缺省就是完整的bio完成来设计的。 因此,调用blk_end_request时,需要指定 IO 操作完成的字节数。因此,如果块设备驱动支持 IO 部分完成特性,则可以使用blk_end_request来支持。

此外,还存在__blk_end_request_all__blk_end_request形式的 IO 完成通知函数。这两个函数必须在获取request_queue队列的锁以后才开始调用。 而blk_end_request_allblk_end_request则不需要拿队列锁。

C 操作对应的具体代码路径,请参考perf 命令对 block:block_rq_complete 的跟踪结果,

100.00%   100.00%  fio      [kernel.vmlinux]    [k] blk_update_request
            |
            ---blk_update_request
               |
               |--99.99%-- blk_update_bidi_request
               |          blk_end_bidi_request
               |          blk_end_request_all
               |          sampleblk_request
               |          __blk_run_queue
               |          queue_unplugged
               |          blk_flush_plug_list
               |          |
               |          |--76.92%-- blk_queue_bio
               |          |          generic_make_request
               |          |          submit_bio
               |          |          ext4_io_submit
               |          |          ext4_writepages
               |          |          do_writepages
               |          |          __filemap_fdatawrite_range
               |          |          sys_fadvise64
               |          |          do_syscall_64
               |          |          return_from_SYSCALL_64
               |          |          posix_fadvise64
               |          |          0
               |          |
               |           --23.08%-- blk_finish_plug
               |                     ext4_writepages
               |                     do_writepages
               |                     __filemap_fdatawrite_range
               |                     sys_fadvise64
               |                     do_syscall_64
               |                     return_from_SYSCALL_64
               |                     posix_fadvise64
               |                     0
                --0.01%-- [...]

5. 小结

本文在与前几篇文章相同的fio测试过程中,使用blktraceperf追踪的块设备层的 IO 操作,解释了 Linux 内核块设备 IO 的基本流程。 第三小节中的 blkparse(1) trace action 的表格对理解blktrace的输出含义也做了简单的总结,有助于熟悉blktrace的使用和结果分析。

results matching ""

    No results matching ""