Memory Dump 2023-05-03T02:20:54+00:00 yangoliver@gmail.com Linux CPI FlameGraph 2018-03-04T00:00:00+00:00 Oliver Yang http://oliveryang.net/2018/03/linux-CPI-flamegraph

本文首发于内核月谈微信号,由杨勇, 吴一昊共同完成; 转载时请包含原文或者作者网站链接:http://oliveryang.net

1. 什么是 CPI ?##

本小节讲述为什么使用 CPI 分析程序性能的意义。如果已经非常了解 CPI 对分析程序性能的意义,可以跳过本小节的阅读。

1.1 程序怎么样才能跑得快 ?###

理解什么是 CPI,首先让我们思考一个问题:在一个给定的处理器上,如何才能让程序跑得更快呢?

假设程序跑得快慢的标准是程序的执行时间,那么程序执行的快慢,就可以用如下公式来表示:

   程序执行时间 = 程序总指令数 x 每 CPU 时钟周期时间 x 每指令执行所需平均时钟周期数

因此,要想程序跑得快,即减少程序执行时间,我们就需要在以下三个方面下功夫:

  1. 减少程序总指令数

    要减少程序执行的总指令数,可能有以下手段:

    • 算法优化;好的算法设计,可能带来更少的指令执行数
    • 更高效的编译器或者解释器;新的编译器或者解释器,可能对同样的源代码,生成更少的机器码。
    • 用更底层的语言优化;这是为何 Linux 内核代码使用 C 语言,并且还喜欢内联汇编。
    • 更新的处理器指令;新的处理器指令,对处理某类特殊目的运算更有帮助,而新版本编译器最重要的工作就是,在新的处理器上,用最新的高效指令;例如,x86 SSE,AVX 指令。
  2. 减少每 CPU 时钟周期时间

    这一点很容易理解,缩短 CPU 时钟周期的时间,实际上就是要提高 CPU 的主频。这正是 Intel 过去战无不胜的法宝之一。今天,由于主频的提高已经到了制造工艺的极限,CPU 时钟周期的时间很难再继续降低了。

  3. 减少每指令执行所需平均时钟周期数

    如何减少每指令执行所需平均 CPU 时钟周期数呢?让我们先从 CPU 设计角度看一下:

    • 标量处理器 (Scalar Processor) ;一个 CPU 时钟周期只能执行一条指令;
    • 超标量处理器 (Superscalar Processor);一个 CPU 时钟周期可以执行多条指令;通常这个是靠在处理器里实现多级流水线 (Pipeline) 来实现的。

    因此不难看出,如果使用支持超标量处理器的 CPU,利用 CPU 流水线提高指令并行度,那么就可以达到我们的目的了。流水线的并行度越高,执行效率越高,那么每指令执行所需平均时钟周期数就会越低。

    当然,流水线的并行度和效率,又取决于很多因素,例如,取指令速度,访存速度,指令乱序执行 (Out-Of-Order Execution),分支预测执行 (Branch Prediction Execution),投机执行 (Speculative Execution)的能力。一旦流水线并行执行的能力降低,那么程序的性能就会受到影响。关于超标量处理器,流水线,乱序执行,投机执行的细节,这里不再一一赘述,请查阅相关资料。

    另外,在 SMP,或者多核处理器系统里,程序还可以通过并行编程来提高指令的并行度,因此,这也是为什么今天在 CPU 主频再难以提高的情况下,CPU 架构转为 Multi-Core 和 Many-Core。

由于提高 CPU 主频的同时,又要保障一个 CPU 时钟周期可以执行更多的指令,因此处理器厂商需要不断地提高制造工艺,降低 CPU 的芯片面积和功耗。

1.2 CPI 和 IPC

在计算机体系结构领域,经常可以看到 CPI 的使用。CPI 即 Cycle Per Instruction 的缩写,它的含义就是每指令周期数。此外,在一些场合,也可以经常看到 IPC,即 Instruction Per Cycle,含义为每周期指令数

因此不难得出,CPI 和 IPC 的关系为,

   CPI = 1 / IPC

使用 CPI 这个定义,本文开篇用于衡量程序执行性能的公式,如果具体到单 CPU 的程序执行性能场景,实际上可以表示为:

   Execution Time (T) = Instruction Count x Time Per Cycle X CPI

由于受到硅材料和制造工艺的限制,处理器主频的提高已经面临瓶颈,因此,程序性能的提高,主要的变量在 Instruction Count 和 CPI 这两个方面。

在 Linux 上,通过 perf 工具,通过 Intel 处理器提供的寄存器 (PMU),可以很容易测量一个程序的 IPC。例如,下例就可以给出 Java 程序的 IPC,8 秒多的时间里,这个 Java 程序的 IPC 是 0.54:

$sudo perf stat -p `pidof java`
^C
 Performance counter stats for process id '3191':

          1.616171      task-clock (msec)         #    0.000 CPUs utilized
               221      context-switches          #    0.137 M/sec
                 0      cpu-migrations            #    0.000 K/sec
                 2      page-faults               #    0.001 M/sec
         2,907,189      cycles                    #    1.799 GHz
         2,083,821      stalled-cycles-frontend   #   71.68% frontend cycles idle
         1,714,355      stalled-cycles-backend    #   58.97% backend  cycles idle
         1,561,667      instructions              #    0.54  insns per cycle
                                                  #    1.33  stalled cycles per insn
           286,102      branches                  #  177.025 M/sec
     <not counted>      branch-misses              (0.00%)

       8.841569895 seconds time elapsed

那么,通过 IPC,我们也可以换算出 CPI 是 1/0.54,约为 1.85.

通常情况下,通过 CPI 的取值,我们可以大致判断一个计算密集型任务,到底是 CPU 密集型的还是 Memory 密集型的:

  • CPI 小于 1,程序通常是 CPU 密集型的;
  • CPI 大于 1,程序通常是 Memory 密集型的;

1.3 重新认识 CPU 利用率

对程序员来说,判断一个计算密集型任务运行效率的重要依据就是看程序运行时的 CPU 利用率。很多人认为 CPU 利用率高就是程序的代码在疯狂运行。实际上,CPU 利用率高,也有可能是 CPU 正在忙等一些资源,如访问内存遇到了瓶颈。

一些计算密集型任务,在正常情况下,CPI 很低,性能原本很好。CPU 利用率很高。但是随着系统负载的增加,其它任务对系统资源的争抢,导致这些计算任务的 CPI 大幅上升,性能下降。而此时,很可能 CPU 利用率上看,还是很高的,但是这种 CPU 利用率的高,实际上体现的是 CPU 的忙等,及流水线的停顿带来的效应。

Brendan Gregg 曾在 CPU Utilization is Wrong 这篇博客中指出,CPU 利用率指标需要结合 CPI/IPC 指标一起来分析。并详细介绍了前因后果。感兴趣的读者可以自行阅读原文,或者订阅内核月谈公众号,阅读我们公众号非常靠谱的译文

至此,相信读者已经清楚,在不修改二进制程序的前提下,通过 CPI 指标了解程序的运行性能,有着非常重要的意义。对于计算密集型的程序,只通过 CPU 利用率这样的传统指标,也无法帮助你确认你的程序的运行效率,必须将 CPU 利用率和 CPI/IPC 结合起来看,确定程序的执行效率。

1.4 如何分析 CPI/IPC 指标异常?###

虽然利用 perf 可以很方便获取 CPI/IPC 指标,但是想分析和优化程序高 CPI 的问题,就需要一些工具和分析方法,将 CPI 高的原因,以及与之关联的软件的调用栈找到,从而决定优化方向。

关于 CPI 高的原因分析,在 Intel 64 and IA-32 Architectures Optimization Reference Manual, 附录 B 里有介绍。其中主要的思路就是按照自顶向下的方法,自顶向下排查, 4 种引起 CPI 变高的主要原因,

由于本文主要是介绍 CPI 火焰图,对于本小节的自顶向下的分析方法,限于篇幅所限,就不详细展开了,我们稍后会有专门的文章做详细介绍。

2. CPI 火焰图

Brendan Gregg 在 CPI Flame Graphs: Catching Your CPUs Napping 一文中,介绍了使用 CPI 火焰图来建立 CPI 和软件调用栈的关联。

我们已经知道,光看 CPU 利用率并不能知道 CPU 在干嘛。因为 CPU 可能执行到一条指令就停下来,等待资源了。这种等待对软件是透明的,因此从用户角度看,CPU 还是在被使用状态,但是实际上,指令并没有有效地执行,CPU 在忙等,这种 CPU 利用率并不是有效的利用率。

要发现 CPU 在 busy 的时候实际上在干什么,最简单的方法就是测量平均 CPI。CPI 高说明运行每条指令用了更多的周期。这些多出来的周期里面,通常是由于流水线的停顿周期 (Stalled Cycles) 造成的,例如,等待内存读写。

而 CPI 火焰图,可以基于 CPU 火焰图,提供一个可视化的基于 CPU 利用率和 CPI 指标,综合分析程序 CPU 执行效率的方案。

下面这个 CPI 火焰图引用自 Brendan Gregg 博客文章。可以看到,CPI 火焰图是基于 CPU 火焰图,根据 CPI 的大小,在每个条加上了颜色。红色代表指令,蓝色代表流水线的停顿:

火焰图中,每个函数帧的宽度,显示了函数或其子函数在 CPU 上的次数,和普通 CPU 火焰图完全一样。而颜色则显示了函数在 CPU 上是运行 (running 红色) 还是停顿 (stalled 蓝色)。

火焰图里,颜色范围,从最高CPI为蓝色(执行最慢的指令),到最低CPI为红色 (执行最快的指令)。火焰图是 SVG 格式,矢量图,因此支持鼠标点击缩放。

然而,Brendan Gregg 博客中的这篇博客,CPI 火焰图是基于 FreeBSD 操作系统特有的命令生成的,而在 Linux 上,应该怎么办呢?

3. 一个小程序

让我们写一个人造的小程序,展示在 Linux 下 CPI 火焰图的使用。

这是一个最简的小程序,其中包含如下两个函数:

  1. cpu_bound 函数主体是 nop 指令的循环;由于 nop 指令是不访问内存的最简指令之一, 因此该函数 CPI 一定小于 1,属于典型的 CPU 密集型的代码。
  2. memory_bound 函数使用 _mm_clflush 驱逐缓存,人为触发程序的 L1 D-Cache Load Miss。 因此该函数 CPI 必然大于 1,属于典型的 Memory 密集型的代码。

下面是程序的源码:

	 #include <stdlib.h>
	 #include <emmintrin.h>
	 #include <stdio.h>
	 #include <signal.h>

	 char a = 1;

	 void memory_bound() {
	         register unsigned i=0;
	         register char b;

	         for (i=0;i<(1u<<24);i++) {
	                 // evict cacheline containing a
 	                 _mm_clflush(&a);
 	                 b = a;
	         }
	 }
	 void cpu_bound() {
	         register unsigned i=0;
	         for (i=0;i<(1u<<31);i++) {
	                 __asm__ ("nop\nnop\nnop");
	         }
	 }
	 int main() {
	         memory_bound();
	         cpu_bound();
	         return 0;
	 }

在上述小程序运行时,我们使用如下命令,编译运行程序,并生成 CPI 火焰图,

$ gcc cpu_and_mem_bound.c -o cpu_and_mem_bound -gdwarf-2 -fno-omit-frame-pointer -O0

$ perf record \
    -e cpu/event=0xa2,umask=0x1,name=resource_stalls_any,period=2000003/ \
    -e cpu/event=0x3c,umask=0x0,name=cpu_clk_unhalted_thread_p,period=2000003/\
    --call-graph dwarf -F 200 ./cpu_and_mem_bound
$ perf script > out.perf

$ FlameGraph/stackcollapse-perf.pl --event-filter=cpu_clk_unhalted_thread_p \
    out.perf > out.folded.cycles
$ FlameGraph/stackcollapse-perf.pl --event-filter=resource_stalls_any \
    out.perf > out.folded.stalls
$ FlameGraph/difffolded.pl -n out.folded.stalls out.folded.cycles | \
    FlameGraph/flamegraph.pl --title "CPI Flame Graph: blue=stalls, red=instructions" \
  --width=900 > cpi_flamegraph_small.svg

最后生成的火焰图如下,

可以看到,CPI 火焰图看到的结果,是符合我们的预期的:

  • 该程序所有的 CPU 时间,都分布在 cpu_boundmemory_bound 两个函数里
  • 同是 CPU 占用时间,但 cpu_bound 是红色的,代表这个函数的指令在 CPU 上一直持续运行
  • memory_bound 是蓝色的,代表这个函数发生了严重的访问内存的延迟,导致了流水线停顿,属于忙等

