Linux Block Driver - 2


转载时请包含原文或者作者网站链接:http://oliveryang.net

1. 背景

Linux Block Driver - 1 中,我们实现了一个最简块设备驱动 Sampleblk。 这个只有 200 多行源码的块设备驱动利用内存创建了标准的 Linux 磁盘。我们在基于 Linux 4.6.0 内核的环境下,加载该驱动,并在其上创建了 Ext4 文件系统。

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

2. 准备

首先,在阅读本文前,请按照 Linux Block Driver - 1 中的步骤准备好实验环境。确保可以做到如下步骤,

  • 编译和加载 Sampleblk Day1 驱动
  • 用 Ext4 格式化 /dev/sampleblk1
  • mount 文件系统到 /mnt

其次,为了继续后续实验,还需做如下准备工作。

  • 安装 fio 测试软件。

    fio 是目前非常流行的 IO 子系统测试工具。作者 Jens Axboe 是 Linux IO 子系统的 maintainer,目前就职于 Facebook。 互联网上 FIO 安装和使用的文章很多,这里就不在赘述。不过最值得细读的还是 fio HOWTO

  • 安装 blktrace 工具。

    也是 Jens Axboe 开发的 IO 子系统追踪和性能调优工具。发行版有安装包。关于该工具的使用可以参考 blktrace man page

  • 安装 Linux Perf 工具。

    Perf 是 Linux 源码树自带工具,运行时动态追踪,性能分析的利器。也可以从发行版找到安装包。 网上的 Perf 使用介绍很多。Perf Wiki 非常值得一看。

  • 下载 perf-tools 脚本。

    perf-tools 脚本 是 Brendan Gregg 写的基于 ftrace 和 perf 的工具脚本。全部由 bash 和 awk 写成,无需安装,非常简单易用。 Ftrace: The hidden light switch 这篇文章是 Brendan Gregg 给 LWN 的投稿,推荐阅读。

3. 实验与分析

3.1 文件顺序写测试

如一般 Linux 测试工具支持命令行参数外,fio 也支持 job file 的方式定义测试参数。 本次实验中使用的 fs_seq_write_sync_001 job file 内容如下,

; -- start job file --
[global]            ; global shared parameters
filename=/mnt/test  ; location of file in file system
rw=write            ; sequential write only, no read
ioengine=sync       ; synchronized, write(2) system call
bs=,4k              ; fio iounit size, write=4k, read and trim are default(4k)
iodepth=1           ; how many in-flight io unit
size=2M             ; total size of file io in one job
loops=1000000       ; number of iterations of one job

[job1]              ; job1 specific parameters

[job2]              ; job2 specific parameters
; -- end job file --

本次实验将在 /dev/sampleblk1 上 mount 的 Ext4 文件系统上进行顺序 IO 写测试。其中 fio 将启动两个测试进程,同时对 /mnt/test 文件进行写操作。

$ sudo fio ./fs_seq_write_sync_001
job1: (g=0): rw=write, bs=4K-4K/4K-4K/4K-4K, ioengine=sync, iodepth=1
job2: (g=0): rw=write, bs=4K-4K/4K-4K/4K-4K, ioengine=sync, iodepth=1
fio-2.1.10
Starting 2 processes
^Cbs: 2 (f=2): [WW] [58.1% done] [0KB/2208MB/0KB /s] [0/565K/0 iops] [eta 13m:27s]
...[snipped]...
fio: terminating on signal 2