4. 一个benchmark

现在,我们可以使用 CPI 火焰图来分析一个略真实一些的测试场景。下面的 CPI 火焰图,来自 fio 的测试场景。

这个 fio 对 SATA 磁盘,做多进程同步 Direct IO 顺序写,可以看到:

  • 红颜色为标记为 CPU Bound 的函数。其中颜色最深的是 _raw_spin_lock,这是自旋锁的等待循环引起的。
  • 蓝颜色为标记为 Memory Bound 的函数。其中颜色最深的是 fio 测试程序的函数 get_io_u,如果使用 perf 程序进一步分析,这个函数里发生了严重的 LLC Cache Miss。

因为 CPI 火焰图是矢量图,支持缩放,所以以上结论可以通过放大 get_io_u 的调用栈进一步确认,

到这里,读者会发现,使用 CPI 火焰图,可以很方便地做 CPU 利用率的分析,找到和定位引发 CPU 停顿的函数。一旦找到相关的函数,就可以通过 perf annotate 命令对引起停顿的指令作出进一步确认。并且,我们可以利用 1.4 小节的自顶向下分析方法,对 CPU 哪个环节产生瓶颈作出判断。最后,结合这些信息,决定优化方向。

5. 小结

本文介绍了使用 CPI 火焰图分析程序性能的方法。CPI 火焰图不但展示了程序的 Call Stack 与 CPU 占用率的关联性,而且还揭示了这些 CPU 占用率里,哪些部分是真正的有效的运行时间,哪些部分实际上是 CPU 因某些停顿造成的忙等。

系统管理员可以通过此工具发现系统存在的资源瓶颈,并且通过一些系统管理命令来缓解资源的瓶颈;例如,应用间的 Cache 颠簸干扰,可以通过将应用绑到不同的 CPU 上解决。

而应用开发者则可以通过优化相关函数,来提高程序的性能。例如,通过优化代码减少 Cache Miss,从而降低应用的 CPI 来减少处理器因访存停顿造成的性能问题。

6. 参考资料

]]>
Cache False Sharing Debug 2018-01-15T00:00:00+00:00 Oliver Yang http://oliveryang.net/2018/01/cache-false-sharing-debug

本文首发于内核月谈微信号,由吴一昊,杨勇共同完成; 转载时请包含原文或者作者网站链接:http://oliveryang.net

1. 关于本文

本文基于 Joe Mario 的一篇博客 改编而成。 Joe Mario 是 Redhat 公司的 Senior Principal Software Engineer,在系统的性能优化领域颇有建树,他也是本文描述的 perf c2c 工具的贡献者之一。 这篇博客行文比较口语化,且假设读者对 CPU 多核架构,Cache Memory 层次结构,以及 Cache 的一致性协议有所了解。 故此,笔者决定放弃照翻原文,并且基于原博客文章做了一些扩展,增加了相关背景知识简介。 本文中若有任何疏漏错误,责任在于编译者。有任何建议和意见,请回复内核月谈微信公众号,或通过 oliver.yang at linux.alibaba.com 反馈。

2. 背景知识

要搞清楚 Cache Line 伪共享的概念及其性能影响,需要对现代理器架构和硬件实现有一个基本的了解。 如果读者已经对这些概念已经有所了解,可以跳过本小节,直接了解 perf c2c 发现 Cache Line 伪共享的方法。 (注:本节中的所有图片,均来自与 Google 图片搜索,版权归原作者所有。)

2.1 存储器层次结构

众所周知,现代计算机体系结构,通过存储器层次结构 (Memory Hierarchy) 的设计,使系统在性能,成本和制造工艺之间作出取舍,从而达到一个平衡。 下图给出了不同层次的硬件访问延迟,可以看到,各个层次硬件访问延迟存在数量级上的差异,越高的性能,往往意味着更高的成本和更小的容量:

Memory Hierarchy

通过上图,可以对各级存储器 Cache Miss 带来的性能惩罚有个大致的概念。

2.2 多核架构

随着多核架构的普及,对称多处理器 (SMP) 系统成为主流。例如,一个物理 CPU 可以存在多个物理 Core,而每个 Core 又可以存在多个硬件线程。 x86 以下图为例,1 个 x86 CPU 有 4 个物理 Core,每个 Core 有两个 HT (Hyper Thread),

从硬件的角度,上图的 L1 和 L2 Cache 都被两个 HT 共享,且在同一个物理 Core。而 L3 Cache 则在物理 CPU 里,被多个 Core 来共享。 而从 OS 内核角度,每个 HT 都是一个逻辑 CPU,因此,这个处理器在 OS 来看,就是一个 8 个 CPU 的 SMP 系统。

2.3 NUMA 架构

一个 SMP 系统,按照其 CPU 和内存的互连方式,可以分为 UMA (均匀内存访问) 和 NUMA (非均匀内存访问) 两种架构。 其中,在多个物理 CPU 之间保证 Cache 一致性的 NUMA 架构,又被称做 ccNUMA (Cache Coherent NUMA) 架构。

以 x86 为例,早期的 x86 就是典型的 UMA 架构。例如下图,四路处理器通过 FSB (前端系统总线) 和主板上的内存控制器芯片 (MCH) 相连,DRAM 是以 UMA 方式组织的,延迟并无访问差异,

x86 UMA

然而,这种架构带来了严重的内存总线的性能瓶颈,影响了 x86 在多路服务器上的可扩展性和性能。

因此,从 Nehalem 架构开始,x86 开始转向 NUMA 架构,内存控制器芯片被集成到处理器内部,多个处理器通过 QPI 链路相连,从此 DRAM 有了远近之分。 而 Sandybridge 架构则更近一步,将片外的 IOH 芯片也集成到了处理器内部,至此,内存控制器和 PCIe Root Complex 全部在处理器内部了。 下图就是一个典型的 x86 的 NUMA 架构:

由于 NUMA 架构的引入,以下主要部件产生了因物理链路的远近带来的延迟差异:

  • Cache

    除物理 CPU 有本地的 Cache 的层级结构以外,还存在跨越系统总线 (QPI) 的远程 Cache 命中访问的情况。需要注意的是,远程的 Cache 命中,对发起 Cache 访问的 CPU 来说,还是被记入了 LLC Cache Miss。

  • DRAM

    在两路及以上的服务器,远程 DRAM 的访问延迟,远远高于本地 DRAM 的访问延迟,有些系统可以达到 2 倍的差异。 需要注意的是,即使服务器 BIOS 里关闭了 NUMA 特性,也只是对 OS 内核屏蔽了这个特性,这种延迟差异还是存在的。

  • Device

    对 CPU 访问设备内存,及设备发起 DMA 内存的读写活动而言,存在本地 Device 和远程 Device 的差别,有显著的延迟访问差异。

因此,对以上 NUMA 系统,一个 NUMA 节点通常可以被认为是一个物理 CPU 加上它本地的 DRAM 和 Device 组成。那么,四路服务器就拥有四个 NUMA 节点。 如果 BIOS 打开了 NUMA 支持,Linux 内核则会根据 ACPI 提供的表格,针对 NUMA 节点做一系列的 NUMA 亲和性的优化。

在 Linux 上,numactl --hardware 可以返回当前系统的 NUMA 节点信息,特别是 CPU 和 NUMA 节点的对应信息。

2.4 Cache Line

Cache Line 是 CPU 和主存之间数据传输的最小单位。当一行 Cache Line 被从内存拷贝到 Cache 里,Cache 里会为这个 Cache Line 创建一个条目。 这个 Cache 条目里既包含了拷贝的内存数据,即 Cache Line,又包含了这行数据在内存里的位置等元数据信息。

由于 Cache 容量远远小于主存,因此,存在多个主存地址可以被映射到同一个 Cache 条目的情况,下图是一个 Cache 和主存映射的概念图:

Cache Line and Memory

而这种 Cache 到主存的映射,通常是由内存的虚拟或者物理地址的某几位决定的,取决于 Cache 硬件设计是虚拟地址索引,还是物理地址索引。 然而,由于索引位一般设计为低地址位,通常在物理页的页内偏移以内,因此,不论是内存虚拟或者物理地址,都可以拿来判断两个内存地址,是否在同一个 Cache Line 里。

Cache Line 的大小和处理器硬件架构有关。在 Linux 上,通过 getconf 就可以拿到 CPU 的 Cache Line 的大小,

$getconf -a | grep CACHE
LEVEL1_ICACHE_SIZE                 32768
LEVEL1_ICACHE_ASSOC                8
LEVEL1_ICACHE_LINESIZE             64
LEVEL1_DCACHE_SIZE                 32768
LEVEL1_DCACHE_ASSOC                8
LEVEL1_DCACHE_LINESIZE             64
LEVEL2_CACHE_SIZE                  262144
LEVEL2_CACHE_ASSOC                 8
LEVEL2_CACHE_LINESIZE              64
LEVEL3_CACHE_SIZE                  15728640
LEVEL3_CACHE_ASSOC                 20
LEVEL3_CACHE_LINESIZE              64
LEVEL4_CACHE_SIZE                  0
LEVEL4_CACHE_ASSOC                 0
LEVEL4_CACHE_LINESIZE              0

2.5 Cache 的结构

前面 Linux getconf 命令的输出,除了 *_LINESIZE 指示了系统的 Cache Line 的大小是 64 字节外,还给出了 Cache 类别,大小。 其中 *_ASSOC 则指示了该 Cache 是几路关联 (Way Associative) 的。

下图很好的说明了 Cache 在 CPU 里的真正的组织结构,

一个主存的物理或者虚拟地址,可以被分成三部分:高地址位当作 Cache 的 Tag,用来比较选中多路 (Way) Cache 中的某一路 (Way),而低地址位可以做 Index,用来选中某一个 Cache Set。 在某些架构上,最低的地址位,Block Offset 可以选中在某个 Cache Line 中的某一部份。

因此,Cache Line 的命中,完全依靠地址里的 Tag 和 Index 就可以做到。关于 Cache 结构里的 Way,Set,Tag 的概念,请参考相关文档或者资料。这里就不再赘述。

2.6 Cache 一致性

如前所述,在 SMP 系统里,每个 CPU 都有自己本地的 Cache。因此,同一个变量,或者同一行 Cache Line,有在多个处理器的本地 Cache 里存在多份拷贝的可能性,因此就存在数据一致性问题。 通常,处理器都实现了 Cache 一致性 (Cache Coherence)协议。如历史上 x86 曾实现了 MESI 协议, 以及 MESIF 协议。

假设两个处理器 A 和 B, 都在各自本地 Cache Line 里有同一个变量的拷贝时,此时该 Cache Line 处于 Shared 状态。当处理器 A 在本地修改了变量,除去把本地变量所属的 Cache Line 置为 Modified 状态以外, 还必须在另一个处理器 B 读同一个变量前,对该变量所在的 B 处理器本地 Cache Line 发起 Invaidate 操作,标记 B 处理器的那条 Cache Line 为 Invalidate 状态。 随后,若处理器 B 在对变量做读写操作时,如果遇到这个标记为 Invalidate 的状态的 Cache Line,即会引发 Cache Miss, 从而将内存中最新的数据拷贝到 Cache Line 里,然后处理器 B 再对此 Cache Line 对变量做读写操作。

本文中的 Cache Line 伪共享场景,就基于上述场景来讲解,关于 Cache 一致性协议更多的细节,请参考相关文档。

2.7 Cache Line 伪共享

Cache Line 伪共享问题,就是由多个 CPU 上的多个线程同时修改自己的变量引发的。这些变量表面上是不同的变量,但是实际上却存储在同一条 Cache Line 里。 在这种情况下,由于 Cache 一致性协议,两个处理器都存储有相同的 Cache Line 拷贝的前提下,本地 CPU 变量的修改会导致本地 Cache Line 变成 Modified 状态,然后在其它共享此 Cache Line 的 CPU 上, 引发 Cache Line 的 Invaidate 操作,导致 Cache Line 变为 Invalidate 状态,从而使 Cache Line 再次被访问时,发生本地 Cache Miss,从而伤害到应用的性能。 在此场景下,多个线程在不同的 CPU 上高频反复访问这种 Cache Line 伪共享的变量,则会因 Cache 颠簸引发严重的性能问题。

下图即为两个线程间的 Cache Line 伪共享问题的示意图,

3. Perf c2c 发现伪共享

当应用在 NUMA 环境中运行,或者应用是多线程的,又或者是多进程间有共享内存,满足其中任意一条,那么这个应用就可能因为 Cache Line 伪共享而性能下降。

但是,要怎样才能知道一个应用是不是受伪共享所害呢?Joe Mario 提交的 patch 能够解决这个问题。Joe 的 patch 是在 Linux 的著名的 perf 工具上,添加了一些新特性,叫做 c2c, 意思是“缓存到缓存” (cache-2-cache)。

Redhat 在很多 Linux 的大型应用上使用了 c2c 的原型,成功地发现了很多热的伪共享的 Cache Line。 Joe 在博客里总结了一下 perf c2c 的主要功能:

  • 发现伪共享的 Cache Line
  • 谁在读写上述的 Cache Line,以及访问发生处的 Cache Line 的内部偏移
  • 这些读者和写者分别的 pid, tid, 指令地址,函数名,二进制文件
  • 每个读者和写者的源代码文件,代码行号
  • 这些热点 Cache Line 上的,load 操作的平均延迟
  • 这些 Cache Line 的样本来自哪些 NUMA 节点, 由哪些 CPU 参与了读写

perf c2cperf 里现有的工具比较类似:

  • 先用 perf c2c record 通过采样,收集性能数据
  • 再用 perf c2c report 基于采样数据,生成报告

如果想了解 perf c2c 的详细使用,请访问: PERF-C2C(1)

这里还有一个完整的 perf c2c输出的样例

最后,还有一个小程序的源代码,可以产生大量的 Cache Line 伪共享,用以测试体验: Fasle sharing .c src file

3.1 perf c2c 的输出

下面,让我们就之前给出的 perf c2c 的输出样例,做一个详细介绍。

输出里的第一个表,概括了 CPU 在 perf c2c 数据采样期间做的 load 和 store 的样本。能够看到 load 操作都是在哪里取到了数据。

perf c2c 输出里,HITM 意为 “Hit In The Modified”,代表 CPU 在 load 操作命中了一条标记为 Modified 状态的 Cache Line。如前所述,伪共享发生的关键就在于此。

Remote HITM,意思是跨 NUMA 节点的 HITM,这个是所有 load 操作里代价最高的情况,尤其在读者和写者非常多的情况下,这个代价会变得非常的高。

对应的,Local HITM,则是本地 NUMA 节点内的 HITM,下面是对 perf c2c 输出的详细注解:

 1  =================================================
 2              Trace Event Information
 3  =================================================
 4    Total records                     :     329219  >> 采样到的 CPU load 和 store 的样本总数
 5    Locked Load/Store Operations      :      14654
 6    Load Operations                   :      69679  >> CPU load 操作的样本总数
 7    Loads - uncacheable               :          0
 8    Loads - IO                        :          0
 9    Loads - Miss                      :       3972
10    Loads - no mapping                :          0
11    Load Fill Buffer Hit              :      11958  >> Load 操作没有命中 L1 Cache,但命中了 L1 Cache 的 Fill Buffer 的次数
12    Load L1D hit                      :      17235  >> Load 操作命中 L1 Dcache 的次数
13    Load L2D hit                      :         21  >> Load 操作命中 L2 Dcache 的次数
14    Load LLC hit                      :      14219  >> Load 操作命中最后一级 (LLC) Cache (通常 LLC 是 L3) 的次数
15    Load Local HITM                   :       3402  >> Load 操作命中了本地 NUMA 节点的修改过的 Cache 的次数
16    Load Remote HITM                  :      12757  >> Load 操作命中了远程 NUMA 节点的修改过的 Cache 的次数
17    Load Remote HIT                   :       5295  >> Load 操作命中了远程未修改的 Clean Cache 的次数
18    Load Local DRAM                   :        976  >> Load 操作命中了本地 NUMA 节点的内存的次数,其实这就是 Cache Miss
19    Load Remote DRAM                  :       3246  >> Load 操作命中了远程 NUMA 节点的内存的次数,其实这是比 Load Local DRAM 更严重的 Cache Miss
20    Load MESI State Exclusive         :       4222  >> Load 操作命中 MESI 状态中,处于 Exclusive 状态的 Cache 的次数
21    Load MESI State Shared            :          0  >> Load 操作命中 MESI 状态中,处于 Shared 状态的 Cache 的次数
22    Load LLC Misses                   :      22274  >> Load 操作产生的本地 NUMA 节点 LLC Cache Miss 的次数,是 Load Remote HITM,Load Remote HIT,Load Local DRAM,Load Remote DRAM 之和
23    LLC Misses to Local DRAM          :        4.4% >> Load 操作产生的 LLC Cache Miss 中,从本地 NUMA 节点拿到内存的样本占 Load LLC Misses 总样本的百分比
24    LLC Misses to Remote DRAM         :       14.6% >> Load 操作产生的 LLC Cache Miss 中,从远程 NUMA 节点拿到内存的样本占 Load LLC Misses 总样本的百分比
25    LLC Misses to Remote cache (HIT)  :       23.8% >> Load 操作产生的 LLC Cache Miss 中,从远程 NUMA 节点拿到 Clean Cache 的样本占 Load LLC Misses 总样本的百分比
26    LLC Misses to Remote cache (HITM) :       57.3% >> Load 操作产生的 LLC Cache Miss 中,从远程 NUMA 节点拿到被修改过的 Cache 的样本占 Load LLC Misses 总样本的百分比,这是代价最高的伪共享
27    Store Operations                  :     259539  >> CPU store 操作的样本总数
28    Store - uncacheable               :          0
29    Store - no mapping                :         11
30    Store L1D Hit                     :     256696  >> Store 操作命中 L1 Dcache 的次数
31    Store L1D Miss                    :       2832  >> Store 操作命中 L1 Dcache Miss 的次数
32    No Page Map Rejects               :       2376
33    Unable to parse data source       :          1

perf c2c 输出的第二个表, 以 Cache Line 维度,全局展示了 CPU load 和 store 活动的情况。 这个表的每一行是一条 Cache Line 的数据,显示了发生伪共享最热的一些 Cache Line。默认按照发生 Remote HITM 的次数比例排序,改下参数也可以按照发生 Local HITM 的次数比例排序。

要检查 Cache Line 伪共享问题,就在这个表里找 Rmt LLC Load HITM(即跨 NUMA 节点缓存里取到数据的)次数比较高的,如果有,就得深挖一下。

54  =================================================
55             Shared Data Cache Line Table
56  =================================================
57  #
58  #                              Total      Rmt  ----- LLC Load Hitm -----  ---- Store Reference ----  --- Load Dram ----      LLC    Total  ----- Core Load Hit -----  -- LLC Load Hit --
59  # Index           Cacheline  records     Hitm    Total      Lcl      Rmt    Total    L1Hit   L1Miss       Lcl       Rmt  Ld Miss    Loads       FB       L1       L2       Llc       Rmt
60  # .....  ..................  .......  .......  .......  .......  .......  .......  .......  .......  ........  ........  .......  .......  .......  .......  .......  ........  ........
61  #
62        0            0x602180   149904   77.09%    12103     2269     9834   109504   109036      468       727      2657    13747    40400     5355    16154        0      2875       529
63        1            0x602100    12128   22.20%     3951     1119     2832        0        0        0        65       200     3749    12128     5096      108        0      2056       652
64        2  0xffff883ffb6a7e80      260    0.09%       15        3       12      161      161        0         1         1       15       99       25       50        0         6         1
65        3  0xffffffff81aec000      157    0.07%        9        0        9        1        0        1         0         7       20      156       50       59        0        27         4
66        4  0xffffffff81e3f540      179    0.06%        9        1        8      117       97       20         0        10       25       62       11        1        0        24         7

下面是共享 Cache Line 的 Pareto 百分比分布表,命名取自帕累托法则 (Pareto principle),即 2/8 法则的喻义,显示了每条内部产生竞争的 Cache Line 的百分比分布的细目信息。 这是最重要的一个表。为了精简,这里只展示了三条 Cache Line 相关的记录,表格里包含了这些信息:

  • 其中 71,72 行是列名,每列都解释了Cache Line的一些活动。
  • 标号为 76,85,91 的行显示了每条 Cache Line 的 HITM 和 store 活动情况: 依次是 CPU load 和 store 活动的计数,以及 Cache Line 的虚拟地址。
  • 78 到 83 行,是针对 76 行 Cache Line 访问的细目统计,具体格式如下:
    • 首先是百分比分布,包含了 HITM 中 remote 和 local 的百分比,store 里的 L1 Hit 和 Miss 的百分比分布。注意,这些百分比纵列相加正好是 100%。
    • 然后是数据地址列。上面提到了 76 行显示了 Cache Line 的虚拟地址,而下面几行的这一列则是行内偏移。
    • 下一列显示了pid,或线程id(如果设置了要输出tid)。
    • 接下来是指令地址。
    • 接下来三列,展示了平均load操作的延迟。我常看着里有没有很高的平均延迟。这个平均延迟,可以反映该行的竞争紧张程度。
    • cpu cnt列展示了该行访问的样本采集自多少个cpu。
    • 然后是函数名,二进制文件名,源代码名,和代码行数。
    • 最后一列展示了对于每个节点,样本分别来自于哪些cpu

以下为样例输出:

67  =================================================
68        Shared Cache Line Distribution Pareto      
69  =================================================
70  #
71  #        ----- HITM -----  -- Store Refs --        Data address                               ---------- cycles ----------       cpu                                     Shared                                   
72  #   Num      Rmt      Lcl   L1 Hit  L1 Miss              Offset      Pid        Code address  rmt hitm  lcl hitm      load       cnt               Symbol                Object                  Source:Line  Node{cpu list}
73  # .....  .......  .......  .......  .......  ..................  .......  ..................  ........  ........  ........  ........  ...................  ....................  ...........................  ....
74  #
75    -------------------------------------------------------------
76        0     9834     2269   109036      468            0x602180
77    -------------------------------------------------------------
78            65.51%   55.88%   75.20%    0.00%                 0x0    14604            0x400b4f     27161     26039     26017         9  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:144   0{0-1,4}  1{24-25,120}  2{48,54}  3{169}
79             0.41%    0.35%    0.00%    0.00%                 0x0    14604            0x400b56     18088     12601     26671         9  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:145   0{0-1,4}  1{24-25,120}  2{48,54}  3{169}
80             0.00%    0.00%   24.80%  100.00%                 0x0    14604            0x400b61         0         0         0         9  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:145   0{0-1,4}  1{24-25,120}  2{48,54}  3{169}
81             7.50%    9.92%    0.00%    0.00%                0x20    14604            0x400ba7      2470      1729      1897         2  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:154   1{122}  2{144}
82            17.61%   20.89%    0.00%    0.00%                0x28    14604            0x400bc1      2294      1575      1649         2  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:158   2{53}  3{170}
83             8.97%   12.96%    0.00%    0.00%                0x30    14604            0x400bdb      2325      1897      1828         2  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:162   0{96}  3{171}

84    -------------------------------------------------------------
85        1     2832     1119        0        0            0x602100
86    -------------------------------------------------------------
87            29.13%   36.19%    0.00%    0.00%                0x20    14604            0x400bb3      1964      1230      1788         2  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:155   1{122}  2{144}
88            43.68%   34.41%    0.00%    0.00%                0x28    14604            0x400bcd      2274      1566      1793         2  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:159   2{53}  3{170}
89            27.19%   29.40%    0.00%    0.00%                0x30    14604            0x400be7      2045      1247      2011         2  [.] read_write_func  no_false_sharing.exe  false_sharing_example.c:163   0{96}  3{171}

90    -------------------------------------------------------------
91        2       12        3      161        0  0xffff883ffb6a7e80
92    -------------------------------------------------------------
93            58.33%  100.00%    0.00%    0.00%                 0x0    14604  0xffffffff810cf16d      1380       941      1229         9  [k] task_tick_fair              [kernel.kallsyms]  atomic64_64.h:21   0{0,4,96}  1{25,120,122}  2{53}  3{170-171}
94            16.67%    0.00%   98.76%    0.00%                 0x0    14604  0xffffffff810c9379      1794         0       625        13  [k] update_cfs_rq_blocked_load  [kernel.kallsyms]  atomic64_64.h:45   0{1,4,96}  1{25,120,122}  2{48,53-54,144}  3{169-171}
95            16.67%    0.00%    0.00%    0.00%                 0x0    14604  0xffffffff810ce098      1382         0       867        12  [k] update_cfs_shares           [kernel.kallsyms]  atomic64_64.h:21   0{1,4,96}  1{25,120,122}  2{53-54,144}  3{169-171}
96             8.33%    0.00%    0.00%    0.00%                 0x8    14604  0xffffffff810cf18c      2560         0       679         8  [k] task_tick_fair              [kernel.kallsyms]  atomic.h:26        0{4,96}  1{24-25,120,122}  2{54}  3{170}
97             0.00%    0.00%    1.24%    0.00%                 0x8    14604  0xffffffff810cf14f         0         0         0         2  [k] task_tick_fair              [kernel.kallsyms]  atomic.h:50        2{48,53}