job1: (groupid=0, jobs=1): err= 0: pid=22977: Thu Jul 21 22:10:28 2016
  write: io=1134.8GB, bw=1038.2MB/s, iops=265983, runt=1118309msec
    clat (usec): min=0, max=66777, avg= 1.63, stdev=21.57
     lat (usec): min=0, max=66777, avg= 1.68, stdev=21.89
    clat percentiles (usec):
     |  1.00th=[    0],  5.00th=[    1], 10.00th=[    1], 20.00th=[    1],
     | 30.00th=[    1], 40.00th=[    1], 50.00th=[    2], 60.00th=[    2],
     | 70.00th=[    2], 80.00th=[    2], 90.00th=[    2], 95.00th=[    3],
     | 99.00th=[    4], 99.50th=[    7], 99.90th=[   18], 99.95th=[   25],
     | 99.99th=[  111]
    lat (usec) : 2=49.79%, 4=49.08%, 10=0.71%, 20=0.34%, 50=0.06%
    lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
    lat (msec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.01%
    lat (msec) : 100=0.01%
  cpu          : usr=8.44%, sys=69.65%, ctx=1935732, majf=0, minf=9
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued    : total=r=0/w=297451591/d=0, short=r=0/w=0/d=0
     latency   : target=0, window=0, percentile=100.00%, depth=1
job2: (groupid=0, jobs=1): err= 0: pid=22978: Thu Jul 21 22:10:28 2016
  write: io=1137.4GB, bw=1041.5MB/s, iops=266597, runt=1118309msec
    clat (usec): min=0, max=62132, avg= 1.63, stdev=21.35
     lat (usec): min=0, max=62132, avg= 1.68, stdev=21.82

...[snipped]...

Run status group 0 (all jobs):
  WRITE: io=2271.2GB, aggrb=2080.5MB/s, minb=1038.2MB/s, maxb=1041.5MB/s, mint=1118309msec, maxt=1118309msec

Disk stats (read/write):
  sda: ios=0/4243062, merge=0/88, ticks=0/1233576, in_queue=1232723, util=37.65%

从 fio 的输出中可以看到 fio 启动了两个 job,并且按照 job file 规定的设置开始做文件系统写测试。 在测试进行到 58.1% 的时候,我们中断程序,得到了上述的输出。从输出中我们得出如下结论,

  • 两个线程总共的写的吞吐量为 2080.5MB/s,在磁盘上的 IPOS 是 4243062。
  • 每个线程的平均完成延迟 (clat) 为 1.63us,方差是 21.57。
  • 每个线程的平均总延迟 (lat) 为 1.68us,方差是 21.89。
  • 磁盘 IO merge 很少,磁盘的利用率也只有 37.65%。
  • 线程所在处理器的时间大部分在内核态:69.65%,用户态时间只有 8.44% 。

3.2 文件 IO Pattern 分析

3.2.1 使用 strace

首先,我们可以先了解一下 fio 测试在系统调用层面看的 IO pattern 是如何的。Linux 的 strace 工具是跟踪应用使用系统调用的常用工具。

在 fio 运行过程中,我们获得 fio 其中一个 job 的 pid 之后,运行了如下的 strace 命令,

$ sudo strace -ttt -T -e trace=desc -C -o ~/strace_fio_fs_seq_write_sync_001.log -p 94302

strace man page 给出了命令的详细用法,这里只对本小节里用到的各个选项做简单的说明,

  • -ttt 打印出每个系统调用发生的起始时间戳。
  • -T 则给出了每个系统调用的开销。
  • -e trace=desc 只记录文件描述符相关系统调用。这样可过滤掉无关信息,因为本实验是文件顺序写测试。
  • -C 则在 strace 退出前可以给出被跟踪进程的系统调用在 strace 运行期间使用比例和次数的总结。
  • -o 则指定把 strace 的跟踪结果输出到文件中去。

3.2.2 分析 strace 日志

根据 strace 的跟踪日志,我们可对本次 fio 测试的 IO pattern 做一个简单的分析。 详细日志信息请访问这里,下面只给出其中的关键部分,

1466326568.892873 open("/mnt/test", O_RDWR|O_CREAT, 0600) = 3 <0.000013>
1466326568.892904 fadvise64(3, 0, 2097152, POSIX_FADV_DONTNEED) = 0 <0.000813>
1466326568.893731 fadvise64(3, 0, 2097152, POSIX_FADV_SEQUENTIAL) = 0 <0.000004>
1466326568.893744 write(3, "\0\260\35\0\0\0\0\0\0\320\37\0\0\0\0\0\0\300\35\0\0\0\0\0\0\340\37\0\0\0\0\0"..., 4096) = 4096 <0.000020>

[...snipped (512 write system calls)...]

1466326568.901551 write(3, "\0p\27\0\0\0\0\0\0\320\37\0\0\0\0\0\0\300\33\0\0\0\0\0\0\340\37\0\0\0\0\0"..., 4096) = 4096 <0.000006>
1466326568.901566 close(3)              = 0 <0.000008>

[...snipped (many iterations of open, fadvise64, write, close)...]

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 72.55    0.192610           2     84992           write
 27.04    0.071788         216       332           fadvise64
  0.28    0.000732           4       166           open
  0.13    0.000355           2       166           close
------ ----------- ----------- --------- --------- ----------------
100.00    0.265485                 85656           total

根据 strace 日志,我们就可以轻松分析这个 fio 测试的 IO Pattern 是如何的了,

  1. 首先调用 open 在 Ext4 上以读写方式打开 /mnt/test 文件,若不存在则创建一个。

    因为 fio job file 指定了文件名,filename=/mnt/test

  2. 调用 fadvise64,使用 POSIX_FADV_DONTNEED 把 /mnt/test 在 page cache 里的数据 flush 到磁盘。

    fio 做文件 IO 前,清除 /mnt/test 文件的 page cache,可以让测试避免受到 page cache 影响。

  3. 调用 fadvise64,使用 POSIX_FADV_SEQUENTIAL 提示内核应用要对 /mnt/test 做顺序 IO 操作。

    这是因为 fio job file 定义了 rw=write,因此这是顺序写测试。

  4. 调用 write 对 /mnt/test 写入 4K 大小的数据。一共 write 512 次,共 2M 数据。

    这是因为 fio job file 定义了 ioengine=sync,bs=,4k,size=2M。

  5. 最后,调用 close 完成一次 /mnt/test 顺序写测试。重复上述过程,反复迭代。

    fio job file 定义了 loops=1000000

另外,根据 strace 日志的系统调用时间和调用次数的总结,我们可以得出如下结论,

  • 系统调用 openwriteclose 的开销非常小,只有几微秒。
  • 测试中 write 调用次数最多,虽然单次 write 只有几微妙,但积累总时间最高。
  • 测试中 fadvise64 调用次数比 write 少,但 POSIX_FADV_DONTNEED 带来的 flush page cache 的操作可以达到几百微秒。

3.2.3 使用 SystemTap

使用 strace 虽然可以拿到单次系统调用读写的字节数,但对大量的 IO 请求来说,不经过额外的脚本处理,很难得到一个总体的认识和分析。 但是,我们可以通过编写 SystemTap 脚本来对这个测试的 IO 请求大小做一个宏观的统计,并且使用直方图来直观的呈现这个测试的文件 IO 尺寸分布。

启动 fio 测试后,只需要运行如下命令,即可收集到指定 PID 的文件 IO 的统计信息,

$ sudo ./fiohist.stp 94302
starting probe
^C
IO Summary:

                                       read     read             write    write
            name     open     read   KB tot    B avg    write   KB tot    B avg
             fio     7917        0        0        0  3698312 14793248     4096

Write I/O size (bytes):

process name: fio
value |-------------------------------------------------- count
 1024 |                                                         0
 2048 |                                                         0
 4096 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  3698312
 8192 |                                                         0
16384 |                                                         0

可以看到,直方图和统计数据显示,整个跟踪数据收集期间都是 4K 字节的 write 写操作,而没有任何读操作。而且,在此期间,没有任何 read IO 操作。 同时,由于 write 系统调用参数并不提供文件内的偏移量,所以我们无法得知文件的写操作是否是随机还是顺序的。

但是,如果是文件的随机读写 IO,应该可以在 strace 时观测到 lseek + readwrite 调用。这也从侧面可以推测测试是顺序写 IO。 此外,preadpwrite 系统调用提供了文件内的偏移量,有这个偏移量的数据,即可根据时间轴画出 IO 文件内偏移的 Heatmap。 通过该图,即可直观地判断是否是随机还是顺序 IO 了。

本例中的 SystemTap 脚本 fiohist.stp 是作者个人为分析本测试所编写。 详细代码请参考文中给出的源码链接。此外,在 Linux Perf Tools Tips 这篇文章里收录了关于在自编译内核上运行 SystemTap 脚本的一些常见问题。

3.2.4 延迟统计

fio 的测试结果已经提供了详尽的 IO 延迟数据。因为 fs_seq_write_sync_001 文件定义的是文件 buffer IO,并且是同步写模式。因此,fio 报告的延迟数据就是在文件 IO 层面上的,我们不需要使用其它的工具了。

查看 fio 源码,可以发现,它记录了一次 IO 流程的三个时间,

起始时间 (io_u->start_time) >>>>>> 触发时间 (io_u->issue_time) >>>>>> 完成时间 (icd->time)

其具体含义分别如下,

  • 起始时间

    在文件打开的状态下,是读写入文件的缓冲区准备好后的时间。源代码定义:io_u->start_time。

  • 触发时间

    同步 IO 时,是读写系统调用发起前的时间。 异步 IO 时,是 IO 请求成功放入请求队列后 (td->io_ops->queue) 返回的时间。 源代码定义:io_u->issue_time。

  • 完成时间

    是 IO 完成时的时间。源代码定义:icd->time。

而在 fio 输出里,则存在三种类型的延迟数据,分别为如下含义,

  • slat (submission latency)

    即 IO 提交延迟。其确切含义是 IO 准备好到 IO 真正开始的时间 (即 io_u->issue_time - io_u->start_time)。 需要注意的是,在同步 (SYNC) IO 模式下,slat 并不计算,这是因为同步 IO 的这两个时间非常接近,没有计算意义。

  • clat (completion latency)

    即 IO 完成延迟。其确切含义是 IO 真正开始到 IO 返回的时间 (即 icd->time - io_u->issue_time)。

  • lat (latency)

    即 IO 总延迟,其确切含义是 IO 准备好到 IO 返回的总时间 (即 icd->time - io_u->issue_time)。

有了以上概念,再解读下面的数据就很简单了。

例如,本测试里完成延迟 clat (completion latency) 的结果如下,其中包含了均值 (avg) 和方差 (stdev),

clat (usec): min=0, max=66777, avg= 1.63, stdev=21.57

而总延迟 lat (latency) 结果如下,

lat (usec): min=0, max=66777, avg= 1.68, stdev=21.89

其中,clat percentiles 给出了各种 IO 完成延迟的百分比分布,

clat percentiles (usec):
 |  1.00th=[    0],  5.00th=[    1], 10.00th=[    1], 20.00th=[    1],
 | 30.00th=[    1], 40.00th=[    1], 50.00th=[    2], 60.00th=[    2],
 | 70.00th=[    2], 80.00th=[    2], 90.00th=[    2], 95.00th=[    3],
 | 99.00th=[    4], 99.50th=[    7], 99.90th=[   18], 99.95th=[   25],
 | 99.99th=[  111]

而总 IO 延迟的百分比分布也包括在输出了,

lat (usec) : 2=49.79%, 4=49.08%, 10=0.71%, 20=0.34%, 50=0.06%
lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
lat (msec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.01%
lat (msec) : 100=0.01%

3.3 On CPU Time 分析

运行 fio 测试期间,我们可以利用 Linux perf, 对系统做 ON CPU Time 分析。这样可以进一步获取如下信息,

  • 在测试中,软件栈的哪一部分消耗了主要的 CPU 资源。可以帮助我们确定 CPU 时间优化的主要方向。

  • 通过查看消耗 CPU 资源的软件调用栈,了解函数调用关系。

  • 利用可视化工具,如 Flamgraph,对 Profiling 的大量数据做直观的呈现。方便进一步分析和定位问题。

3.3.1 使用 perf

首先,当 fio 测试进入稳定状态,运行 perf record 命令,

# perf record -a -g --call-graph dwarf -F 997 sleep 60

其中主要的命令行选项如下,

  • -F 选项指定 perf997 次每秒的频率对 CPU 上运行的用户进程或者内核上下文进行采样 (Sampling)。

    由于 Linux 内核的时钟中断是以 1000 次每秒的频率周期触发,所以按照 997 频率采样可以避免每次采样都采样到始终中断相关的处理,减少干扰。

  • -a 选项指定采样系统中所有的 CPU。

  • -g 选项指定记录下用户进程或者内核的调用栈。

    其中,--call-graph dwarf 指定调用栈收集的方式为 dwarf,即 libdwarflibdunwind 的方式。Perf 还支持 fplbs 方式。

  • sleep 60 则是通过 perf 指定运行的命令,这个命令起到了让 perf 运行 60 秒然后退出的效果。

perf record 之后,运行 perf report 查看采样结果的汇总,

# sudo perf report --stdio

[...snipped...]

27.51%     0.10%  fio    [kernel.kallsyms]      [k] __generic_file_write_iter
                    |
                    ---__generic_file_write_iter
                       |
                       |--99.95%-- ext4_file_write_iter
                       |          __vfs_write
                       |          vfs_write
                       |          sys_write
                       |          do_syscall_64
                       |          return_from_SYSCALL_64
                       |          0x7ff91cd381cd
                       |          fio_syncio_queue
                       |          td_io_queue
                       |          thread_main
                       |          run_threads
                        --0.05%-- [...]
[...snipped...]

3.3.2 使用 Flamegraph

使用 Flamegraph,可以把前面产生的 perf record 的结果可视化,生成火焰图。 运行如下命令,

# perf script | stackcollapse-perf.pl > out.perf-folded
# cat out.perf-folded | flamegraph.pl > flamegraph_on_cpu_perf_fs_seq_write_sync_001.svg

然后,即可生成如下火焰图,

该火焰图是 SVG 格式的矢量图,基于 XML 文件定义。在浏览器里右击在新窗口打开图片,即可进入与火焰图的交互模式。该模式下,统计数据信息和缩放功能都可以移动和点击鼠标来完成交互。 通过在交互模式下浏览和缩放火焰图,我们可以得出如下结论,

  • perf record 共有 119644 个采样数据,将此定义为 100% CPU 时间。
  • fio 进程共有 91079 个采样数据,占用 76.13% 的 CPU 时间。

    fiofio_syncio_queue 用掉了 48.53% 的 CPU,其中绝大部分时间在内核态,sys_write 系统调用就消耗了 45.78%。

    fiofile_invalidate_cache 函数占用了 20.88% 的 CPU,其中大部分都在内核态,sys_fadvise64 系统调用消耗了 20.81%。

    在这里我们注意到,sys_writesys_fadvise64 系统调用 CPU 占用资源的比例是 2:1。而之前 strace 得出的两个系统调用消耗时间的比例是 3:1。 这就意味着,sys_write 花费了很多时间在睡眠态

  • 在 Ext4 文件系统的写路径,存在热点锁。

    ext4_file_write_iter 函数里的 inode mutex 的 mutex 自旋等待时间,占用了 16.93% 的 CPU。与 sys_write 系统调用相比,CPU 消耗占比达到三分之一强。

  • swapper 为内核上下文,包含如下部分,

    native_safe_halt 代表 CPU 处于 IDEL 状态,共有两次,9.04% 和 9.18%。

    smp_reschedule_interrupt 代表 CPU 处理调度器的 IPI 中断,用于处理器间调度的负载均衡。共有两次,1.66% 和 1.61%。这部分需要方大矢量图移动鼠标到相关函数才能看到。

  • kblockd 工作队列线程。

    block_run_queue_async 触发,最终调用 __blk_run_queue 把 IO 发送到下层的 sampleblk 块驱动。共有两部份,合计 0.88%。

  • rcu_gp_kthread 处理 RCU 的内核线程,占用 0.04 % 的 CPU 时间。

综合以上分析,我们可以看到,火焰图不但可以帮助我们理解 CPU 全局资源的占用情况,而且还能进一步分析到微观和细节。例如局部的热锁,父子函数的调用关系,和所占 CPU 时间比例。

关于进一步的 Flamegraph 的介绍和资料,请参考 Brenden Gregg 的 Flamegraph 相关资源

4. 小结

本文通过使用 Linux 下的各种追踪工具 Strace,Systemtap,Perf,Ftrace,来分析 fio 测试的运行情况。实际上,利用 Linux 下的动态追踪工具我们达到了以下目的,

  • 掌握了本文中 fio 测试的主要特征,文件 IO size,IO 时间分布。这是性能分析里 workload analysis 方法的一部分。
  • 了解了 fio 测试 On CPU 时间的分析方法。这是性能分析里 resource analysis 方法的一部分。

关于 Linux 动态追踪工具的更多信息,请参考延伸阅读章节里的链接。

5. 延伸阅读

Oliver Yang /
Published under (CC) BY-NC-SA in categories Chinese  Software  Hardware  tagged with driver  perf  trace  file system  kernel  linux  storage