3.2 如何用 perf c2c

下面是常见的 perf c2c 使用的命令行:

# perf c2c record -F 60000 -a --all-user sleep 5
# perf c2c record -F 60000 -a --all-user sleep 3     // 采样较短时间
# perf c2c record -F 60000 -a --all-kernel sleep 3   // 只采样内核态样本
# perf c2c record -F 60000 -a -u --ldlat 50 sleep 3  // 或者只采集 load 延迟大于 60 个(默认是 30 个)机器周期的样本

熟悉 perf 的读者可能已经注意到,这里的 -F 选项指定了非常高的采样频率: 60000。请特别注意:这个采样频率不建议在线上或者生产环境使用,因为这会在高负载机器上带来不可预知的影响。 此外 perf c2c 对 CPU load 和 store 操作的采样会不可避免的影响到被采样应用的性能,因此建议在研发测试环境使用 perf c2c 去优化应用。

对采样数据的分析,可以使用带图形界面的 tui 来看输出,或者只输出到标准输出

# perf report -NN -c pid,iaddr                 // 使用tui交互式界面
# perf report -NN -c pid,iaddr --stdio         // 或者输出到标准输出
# perf report -NN -d lcl -c pid,iaddr --stdio  // 或者按 local time 排序

默认情况,为了规范输出格式,符号名被截断为定长,但可以用 “–full-symbols” 参数来显示完整符号名。

例如:

# perf c2c report -NN -c pid,iaddr --full-symbols --stdio

3.3 找到 Cache Line 访问的调用栈

有的时候,很需要找到读写这些 Cache Line 的调用者是谁。下面是获得调用图信息的方法。但一开始,一般不会一上来就用这个,因为输出太多,难以定位伪共享。一般都是先找到问题,再回过头来使用调用图。

# perf c2c record --call-graph dwarf,8192 -F 60000 -a --all-user sleep 5
# perf c2c report -NN -g --call-graph -c pid,iaddr --stdio

3.4 如何增加采样频率

为了让采样数据更可靠,会把 perf 采样频率提升到 -F 60000 或者 -F 80000,而系统默认的采样频率是 1000。

提升采样频率,可以短时间获得更丰富,更可靠的采样集合。想提升采样频率,可以用下面的方法。 可以根据 dmesg 里有没有 perf interrupt took too long … 信息来调整频率。注意,如前所述,这有一定风险,严禁在线上或者生产环境使用

# echo    500 > /proc/sys/kernel/perf_cpu_time_max_percent
# echo 100000 > /proc/sys/kernel/perf_event_max_sample_rate

然后运行前面讲的 perf c2c record 命令。之后再运行,

# echo     50 > /proc/sys/kernel/perf_cpu_time_max_percent

3.5 如何让避免采样数据过量

在大型系统上(比如有 4,8,16 个物理 CPU 插槽的系统)运行 perf c2c,可能会样本太多,消耗大量的CPU时间,perf.data文件也可能明显变大。 对于这个问题,有以下建议(包含但不仅限于):

  • ldlat 从 30 增加大到 50。这使得 perf 跳过没有性能问题的 load 操作。
  • 降低采样频率。
  • 缩短 perf record 的睡眠时间窗口。比如,从 sleep 5 改成 sleep 3

3.6 使用 c2c 优化应用的收获

一般搭建看见性能工具的输出,都会问这些数据意味着什么。Joe 总结了他使用 c2c 优化应用时,学到的东西,

  • perf c2c 采样时间不宜过长。Joe 建议运行 perf c2c 3 秒、5 秒或 10 秒。运行更久,观测到的可能就不是并发的伪共享,而是时间错开的 Cache Line 访问。
  • 如果对内核样本没有兴趣,只想看用户态的样本,可以指定 --all-user。反之使用 --all-kernel
  • CPU 很多的系统上(如 >148 个),设置 -ldlat 为一个较大的值(50 甚至 70),perf 可能能产生更丰富的C2C样本。
  • 读最上面那个具有概括性的 Trace Event 表,尤其是 LLC Misses to Remote cache HITM 的数字。只要不是接近 0,就可能有值得追究的伪共享。
  • Pareto 表时,需要关注的,多半只是最热的两三个 Cache Line。
  • 有的时候,一段代码,它不在某一行 Cache Line 上竞争严重,但是它却在很多 Cache Line 上竞争,这样的代码段也是很值得优化的。同理还有多进程程序访问共享内存时的情况。
  • Pareto 表里,如果发现很长的 load 操作平均延迟,常常就表明存在严重的伪共享,影响了性能。
  • 接下来去看样本采样自哪些节点和 CPU,据此进行优化,将哪些内存或 Task 进行 NUMA 节点锁存。

最后,Pareto 表还能对怎么解决对齐得很不好的Cache Line,提供灵感。 例如:

  • 很容易定位到:写地很频繁的变量,这些变量应该在自己独立的 Cache Line。可以据此进行对齐调整,让他们不那么竞争,运行更快,也能让其它的共享了该 Cache Line 的变量不被拖慢。
  • 很容易定位到:没有 Cache Line 对齐的,跨越了多个 Cache Line 的热的 Lock 或 Mutex。
  • 很容易定位到:读多写少的变量,可以将这些变量组合到相同或相邻的 Cache Line。

3.7 使用原始的采样数据

有时直接去看用 perf c2c record 命令生成的 perf.data 文件,其中原始的采样数据也是有用的。 可以用 perf script 命令得到原始样本,man perf-script 可以查看这个命令的手册。输出可能是编码过的,但你可以按 load weight 排序(第 5 列),看看哪个 load 样本受伪共享影响最严重,有最大的延迟。

4 致谢

最后,在文章末尾,Joe 给出了如下总结,并在博客中致谢了所有的贡献者:

Linux perf c2c 功能在上游的 4.2 内核已经可用了。这是集体努力的结果。

]]>
Linux High Loadavg Analysis - 1 2017-12-06T00:00:00+00:00 Oliver Yang http://oliveryang.net/2017/12/linux-high-loadavg-analysis-1

文本首发于 Linuxer 微信订阅号,文章原文为:浅谈 Linux 高负载的系统化分析。 转载时请包含原文或者作者网站链接:http://oliveryang.net

从消除误解开始

讲解 Linux Load 高如何排查的话题属于老生常谈了,但多数文章只是聚焦了几个点,缺少整体排查思路的介绍。所谓 “授人以鱼不如授人以渔”。本文试图建立一个方法和套路,来帮助读者对 Load 高问题排查有一个更全面的认识。

没有基线的 Load,是不靠谱的 Load

从接触 Unix/Linux 系统管理的第一天起,很多人就开始接触 System Load Average 这个监控指标了,然而,并非所有人都知道这个指标的真正含义。一般说来,经常能听到以下误解:

  • Load 高是 CPU 负载高……

    传统 Unix 于 Linux 设计不同。Unix 系统,Load 高就是可运行进程多引发的,但对 Linux 来说不是。对 Linux 来说 Load 高可能有两种情况:

    • 系统中处于 R 状态的进程数增加引发的
    • 系统中处于 D 状态的进程数增加引发的
  • Loadavg 数值大于某个值就一定有问题……

    Loadavg 的数值是相对值,受到 CPU 和 IO 设备多少的影响,甚至会受到某些软件定义的虚拟资源的影响。Load 高的判断需要基于某个历史基线 (Baseline),不能无原则的跨系统去比较 Load。

  • Load 高系统一定很忙…..

Load 高系统可以很忙,例如 CPU 负载高,CPU 很忙。但 Load 高,系统不都很忙,如 IO 负载高,磁盘可以很忙,但 CPU 可以比较空闲,如 iowait 高。这里要注意,iowait 本质上是一种特殊的 CPU 空闲状态。另一种 Load 高,可能 CPU 和磁盘外设都很空闲,可能支持锁竞争引起的,这时候 CPU 时间里,iowait 不高,但 idle 高。

Brendan Gregg 在最近的博客 Linux Load Averages: Solving the Mystery 中,讨论了 Unix 和 Linux Load Average 的差异,并且回朔到 24 年前 Linux 社区的讨论,并找到了当时为什么 Linux 要修改 Unix Load Average 的定义。文章认为,正是由于 Linux 引入的 D 状态线程的计算方式,从而导致 Load 高的原因变得含混起来。因为系统中引发 D 状态切换的原因实在是太多了,绝非 IO 负载,锁竞争这么简单!正是由于这种含混,Load 的数值更加难以跨系统,跨应用类型去比较。所有 Load 高低的依据,全都应该基于历史的基线。本文无意过多搬运原文的内容,因此,进一步的细节,建议阅读原文。

如何排查 Load 高的问题

如前所述,由于在 Linux 操作系统里,Load 是一个定义及其含混的指标,排查 loadavg 高就是一个很复杂的过程。其基本思路就是,根据引起 Load 变化的根源是 R 状态任务增多,还是 D 状态任务增多,来进入到不同的流程。

这里给出了 Load 增高的排查的一般套路,仅供参考:

loadavg_debug.png

在 Linux 系统里,读取 /proc/stat 文件,即可获取系统中 R 状态的进程数;但 D 状态的任务数恐怕最直接的方式还是使用 ps 命令比较方便。而 /proc/stat 文件里 procs_blocked 则给出的是处于等待磁盘 IO 的进程数:

$cat /proc/stat 
.......
processes 50777849
procs_running 1
procs_blocked 0
......

通过简单区分 R 状态任务增多,还是 D 状态任务增多,我们就可以进入到不同的排查流程里。下面,我们就这个大图的排查思路,做一个简单的梳理。

R 状态任务增多

即通常所说的 CPU 负载高。此类问题的排查定位主要思路是系统,容器,进程的运行时间分析上,找到在 CPU 上的热点路径,或者分析 CPU 的运行时间主要是在哪段代码上。

CPU usersys 时间的分布通常能帮助人们快速定位与用户态进程有关,还是与内核有关。另外,CPU 的 run queue 长度和调度等待时间,非主动的上下文切换 (nonvoluntary context switch) 次数都能帮助大致理解问题的场景。

因此,如果要将问题的场景关联到相关的代码,通常需要使用 perfsystemtap, ftrace 这种动态的跟踪工具。

关联到代码路径后,接下来的代码时间分析过程中,代码中的一些无效的运行时间也是分析中首要关注的,例如用户态和内核态中的自旋锁 (Spin Lock)。

当然,如果 CPU 上运行的都是有非常意义,非常有效率的代码,那唯一要考虑的就是,是不是负载真得太大了。

D 状态任务增多

根据 Linux 内核的设计, D 状态任务本质上是 TASK_UNINTERRUPTIBLE 引发的主动睡眠,因此其可能性非常多。但是由于 Linux 内核 CPU 空闲时间上对 IO 栈引发的睡眠做了特殊的定义,即 iowait,因此 iowait 成为 D 状态分类里定位是否 Load 高是由 IO 引发的一个重要参考。

当然,如前所述, /proc/stat 中的 procs_blocked 的变化趋势也可以是一个非常好的判定因 iowait 引发的 Load 高的一个参考。

CPU iowait

很多人通常都对 CPU iowait 有一个误解,以为 iowait 高是因为这时的 CPU 正在忙于做 IO 操作。其实恰恰相反, iowait 高的时候,CPU 正处于空闲状态,没有任何任务可以运行。只是因为此时存在已经发出的磁盘 IO,因此这时的空闲状态被标识成了 iowait ,而不是 idle

但此时,如果用 perf probe 命令,我们可以清楚得看到,在 iowait 状态的 CPU,实际上是运行在 pid 为 0 的 idle 线程上:

$ sudo perf probe -a account_idle_ticks
$sudo perf record -e probe:account_idle_ticks -ag sleep 1
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.418 MB perf.data (843 samples) ]

$sudo perf script
swapper     0 [013] 5911414.451891: probe:account_idle_ticks: (ffffffff810b6af0)
              2b6af1 account_idle_ticks (/lib/modules/3.10.0/build/vmlinux)
              2d65d9 cpu_startup_entry (/lib/modules/3.10.0/build/vmlinux)
              24840a start_secondary (/lib/modules/3.10.0/build/vmlinux)

相关的 idle 线程的循环如何分别对 CPU iowaitidle 计数的代码,如下所示:

/*       
 * Account multiple ticks of idle time.
 * @ticks: number of stolen ticks
 */   
void account_idle_ticks(unsigned long ticks)
{        
         
    if (sched_clock_irqtime) {
        irqtime_account_idle_ticks(ticks);
        return;
    }   
         
    account_idle_time(jiffies_to_cputime(ticks));                                                                                                                                                       
}        
         

/*
 * Account for idle time.
 * @cputime: the cpu time spent in idle wait
 */
void account_idle_time(cputime_t cputime)                                                                                                                                                               
{
    u64 *cpustat = kcpustat_this_cpu->cpustat;
    struct rq *rq = this_rq();
 
    if (atomic_read(&rq->nr_iowait) > 0)
        cpustat[CPUTIME_IOWAIT] += (__force u64) cputime;
    else
        cpustat[CPUTIME_IDLE] += (__force u64) cputime;
}

而 Linux IO 栈和文件系统的代码则会调用 io_schedule,等待磁盘 IO 的完成。这时候,对 CPU 时间被记为 iowait 起关键计数的原子变量 rq->nr_iowait 则会在睡眠前被增加。注意,io_schedule 在被调用前,通常 caller 会先将任务显式地设置成 TASK_UNINTERRUPTIBLE 状态:

/*           
 * This task is about to go to sleep on IO. Increment rq->nr_iowait so
 * that process accounting knows that this is a task in IO wait state.
 */          
void __sched io_schedule(void)
{            
    io_schedule_timeout(MAX_SCHEDULE_TIMEOUT);
}            
EXPORT_SYMBOL(io_schedule);
             
long __sched io_schedule_timeout(long timeout)
{            
    int old_iowait = current->in_iowait;
    struct rq *rq; 
    long ret;
             
    current->in_iowait = 1; 
    if (old_iowait)
        blk_schedule_flush_plug(current);
    else 
        blk_flush_plug(current);
             
    delayacct_blkio_start();
    rq = raw_rq();
    atomic_inc(&rq->nr_iowait);
    ret = schedule_timeout(timeout);                                                                                                                                                                        
    current->in_iowait = old_iowait;
    atomic_dec(&rq->nr_iowait);
    delayacct_blkio_end();
             
    return ret; 
}            
EXPORT_SYMBOL(io_schedule_timeout);

CPU idle

如前所述,有相当多的内核的阻塞,即 TASK_UNINTERRUPTIBLE 的睡眠,实际上与等待磁盘 IO 无关,如内核中的锁竞争,再如内存直接页回收的睡眠,又如内核中一些代码路径上的主动阻塞,等待资源。

Brendan Gregg 在最近的博客 Linux Load Averages: Solving the Mystery中,使用 perf 命令产生的 TASK_UNINTERRUPTIBLE 的睡眠的火焰图,很好的展示了引起 CPU idle 高的多样性。本文不在赘述。

Uninterruptable sleep flame graph

因此,CPU idle 高的分析,实质上就是分析内核的代码路径引起阻塞的主因是什么。通常,我们可以使用 perf injectperf record 记录的上下文切换的事件进行处理,关联出进程从 CPU 切出 (swtich out) 和再次切入 (switch in) 的内核代码路径,生成一个所谓的 Off CPU 火焰图.

当然,类似于锁竞争这样的比较简单的问题,Off CPU 火焰图足以一步定位出问题。但是对于更加复杂的因 D 状态而阻塞的延迟问题,可能 Off CPU 火焰图只能给我们一个调查的起点。

例如,当我们看到,Off CPU 火焰图的主要睡眠时间是因为 epoll_wait 等待引发的。那么,我们继续要排查的应该是网络栈的延迟,即本文大图中的 Net Delay 这部分。

至此,你也许会发现,CPU iowaitidle 高的性能分析的实质就是 延迟分析。这就是大图按照内核中资源管理的大方向,将延迟分析细化成了六大延迟分析

  • CPU 延迟
  • 内存延迟
  • 文件系统延迟
  • IO 栈延迟
  • 网络栈延迟
  • 锁及同步原语竞争

任何上述代码路径引发的 TASK_UNINTERRUPTIBLE 的睡眠,都是我们要分析的对象!

以问题结束

限于篇幅,本文很难将其所涉及的细节一一展开,因为读到这里,你也许会发现,原来 Load 高的分析,实际上就是对系统的全面负载分析。怪不得叫 System Load 呢。这也是 Load 分析为什么很难在一篇文章里去全面覆盖。

本文也开启了浅谈 Linux 性能分析系列的第一章。后续我们会推出系列文章,就前文所述的六大延迟分析,一一展开介绍,敬请期待……

]]>
Linux Memory Pressure - 1 2017-06-11T00:00:00+00:00 Oliver Yang http://oliveryang.net/2017/06/linux-memroy-pressure

草稿状态,请勿装载。转载时请包含作者网站链接:http://oliveryang.net

1. 内存压力的定义

当系统内存的分配,访问,释放因资源竞争而导致了恶化,进而影响到了应用软件的性能指标,我们就称系统处于内存压力之下。

这里面强调了以下几点:

  • 内存资源使用的三个方面:分配,访问,释放。

    三个方面中,内存访问是关键,内存的分配和释放通常会影响到内存的访问性能。 由于大多数现代操作系统都实现了虚拟内存管理,并采用了 Demand Paging 的设计,因此影响到应用性能指标的物理内存的分配和释放,经常发生在该虚拟地址首次被访问时。 例如,访问内存引起 page fault,这时引发了物理页的分配。若物理页不足,则又引发页回收,导致脏页或者匿名页写入磁盘,然后页面释放后被回收分配。

  • 内存资源竞争:可以是硬件层面和软件层面的竞争。

    由于现代处理器的设计,访问内存硬件资源的竞争可以发生在各个层面,下面以 x86 为例:

    • Hyper thread 引起流水线 frontend 和 bandend 的竞争,引发访存停滞 (Stall)。
    • 多级 TLB 和 paging sructure cache,因进程上下文切换或者内存映射改变而失效 (TLB flush),或因 TLB miss 而引发延迟。
    • 多级 Cache,因 Cache miss 而引发延迟。
    • NUMA 架构, 因 CPU 本地访存 (iMC) 或者远程访存 (QPI) 而引发内存访问延迟上的差异。同样地,设备 DMA 也有本地和远程访存延迟差异。

    软件层面的竞争就更多了,由于 Linux 内核虚拟内存的设计,可能发生如下领域的竞争:

    • Page fault
    • 页分配
    • 页回收
    • 用户内存和内核内存管理的开销:如元数据状态更新和锁竞争。
  • 应用可感知:内存资源竞争而引起其性能指标的变化,从用户角度可感知才有意义。

    一个应用的端到端的操作,可以转化成为成百上千次访存操作。当一个端到端操作可以量化到 N 次访存操作时,访存的性能指标的差异变得可以估算起来。

2. 内存性能指标

下面将从三个维度去考量内存的性能指标。从应用的角度看,它们对应用的端到端性能可能会造成一定的影响。

2.1 Throughput

内存访问吞吐量。通常计量方式为 BPS,即每秒的字节数。

例如,Intel Broadwell 某个系统的硬件内存带宽上限为 130GB/s。那么我们可以利用 perf -e intel_cqm 命令观查当前的应用的内存使用的吞吐量,看是否达到了系统硬件的带宽瓶颈。 详情请参考关联文档中的 Introduction to Cache Quality of service in Linux Kernel 这篇文档。

TBD.

2.2 IOPS

内存访问每秒操作次数。通常计量方式为 IOPS。对内存来说,IO 操作分为 Load 和 Store,IOPS 即每秒的 Load/Store 次数。

由于现代处理器存在非常复杂的 Memory Cache Hierarchy 的设计,因此,软件发起的一条引起内存 Read 访存指令,可能引起 Cache Evition 操作,进而触发内存的 Store 操作。 而一条引起内存 Write 访存指令,可能也会引起 Cache Miss 操作,进而引发内存的 Load 操作。 因此,要观察系统的内存 IOPS 指标,只能借助 CPU 的 PMU 机制。

例如,Intel 的 PMU 支持 PEBS 机制,可以利用 perf mem 命令来观察当前应用的内存访问的 IOPS。

TBD.

2.3 Latency

内存访问的延迟。通常计量单位为时间。

内存访问延迟的时间单位,从纳秒 (ns), 微妙 (us), 毫秒 (ms),甚至秒 (s) 不等,这取决于硬件和软件上的很多因素:

  • 当内存访问命中 cache 时,访存延迟通常是 ns 级别的。
  • 若内存访问引起操作系统的 page fault,访存延迟对应用来说就推迟到了 us ,甚至百 us 级别。
  • 若内存访问时的 page fault 遭遇了物理内存紧张,引发了操作系统的页回收,访存延迟可以推迟到 ms 级,一些极端情况可以是百 ms 级,直至 s 级。

因此,观察内存访问延迟并没有通用的,很方便的工具。软件层面的访存延迟,可以利用 OS 的动态跟踪工具。例如 page fult 的延迟,可以利用 ftrace 和 perf 之类的工具观测。

3. 影响内存访问性能的因素

很多因素可以影响到系统内存的性能,

  • 资源总数量的竞争

  • 软件并发访问竞争

  • 总线并发访问竞争

  • 内存的局部性问题

TBD。

4. 关联文档

]]>
Linux perf sched Summary 2017-04-24T00:00:00+00:00 Oliver Yang http://oliveryang.net/2017/04/linux-perf-sched

本文翻译自 Brendan Gregg 博客文章,所有权利归原作者所有.
译者: Oliver Yang
原文链接: http://www.brendangregg.com/blog/2017-03-16/perf-sched.html

1. Overview

随着 Linux 4.10 perf sched timehist 新特性的推出,Linux 的 perf 工具又增加了一个新的 CPU 调度器性能分析的方式。因为我之前从未谈到过 perf sched 这个子命令,本文将对其功能做一个总结。 对急于了解本文是否对自己有用的同学,可以先大致浏览一下本文中的命令输出截屏。这些 perf sched 命令的例子也被加入到之前维护的 perf exmaples 页面里。

perf sched 使用了转储后再分析 (dump-and-post-process) 的方式来分析内核调度器的各种事件。而这往往带来一些问题,因为这些调度事件通常非常地频繁,可以达到每秒钟百万级, 进而导致 CPU,内存和磁盘的在调度器事件记录上的严重开销。我最近一直在用 eBPF/bcc (包括 runqlat) 来写内核调度器分析工具,使用 eBPF 特性在内核里直接对调度事件的分析处理,可以极大的减少这种事件记录的开销。但是有一些性能分析场景里,我们可能想用 perf shed 命令去记录每一个调度事件, 尽管比起 ebpf 的方式,这会带来更大的开销。想象一下这个场景,你有只有五分钟时间去分析一个有问题的云的虚拟机实例,在这个实例自动销毁之前,你想要为日后的各种分析去记录每个调度事件。

我们以记录一秒钟内的所有调度事件作为开始:

# perf sched record -- sleep 1
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 1.886 MB perf.data (13502 samples) ]

这个命令的结果是一秒钟记录了 1.9 Mb 的数据,其中包含了 13,502 个调度事件样本。数据的大小和速度和系统的工作负载以及 CPU 数目的多少直接相关 (本例中正在一个 8 CPU 的服务器上 build 一个软件)。 事件记录如何被写入文件系统的过程已经被优化过了:为减少记录开销,perf 命令仅仅被唤醒一次,去读事件缓冲区里的数据,然后写到磁盘上。因此这种方式下,仍旧会有非常显著的开销, 包括调度器事件的产生和文件系统的数据写入。

这些调度器事件包括,

# perf script --header
# ========
# captured on: Sun Feb 26 19:40:00 2017
# hostname : bgregg-xenial
# os release : 4.10-virtual
# perf version : 4.10
# arch : x86_64
# nrcpus online : 8
# nrcpus avail : 8
# cpudesc : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz
# cpuid : GenuineIntel,6,62,4
# total memory : 15401700 kB
# cmdline : /usr/bin/perf sched record -- sleep 1 
# event : name = sched:sched_switch, , id = { 2752, 2753, 2754, 2755, 2756, 2757, 2758, 2759...
# event : name = sched:sched_stat_wait, , id = { 2760, 2761, 2762, 2763, 2764, 2765, 2766, 2...
# event : name = sched:sched_stat_sleep, , id = { 2768, 2769, 2770, 2771, 2772, 2773, 2774, ...
# event : name = sched:sched_stat_iowait, , id = { 2776, 2777, 2778, 2779, 2780, 2781, 2782,...
# event : name = sched:sched_stat_runtime, , id = { 2784, 2785, 2786, 2787, 2788, 2789, 2790...
# event : name = sched:sched_process_fork, , id = { 2792, 2793, 2794, 2795, 2796, 2797, 2798...
# event : name = sched:sched_wakeup, , id = { 2800, 2801, 2802, 2803, 2804, 2805, 2806, 2807...
# event : name = sched:sched_wakeup_new, , id = { 2808, 2809, 2810, 2811, 2812, 2813, 2814, ...
# event : name = sched:sched_migrate_task, , id = { 2816, 2817, 2818, 2819, 2820, 2821, 2822...
# HEADER_CPU_TOPOLOGY info available, use -I to display
# HEADER_NUMA_TOPOLOGY info available, use -I to display
# pmu mappings: breakpoint = 5, power = 7, software = 1, tracepoint = 2, msr = 6
# HEADER_CACHE info available, use -I to display
# missing features: HEADER_BRANCH_STACK HEADER_GROUP_DESC HEADER_AUXTRACE HEADER_STAT 
# ========
#
    perf 16984 [005] 991962.879966:   sched:sched_wakeup: comm=perf pid=16999 prio=120 target_cpu=005
	[...]

perf sched 可以用几种不同的方式记录调度事件,其 help 子命令总结如下:

# perf sched -h

 Usage: perf sched [] {record|latency|map|replay|script|timehist}

    -D, --dump-raw-trace  dump raw trace in ASCII
    -f, --force           don't complain, do it
    -i, --input     input file name
    -v, --verbose         be more verbose (show symbol address, etc)

2. perf sched latency

其中,perf sched latency 可以给出每个任务 (task) 的调度延迟,包括平均和最大延迟:

# perf sched latency

 -----------------------------------------------------------------------------------------------------------------
  Task                  |   Runtime ms  | Switches | Average delay ms | Maximum delay ms | Maximum delay at       |
 -----------------------------------------------------------------------------------------------------------------
  cat:(6)               |     12.002 ms |        6 | avg:   17.541 ms | max:   29.702 ms | max at: 991962.948070 s
  ar:17043              |      3.191 ms |        1 | avg:   13.638 ms | max:   13.638 ms | max at: 991963.048070 s
  rm:(10)               |     20.955 ms |       10 | avg:   11.212 ms | max:   19.598 ms | max at: 991963.404069 s
  objdump:(6)           |     35.870 ms |        8 | avg:   10.969 ms | max:   16.509 ms | max at: 991963.424443 s
  :17008:17008          |    462.213 ms |       50 | avg:   10.464 ms | max:   35.999 ms | max at: 991963.120069 s
  grep:(7)              |     21.655 ms |       11 | avg:    9.465 ms | max:   24.502 ms | max at: 991963.464082 s
  fixdep:(6)            |     81.066 ms |        8 | avg:    9.023 ms | max:   19.521 ms | max at: 991963.120068 s
  mv:(10)               |     30.249 ms |       14 | avg:    8.380 ms | max:   21.688 ms | max at: 991963.200073 s
  ld:(3)                |     14.353 ms |        6 | avg:    7.376 ms | max:   15.498 ms | max at: 991963.452070 s
  recordmcount:(7)      |     14.629 ms |        9 | avg:    7.155 ms | max:   18.964 ms | max at: 991963.292100 s
  svstat:17067          |      1.862 ms |        1 | avg:    6.142 ms | max:    6.142 ms | max at: 991963.280069 s
  cc1:(21)              |   6013.457 ms |     1138 | avg:    5.305 ms | max:   44.001 ms | max at: 991963.436070 s
  gcc:(18)              |     43.596 ms |       40 | avg:    3.905 ms | max:   26.994 ms | max at: 991963.380069 s
  ps:17073              |     27.158 ms |        4 | avg:    3.751 ms | max:    8.000 ms | max at: 991963.332070 s
...]

为说明这些调度事件是如何记录和计算的,这里会以上面一行,最大调度延迟 29.702 毫秒的例子来说明。同样的结果,可以使用 perf sched script 展现其原始调度事件:

sh 17028 [001] 991962.918368:  sched:sched_wakeup_new: comm=sh pid=17030 prio=120 target_cpu=002
[...]
cc1 16819 [002] 991962.948070: sched:sched_switch: prev_comm=cc1 prev_pid=16819 prev_prio=120
                                                   prev_state=R ==> next_comm=sh next_pid=17030 next_prio=120
[...]

sh 任务被唤醒时间点 (991962.918368 单位是秒) 到 sh 通过上下文切换即将被执行的时间点 (991962.948070) 的时间间隔是 29.702 毫秒。原始调度事件列出的 sh (shell) 进程, 很快就会执行 cat 命令, 因此在 perf sched latency 输出里显示的是 cat 命令的调度延迟。

3. perf sched map

perf sched map 显示所有的 CPU 的上下文切换的事件,其中的列输出了每个 CPU 正在做什么,以及具体时间。它和我们在内核调度器分析 GUI 软件看到的可视化数据 (包括 perf timechart 的输出做 90 度旋转后) 都是一样的。下面是一个输出的例子:

# perf sched map
                      *A0           991962.879971 secs A0 => perf:16999
                       A0     *B0   991962.880070 secs B0 => cc1:16863
          *C0          A0      B0   991962.880070 secs C0 => :17023:17023
  *D0      C0          A0      B0   991962.880078 secs D0 => ksoftirqd/0:6
   D0      C0 *E0      A0      B0   991962.880081 secs E0 => ksoftirqd/3:28
   D0      C0 *F0      A0      B0   991962.880093 secs F0 => :17022:17022
  *G0      C0  F0      A0      B0   991962.880108 secs G0 => :17016:17016
   G0      C0  F0     *H0      B0   991962.880256 secs H0 => migration/5:39
   G0      C0  F0     *I0      B0   991962.880276 secs I0 => perf:16984
   G0      C0  F0     *J0      B0   991962.880687 secs J0 => cc1:16996
   G0      C0 *K0      J0      B0   991962.881839 secs K0 => cc1:16945
   G0      C0  K0      J0 *L0  B0   991962.881841 secs L0 => :17020:17020
   G0      C0  K0      J0 *M0  B0   991962.882289 secs M0 => make:16637
   G0      C0  K0      J0 *N0  B0   991962.883102 secs N0 => make:16545
   G0     *O0  K0      J0  N0  B0   991962.883880 secs O0 => cc1:16819
   G0 *A0  O0  K0      J0  N0  B0   991962.884069 secs 
   G0  A0  O0  K0 *P0  J0  N0  B0   991962.884076 secs P0 => rcu_sched:7
   G0  A0  O0  K0 *Q0  J0  N0  B0   991962.884084 secs Q0 => cc1:16831
   G0  A0  O0  K0  Q0  J0 *R0  B0   991962.884843 secs R0 => cc1:16825
   G0 *S0  O0  K0  Q0  J0  R0  B0   991962.885636 secs S0 => cc1:16900
   G0  S0  O0 *T0  Q0  J0  R0  B0   991962.886893 secs T0 => :17014:17014
   G0  S0  O0 *K0  Q0  J0  R0  B0   991962.886917 secs 
[...]

这是一个 8 CPU 系统,你可以看到从左到右 8 个列的输出来代表每个 CPU。一些 CPU 的列以空白开始,因为在我们开始运行命令分析系统时,刚开始记录调度事件。很快地,各个 CPU 所在列就开始填满输出了。

输出中的两个字符的代码 (“A0” “C0”),时用于区分识别各个任务的,其关联的任务的名字显示在右侧 (“=>”)。“*” 字符表示这个 CPU 上当时有上下文切换事件,从而导致新的调度事件发生。 例如,上面输出的最后一行表示在 991962.886917 秒的时间点, CPU 4 上发生了上下文切换,导致了任务 K0 的执行 (“cc1” 进程,PID 是 16945)。

上面这个例子来自一个繁忙的系统,下面是一个空闲系统的例子:

# perf sched map
                      *A0           993552.887633 secs A0 => perf:26596
  *.                   A0           993552.887781 secs .  => swapper:0
   .                  *B0           993552.887843 secs B0 => migration/5:39
   .                  *.            993552.887858 secs 
   .                   .  *A0       993552.887861 secs 
   .                  *C0  A0       993552.887903 secs C0 => bash:26622
   .                  *.   A0       993552.888020 secs 
   .          *D0      .   A0       993552.888074 secs D0 => rcu_sched:7
   .          *.       .   A0       993552.888082 secs 
   .           .      *C0  A0       993552.888143 secs 
   .      *.   .       C0  A0       993552.888173 secs 
   .       .   .      *B0  A0       993552.888439 secs 
   .       .   .      *.   A0       993552.888454 secs 
   .      *C0  .       .   A0       993552.888457 secs 
   .       C0  .       .  *.        993552.889257 secs 
   .      *.   .       .   .        993552.889764 secs 
   .       .  *E0      .   .        993552.889767 secs E0 => bash:7902
...]

输出中,空闲 CPU 被显示为 “.”。

注意检查输出中的时间戳所在的列,这对数据的可视化很有意义 (GUI 的调度器分析工具使用时间戳作为一个维度,这样非常易于理解,但是 CLI 的工具只列出了时间戳的值)。这个输出里也只显示了上下文切换事件的情况, 而没有包含调度延迟。新子命令 timehist 有一个可视化 (-V) 选项,允许包含唤醒 (wakeup) 事件。

4. perf sched timehist

perf sched timehist 是 Linux 4.10 里加入的,可以按照调度事件表示调度延迟,其中包括了任务等待被唤醒的时间 (wait time) 和任务从被唤醒之后到运行态的调度延迟。其中的调度延迟是我们更感兴趣去优化的。 详见下例:

# perf sched timehist
Samples do not have callchains.
           time    cpu  task name                       wait time  sch delay   run time
                        [tid/pid]                          (msec)     (msec)     (msec)
--------------- ------  ------------------------------  ---------  ---------  ---------
  991962.879971 [0005]  perf[16984]                         0.000      0.000      0.000 
  991962.880070 [0007]  :17008[17008]                       0.000      0.000      0.000 
  991962.880070 [0002]  cc1[16880]                          0.000      0.000      0.000 
  991962.880078 [0000]  cc1[16881]                          0.000      0.000      0.000 
  991962.880081 [0003]  cc1[16945]                          0.000      0.000      0.000 
  991962.880093 [0003]  ksoftirqd/3[28]                     0.000      0.007      0.012 
  991962.880108 [0000]  ksoftirqd/0[6]                      0.000      0.007      0.030 
  991962.880256 [0005]  perf[16999]                         0.000      0.005      0.285 
  991962.880276 [0005]  migration/5[39]                     0.000      0.007      0.019 
  991962.880687 [0005]  perf[16984]                         0.304      0.000      0.411 
  991962.881839 [0003]  cat[17022]                          0.000      0.000      1.746 
  991962.881841 [0006]  cc1[16825]                          0.000      0.000      0.000 
[...]
  991963.885740 [0001]  :17008[17008]                      25.613      0.000      0.057 
  991963.886009 [0001]  sleep[16999]                     1000.104      0.006      0.269 
  991963.886018 [0005]  cc1[17083]                         19.998      0.000      9.948 

上面的输出包含了前面 perf record 运行时,为设定跟踪时间为 1 秒钟而执行的 sleep 命令。你可以注意到,sleep 命令的等待时间是 10000.104 毫秒,这是该命令等待被定时器唤醒的时间。 这个 sleep 命令的调度延迟只有 0.006 毫秒,并且它在 CPU 上真正运行的时间只有 0.269 毫秒。

timehist 子命令有很多命令选项,包括 -V 选项用以增加 CPU 可视化列输出,-M 选项用以增加调度迁移事件,和 -w 选项用以显示唤醒事件。例如:

# perf sched timehist -MVw
Samples do not have callchains.
           time    cpu  012345678  task name           wait time  sch delay   run time
                                   [tid/pid]              (msec)     (msec)     (msec)
--------------- ------  ---------  ------------------  ---------  ---------  ---------
  991962.879966 [0005]             perf[16984]                                          awakened: perf[16999]
  991962.879971 [0005]       s     perf[16984]             0.000      0.000      0.000
  991962.880070 [0007]         s   :17008[17008]           0.000      0.000      0.000
  991962.880070 [0002]    s        cc1[16880]              0.000      0.000      0.000
  991962.880071 [0000]             cc1[16881]                                           awakened: ksoftirqd/0[6]
  991962.880073 [0003]             cc1[16945]                                           awakened: ksoftirqd/3[28]
  991962.880078 [0000]  s          cc1[16881]              0.000      0.000      0.000
  991962.880081 [0003]     s       cc1[16945]              0.000      0.000      0.000
  991962.880093 [0003]     s       ksoftirqd/3[28]         0.000      0.007      0.012
  991962.880108 [0000]  s          ksoftirqd/0[6]          0.000      0.007      0.030
  991962.880249 [0005]             perf[16999]                                          awakened: migration/5[39]
  991962.880256 [0005]       s     perf[16999]             0.000      0.005      0.285
  991962.880264 [0005]        m      migration/5[39]                                      migrated: perf[16999] cpu 5 => 1
  991962.880276 [0005]       s     migration/5[39]         0.000      0.007      0.019
  991962.880682 [0005]        m      perf[16984]                                          migrated: cc1[16996] cpu 0 => 5
  991962.880687 [0005]       s     perf[16984]             0.304      0.000      0.411
  991962.881834 [0003]             cat[17022]                                           awakened: :17020
...]
  991963.885734 [0001]             :17008[17008]                                        awakened: sleep[16999]
  991963.885740 [0001]   s         :17008[17008]          25.613      0.000      0.057
  991963.886005 [0001]             sleep[16999]                                         awakened: perf[16984]
  991963.886009 [0001]   s         sleep[16999]         1000.104      0.006      0.269
  991963.886018 [0005]       s     cc1[17083]             19.998      0.000      9.948 # perf sched timehist -MVw

CPU 可视化列 (“012345678”) 用于表示对应 CPU 的调度事件。如果包含一个 “s” 字符,就表示上下文切换事件的发生,但如果是 “m” 字符,就代表调度迁移事件发生。

以上输出的最后几个事件里包含了之前 perf record 里设定时间的 sleep 命令。其中唤醒发生在时间点 991963.885734,并且在时间点 991963.885740 (6 微妙之后),CPU 1 开始发生上下文切换,sleep 命令被调度执行。 其中任务名所在的列仍然显示 “:17008[17008]”,以代表上下文切换开始前在 CPU 上的进程,但是上下文切换完成后被调度的目标进程并没有显示,它实际上可以在原始调度事件 (raw events) 里可以被找到:

:17008 17008 [001] 991963.885740:       sched:sched_switch: prev_comm=cc1 prev_pid=17008 prev_prio=120
                                                             prev_state=R ==> next_comm=sleep next_pid=16999 next_prio=120

时间戳为 991963.886005 的事件表示了 sleep 进程在 CPU 上运行时,perf 命令收到了一个唤醒 (这应该是 sleep 命令在结束退出时唤醒了它的父进程)。随后在时间点 991963.886009,上下文切换事件发生, 这时 sleep 命令停止执行,并且打印出上下文切换事件的跟踪记录:在 sleep 运行期间,它有 1000.104 毫秒的等待时间,0.006 毫秒的调度延迟,和 0.269 毫秒的 CPU 实际运行时间。

下面对 timehist 的输出做了上下文切换的目标进程相关的标注 (“next:” 为前缀):

991963.885734 [0001]             :17008[17008]                                        awakened: sleep[16999]
991963.885740 [0001]   s         :17008[17008]          25.613      0.000      0.057  next: sleep[16999]
991963.886005 [0001]             sleep[16999]                                         awakened: perf[16984]
991963.886009 [0001]   s         sleep[16999]         1000.104      0.006      0.269  next: cc1[17008]
991963.886018 [0005]       s     cc1[17083]             19.998      0.000      9.948  next: perf[16984]

当 sleep 结束后,正在等待的 “cc1” 进程被执行。随后的上下文切换, perf 命令被执行,并且它是最后一个调度事件 (因为 perf 命令最后退出了)。笔者给社区提交了一个 patch, 通过 -n 命令选项,用以支持显示上下文切换的目标进程。

4. perf sched script

perf sched script 子命令用来显示所有原始调度事件 (raw event),与 perf script 的作用类似:

# perf sched script
    perf 16984 [005] 991962.879960: sched:sched_stat_runtime: comm=perf pid=16984 runtime=3901506 [ns] vruntime=165...
    perf 16984 [005] 991962.879966:       sched:sched_wakeup: comm=perf pid=16999 prio=120 target_cpu=005
    perf 16984 [005] 991962.879971:       sched:sched_switch: prev_comm=perf prev_pid=16984 prev_prio=120 prev_stat...
    perf 16999 [005] 991962.880058: sched:sched_stat_runtime: comm=perf pid=16999 runtime=98309 [ns] vruntime=16405...
     cc1 16881 [000] 991962.880058: sched:sched_stat_runtime: comm=cc1 pid=16881 runtime=3999231 [ns] vruntime=7897...
  :17024 17024 [004] 991962.880058: sched:sched_stat_runtime: comm=cc1 pid=17024 runtime=3866637 [ns] vruntime=7810...
     cc1 16900 [001] 991962.880058: sched:sched_stat_runtime: comm=cc1 pid=16900 runtime=3006028 [ns] vruntime=7772...
     cc1 16825 [006] 991962.880058: sched:sched_stat_runtime: comm=cc1 pid=16825 runtime=3999423 [ns] vruntime=7876...

上面输出对应的每一个事件 (如 sched:sched_stat_runtime),都与内核调度器相关代码里的 tracepoint 相对应,这些 tracepoint 都可以使用 perf record 来直接激活并且记录其对应事件。 如前文所示,这些原始调度事件 (raw event) 可以用来帮助理解和解释其它 perf sched 子命令的相关输出。

就酱,玩得高兴!

5. References

]]>
Linux Kernel Memory Hacking 2017-03-20T00:00:00+00:00 Oliver Yang http://oliveryang.net/2017/03/linux-kernel-memory-hacking

The content reuse need include the original link: http://oliveryang.net

1. 问题

修改正在运行的 Linux 内核的内存是在内核开发中常见的需求之一。通过修改内核内存,我们可以轻松地实现内核开发里的单元测试,或者注入一个内核错误。 虽然修改内核内存有很多方式,例如使用 systemtap 脚本可以实现一些简单的内核内存的修改,但是论灵活性和安全性,还是要数 Linux 的 crash 工具。

如果您不熟悉 Linux Crash 工具,请阅读 Linux Crash - background 这篇文章。第 4 小节还有大量其它链接可以参考。

Linux crash 工具提供了简单的 wr 命令,可以轻松修改指定的虚拟内存地址的内存。而且,crash 可以轻松解析内核的数据结构,计算出结构成员,链表成员的地址偏移。 但在 RHEL 7 上,使用 crash 修改内存,却报告如下错误,

crash> wr panic_on_oops 1
wr: cannot write to /dev/crash!

2. 原因

Linux crash 自带了一个内存驱动 (/dev/crash),用于支持对内核内存的操作。 但 crash 的代码 并没有提供写内存的支持。

此时,我们可以利用实现了写操作的 /dev/mem 设备做为 /dev/crash 替换来解决问题,

$ sudo crash /dev/mem

在 RHEL 和一些其它 Linux 发行版,因为系统默认打开了 CONFIG_STRICT_DEVMEM 编译选项,从而限制了 /dev/mem 驱动的使用,

crash: this kernel may be configured with CONFIG_STRICT_DEVMEM, which
	renders /dev/mem unusable as a live memory source.
crash: trying /proc/kcore as an alternative to /dev/mem

于是,crash 提示使用 /proc/kcore,这种方式也无法提供对内存的修改操作。

3. 解决

如果阅读内核源码,可以看到,CONFIG_STRICT_DEVMEM 的限制主要依赖如下函数的返回值,

int devmem_is_allowed(unsigned long pagenr)
{
	if (pagenr <= 256)
		return 1;
	if (!page_is_ram(pagenr))
		return 1;
	return 0;
}

如果我们能不重新编译内核,就让这个函数的返回永远是 1,则可以轻松绕过这个限制。基于此分析,我们可以用一行 systemtap 命令搞定这件事情,

$ sudo stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return = 1 }'

运行完上述命令后,在另一个终端执行 crash 命令测试我们的内存修改命令 (例子里修改了 panic_on_oops 的内核全局变量),一切工作正常,

crash> wr panic_on_oops 1
crash> p panic_on_oops
panic_on_oops = $1 = 1

至此,我们也可以想到,利用 systemtap,通过修改内核函数的返回值,来做内核开发的单元测试和错误注入也是轻而易举的。

4. 延伸阅读

]]>
Linux Printk Flooding 2017-03-11T00:00:00+00:00 Oliver Yang http://oliveryang.net/2017/03/linux-printk-flooding

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

1. 什么是 printk flooding?

Linux 内核里,printk 是程序员们常用的错误报告和调试手段之一。然而,不恰当使用或者滥用 printk 会导致高频 printk 调用,最终在内核里引发 printk flooding 这样的问题。

printk 的实现里,中断会被关掉,而且还会拿自旋锁。如果内核需要打印的消息太多,printk 会在关中断的情况下反复循环打印日志缓冲区的内容,这就最终让 CPU 始终忙于打印而无法调度其它任务, 包括 touch softlockup watchdog 的内核线程。 尤其当系统的 console 被设置为串口设备时,printk flooding 会更容易发生,因为串口实在是太慢了,这会导致 printk 循环超时的概率大大增加。

最终,系统会因为 printk flooding 导致的没有响应。在使能了 softlockup watchdog 的系统上,会触发这个 watchdog 的超时和 panic。

2. 解决方法

由于 printk 要被设计成可以工作在任何内核的上下文,拿自旋锁和关中断似乎无法避免。

在 printk flooding 导致的 softlockup 超时中,假若引发 printk 的错误本来对系统就是致命的,那么这个问题最多影响了人们定位真正的错误根源。 但若引发 printk 的错误原本不是致命错误,那么 printk flooding 就破坏了系统的稳定性和可用性。

虽然 printk flooding 是个经典的老问题了,但我们没有快速的解决办法。 一般来说我们可以在下面几个方向上缓解和解决这个问题,

  1. 提高串口速率,缓解问题。

    由于 printk 最终将调用底层注册的 console 驱动将消息打印到 console 设备上。当串口作为 console 设备时,默认为 9600 波特率的慢速串口设备将成为一大瓶颈。 这加剧了问题的发生。为了缓解问题,我们可以将串口速率从 9600 设置到更高的波特率,例如 119200。这可以让 printk 执行的更有效率。

  2. 用于 debug 性质的 printk,不要在产品模式下使用。

    内核和驱动开发者经常会定义一些打印语句帮助调试和维护代码。在产品发布前,需要注意清理这些不必要的打印语句。比如,使用条件编译开关。 一些对维护产品非常有意义的调试打印,也可以利用内核全局的变量开关控制,默认关闭这些打印语句,需要时再动态打开。

  3. 非致命错误或者消息,使用 printk_ratelimited 替换一般 printk

    致命错误先引发 printk flooding 问题不大。凡是非致命错误引发的 printk flooding,可以用 printk_ratelimited 来限流。打印频率超过每 5 秒 10 个消息,多余的消息会被丢掉。 关于 printk_ratelimited 的使用,可以参考内核源码中的例子。其中 printk_ratelimitprintk_ratelimit_burst 可以控制打印的时间间隔和消息个数。

3. 其它坑

由于 printk 的语义和设计目标是让它工作在所有内核上下文,因此其代码实现并没有像它的功能那样看起来那么简单。修改它的代码可能就会踩到意想不到的坑。

3.1 重入

printk 的调用栈里又调用了 printk,这是经典老坑,因为内核代码到处在调用 printk

3.1.1 异常引发重入

printk 到一半突然系统因触发异常 crash,就会引发重入。为了让这个重入可以正常工作,需要调用 zap_locks 来把函数里用到的锁都复位,避免死锁发生。

printk 的代码逻辑是想要阻止 crash 以外的重入的,因此有了 recursion_bug 这种断言 bug 的逻辑。下例来自 3.10.

1516     /*
1517      * Ouch, printk recursed into itself!
1518      */
1519     if (unlikely(logbuf_cpu == this_cpu)) {
1520         /*
1521          * If a crash is occurring during printk() on this CPU,
1522          * then try to get the crash message out but make sure
1523          * we can't deadlock. Otherwise just return to avoid the
1524          * recursion and return - but flag the recursion so that
1525          * it can be printed at the next appropriate moment:
1526          */
1527         if (!oops_in_progress && !lockdep_recursing(current)) {
1528             recursion_bug = 1;
1529             goto out_restore_irqs;
1530         }
1531         zap_locks();
1532     }
1533 
1534     lockdep_off();
1535     raw_spin_lock(&logbuf_lock);
1536     logbuf_cpu = this_cpu;
1537 
1538     if (recursion_bug) {
1539         static const char recursion_msg[] =
1540             "BUG: recent printk recursion!";

3.1.2 NMI 引发重入

尽管 printk 一开始就关中断,但 NMI 还是不可屏蔽的。因此 NMI 上下文如果调用 printk,而碰巧之前的 printk 还在拿锁状态,重入引起的死锁就发生了。

因此,很长一段时间内,Linux 内核是不能安全地在 NMI 上下文使用 printk 的。为此,也有很多人提交了 patch,并且引发了讨论

然而,去年的一个 printk/nmi: generic solution for safe printk in NMI 终于解决了问题。 从此,NMI 中断处理函数在进入前,都会调用 printk_nmi_enter 来标识这个特殊上下文,从而使 NMI 再次调用的 printk 进入到特殊的无锁化的实现里。

3.1.3 Scheduler 引发重入

除了异常,NMI,printk 自身因为用到了锁的接口,这些接口引发调度器代码被调用时,可能会再次调用 printk,从而导致重入。

由于有 NMI 的方案,因此这一次就是复制了 NMI 的处理方式:printk: introduce per-cpu safe_print seq buffer。 这样,在 printk 用到的锁代码里,都调用 printk_safe_enter_irqsave 这样的调用,通过标示这个特殊上下文,来对重入做无锁化处理。

3.2 Touch watchdog

由于 printk 在大多数正常内核上下文中还是需要拿自旋锁并关中断,在 printk 中加入引发循环或者其它显著延时的代码都需要防止内核产生 hardlockup(NMI) 和 sotflockup watchdog 超时。 因此,在任何可能引发超时的循环代码,插入 touch_nmi_watchdog 是可以解决一些问题的。然而,即使 watchdog 没有了超时问题,CPU 还是可能因为只能运行打印函数而无法调度其它任务。 总之,printk flooding 问题不能通过 touch_nmi_watchdog 来解决。

]]>
Linux Block Driver - 7 2017-01-01T00:00:00+00:00 Oliver Yang http://oliveryang.net/2017/01/linux-block-driver-basic-7

本文处于草稿状态,内容可能随时更改。转载时请包含原文或者作者网站链接:http://oliveryang.net

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,吞吐等。
  • Linux Block Driver - 5 中,使用 blktrace 跟踪了 fio 顺序写测试的 IO 操作,并对跟踪结果和 IO 流程做了详细总结。
  • Linux Block Driver - 6 里,我们解决了 BIO 拆分的问题,进一步理解了吞吐,延迟和 IOPS 的关系。

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

2. 准备

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

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

3. IO 调度器基本原理

4. 实验

5. 小结

6. 延伸阅读

]]>
Linux Block Driver - 6 2016-11-12T00:00:00+00:00 Oliver Yang http://oliveryang.net/2016/11/linux-block-driver-basic-6

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

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,吞吐等。
  • Linux Block Driver - 5 中,使用 blktrace 跟踪了 fio 顺序写测试的 IO 操作,并对跟踪结果和 IO 流程做了详细总结。

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

2. 准备

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

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

3. bio 拆分问题

Linux Block Driver - 5 中,我们发现,每次 IO,在块设备层都会经历一次 bio 拆分操作。

$ 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]

而我们知道,IO 拆分操作对块设备性能是有负面的影响的。那么,为什么会出现这样的问题?我们应当如何避免这个问题?

3.1 原因分析

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

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

那么,本例中的 sampleblk 驱动的块设备,是否设置了 request_queue 的相关属性呢?我们可以利用 crash 命令,查看该设备驱动的 request_queue 及其属性,

crash7> dev -d
MAJOR GENDISK            NAME       REQUEST_QUEUE      TOTAL ASYNC  SYNC   DRV
    8 ffff88003505f000   sda        ffff880034e34800       0     0     0     0
   11 ffff88003505e000   sr0        ffff880034e37500       0     0     0     0
   11 ffff880035290800   sr1        ffff880034e36c00       0     0     0     0
  253 ffff88003669b800   sampleblk1 ffff880034e30000       0     0     0     0  >>> sampleblk 驱动对应的块设备

crash7> request_queue.limits ffff880034e30000
  limits = {
    bounce_pfn = 4503599627370495,
    seg_boundary_mask = 4294967295,
    virt_boundary_mask = 0,
    max_hw_sectors = 255,
    max_dev_sectors = 0,
    chunk_sectors = 0,
    max_sectors = 255,				>>> Split bio 的原因
    max_segment_size = 65536,
    physical_block_size = 512,
    alignment_offset = 0,
    io_min = 512,
    io_opt = 0,
    max_discard_sectors = 0,
    max_hw_discard_sectors = 0,
    max_write_same_sectors = 0,
    discard_granularity = 0,
    discard_alignment = 0,
    logical_block_size = 512,
    max_segments = 128,             >>> Split bio 的又一原因
    max_integrity_segments = 0,
    misaligned = 0 '\000',
    discard_misaligned = 0 '\000',
    cluster = 1 '\001',
    discard_zeroes_data = 0 '\000',
    raid_partial_stripes_expensive = 0 '\000'
  }

而这里请求队列的 limits.max_sectorslimits.max_segments 属性,则是由块设备驱动程序在初始化时,根据自己的处理能力设置的。

那么 sampleblk 是如何初始化 request_queue 的呢?

在 sampleblk 驱动的代码里,我们只找到如下函数,

static int sampleblk_alloc(int minor)
{

[...snipped...]

    spin_lock_init(&sampleblk_dev->lock);
    sampleblk_dev->queue = blk_init_queue(sampleblk_request,
        &sampleblk_dev->lock);

[...snipped...]

而进一步研究 blk_init_queue 的实现,我们就发现,这个 limits.max_sectors 的限制,正好就是调用 blk_init_queue 引起的,

blk_init_queue->blk_init_queue_node->blk_init_allocated_queue->blk_queue_make_request->blk_set_default_limits

调用 blk_init_queue 会最终导致 blk_set_default_limits 将系统定义的默认限制参数设置到 request_queue 上,

include/linux/blkdev.h

enum blk_default_limits {
    BLK_MAX_SEGMENTS    = 128,
    BLK_SAFE_MAX_SECTORS    = 255, >>> sampleblk 使用的初值
    BLK_DEF_MAX_SECTORS = 2560,
    BLK_MAX_SEGMENT_SIZE    = 65536,
    BLK_SEG_BOUNDARY_MASK   = 0xFFFFFFFFUL,
};

3.2 问题解决

由于 sampleblk 设备是基于内存的块设备,并不存在一般块设备硬件的限制,故此,我们可以通过调用 blk_set_stacking_limits 解除 Linux IO 栈的诸多限制。 具体改动可以参考 lktm 里 sampleblk 的改动.

经过驱动的重新编译、加载、文件系统格式化,装载,可以查看 sampleblk 驱动的 request_queue 确认限制已经解除,

crash7> mod -s sampleblk /home/yango/ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko
     MODULE       NAME                   SIZE  OBJECT FILE
ffffffffa0068580  sampleblk              2681  /home/yango/ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko
crash7> dev -d
MAJOR GENDISK            NAME       REQUEST_QUEUE      TOTAL ASYNC  SYNC   DRV
   11 ffff88003501b800   sr0        ffff8800338b0000       0     0     0     0
    8 ffff88003501e000   sda        ffff880034a8c800       0     0     0     0
    8 ffff88003501f000   sdb        ffff880034a8da00       0     0     0     0
   11 ffff88003501d800   sr1        ffff8800338b0900       0     0     0     0
  253 ffff880033867800   sampleblk1 ffff8800338b2400       0     0     0     0
crash7> request_queue.limits -x ffff8800338b2400
  limits = {
    bounce_pfn = 0xfffffffffffff,
    seg_boundary_mask = 0xffffffff,
    virt_boundary_mask = 0x0,
    max_hw_sectors = 0xffffffff,
    max_dev_sectors = 0xffffffff,  >>> 最大值,原来是 255
    chunk_sectors = 0x0,
    max_sectors = 0xffffffff,
    max_segment_size = 0xffffffff,
    physical_block_size = 0x200,
    alignment_offset = 0x0,
    io_min = 0x200,
    io_opt = 0x0,
    max_discard_sectors = 0x0,
    max_hw_discard_sectors = 0x0,
    max_write_same_sectors = 0xffffffff,
    discard_granularity = 0x0,
    discard_alignment = 0x0,
    logical_block_size = 0x200,
    max_segments = 0xffff,   >>> 最大值,原来是 128
    max_integrity_segments = 0x0,
    misaligned = 0x0,
    discard_misaligned = 0x0,
    cluster = 0x1,
    discard_zeroes_data = 0x1,
    raid_partial_stripes_expensive = 0x0
  }

运行与之前系列文章中相同的 fio 测试,同时用 blktrace 跟踪 IO 操作,

$ sudo blktrace /dev/sampleblk1

可以看到,此时已经没有 bio 拆分操作,即 X action,

$ blkparse sampleblk1.blktrace.0 | grep X | wc -l
0

如果查看 IO 完成的操作,可以看到,文件系统的 page cache 4K 大小的页面可以在一个块 IO 操作完成,也可以分多次完成,如下例中 2 次和 3 次,

$ blkparse sampleblk1.blktrace.0 | grep C | head -10
253,1    0        9     0.000339563 128937  C   W 2486 + 4096 [0]
253,1    0       18     0.002813922 128937  C   W 2486 + 4096 [0]
253,1    1        9     0.006499452 128938  C   W 4966 + 1616 [0]
253,1    0       27     0.006674160 128924  C   W 2486 + 2256 [0]
253,1    0       34     0.006857810 128937  C   W 4742 + 224 [0]
253,1    1       19     0.009835686 128938  C   W 2486 + 1736 [0]
253,1    1       21     0.010198002 128938  C   W 4222 + 2360 [0]
253,1    0       45     0.012429598 128937  C   W 2486 + 4096 [0]
253,1    0       54     0.015565043 128937  C   W 2486 + 4096 [0]
253,1    0       63     0.017418088 128937  C   W 2486 + 4096 [0]

但如果查看 Linux Block Driver - 5 的结果,则一个页面的写要固定被拆分成 20 次 IO 操作。

iostat 查看块设备的性能,我们可以发现尽管 IO 吞吐量在新的改动下得到提升,但是却增加了 IO 等待时间,

$ iostat /dev/sampleblk1  -xmdz 1

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sampleblk1        0.00   558.42    0.00  761.39     0.00  1088.20  2927.08     0.22    0.29    0.00    0.29   0.27  20.79

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sampleblk1        0.00   542.00    0.00  768.00     0.00  1097.41  2926.44     0.20    0.26    0.00    0.26   0.25  19.20

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sampleblk1        0.00   524.00    0.00  799.00     0.00  1065.24  2730.43     0.21    0.26    0.00    0.26   0.25  20.00

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sampleblk1        0.00   846.00    0.00  742.00     0.00  1079.73  2980.17     0.20    0.27    0.00    0.27   0.26  19.60

Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sampleblk1        0.00   566.00    0.00  798.00     0.00  1068.08  2741.14     0.21    0.26    0.00    0.26   0.26  20.50

与修复问题之前的 iostat 结果相比,我们可以得到如下结论,

  • 设备的 IO 吞吐量比 Linux Block Driver - 4 提高了 10% ~ 15%。
  • 原来版本驱动 900 多 MB/s 的吞吐量提升到了 1000 多 MB/s。
  • 但另一方面,IOPS 从原有的 8000 多降到了 700 多,整整差了 10 倍。
  • 平均的 IO 请求大小 (avgrq-sz) 从 220 左右增加到了 2700 ~ 2900 多子节,增加了 10 倍多。
  • 平均写等待时间 (w_await) 从 0.10 ~ 0.15 毫秒增加到了 0.20 ~ 0.22 毫秒。

4. 小结

本文分析了在前几篇文章中的 fio 测试中观测到的 bio 拆分的问题,并给出了一个解决方案。修复了拆分问题后,测试的吞吐量得到了提高,IO 延迟增加了。同时,IOPS 也下降了 10 倍。 而 IOPS 的下降比例,和 IO 请求的增加比例相当,可以看出 IOPS 是和 IO 大小密切相关的。 实际上,这个测试也从另一个角度说明了,抛开测试负载的各种属性,IO 大小,IO 读写比例,顺序还是随机 IO,去比较 IOPS 是没有意义的。

5. 延伸阅读

]]>
Linux Block Driver - 5 2016-10-27T00:00:00+00:00 Oliver Yang http://oliveryang.net/2016/10/linux-block-driver-basic-5

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

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 触发,是由 fadvise64 flush 脏的 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 调度器的代码找到合适的 IO request,并将 bio 合并到该 request 中。
  • 如果无法将 bio 合并到已经存在的 IO request 结构里,那么就进入到单独为该 bio 分配空闲 IO request 的逻辑里。

不论是 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 到已存在的 IO request 里, 该函数会为 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 的使用和结果分析。

6. 延伸阅读

]]>