差生文具多之(二): perf

news2024/12/30 1:58:53

栈回溯和符号解析是使用 perf 的两大阻力,本文以应用程序 fio 的观测为例子,提供一些处理它们的经验法则,希望帮助大家无痛使用 perf。

前言

系统级性能优化通常包括两个阶段:性能剖析和代码优化:

  1. 性能剖析的目标是寻找性能瓶颈,查找引发性能问题的原因及热点代码;

  2. 代码优化的目标是针对具体性能问题而优化代码或调整编译选项,以改善软件性能。

在步骤一性能剖析阶段,最常用的工具就是 perf。perf 是 linux 官方提供的性能分析工具,被包含在 Linux 内核源码树中。它是一个庞大的工具集合,功能相当繁杂。但在工作中,通常我们只会使用到 perf 其中相当小的一个子集,主要包含以下四个步骤:

  1. perf record: 采集数据,采的时间越长越心安;

  2. perf report: 查看采集数据,因为采集太长时间,解析数据会卡很久,我们试图理解数据,通常无法理解;

  3. perf script: 尝试查看原始采样点,通常无法理解;

  4. 生成火焰图: 色彩丰富,通常发给领导理解。

综上,后三个步骤是我们无法控制的,本文主要聊聊如何在步骤一尽量生成可信的采样数据。

workflow of perf

虽然听起来调侃,但上述步骤确实是标准的分析流程,毕竟有火焰图发明人 Brandon 的背书:

@Brandon

可以看到,它们被包含在 perf 工作流第三列的 capture stacks 中,简单回顾一下这四个步骤:

  1. perf record: 通过指定 -g 选项可以收集系统整体的函数调用栈(包含用户态和内核态),默认以 4000HZ 的频率收集,大约每秒生成 4000 个采样点,被保存在 perf.data 文件中;

$ perf record -g -C 0 -- sleep 1
[ perf record: Captured and wrote 0.906 MB perf.data (4001 samples) ]
  1. perf report: 通过解析 perf.data,生成热点函数占用 CPU 的比例。例如以下输出中,CPU0 大部分时间(99.73%)停留在内核代码的 idle 函数中,即 CPU0 大部分时间处于空闲状态:

$ perf report --no-child --stdio
99.73%  swapper          [kernel.kallsyms]  [k] native_safe_halt
  |
  ---native_safe_halt
     acpi_idle_do_entry
     acpi_idle_enter
     cpuidle_enter_state
     cpuidle_enter
     do_idle
     cpu_startup_entry
     start_kernel
     secondary_startup_64_no_verify
  1. perf script: 查看每个采样样本(栈),例如以下栈样本表明: cpu-clock:pppH: 事件于时间 45399.463561 发生,在 CPU0 触发了中断,中断打断的任务是进程号为 0 的内核线程 swapper,栈从下往上看,被打断时 CPU 正在执行 native_safe_halt 偏移 0xe 处的指令:

$ perf script
swapper     0 [000] 45399。463561:     250000 cpu-clock:pppH: 
        ffffffffa234c45e native_safe_halt+0xe ([kernel.kallsyms])
        ffffffffa234c806 acpi_idle_do_entry+0x46 ([kernel.kallsyms])
        ffffffffa1f4bafb acpi_idle_enter+0x9b ([kernel.kallsyms])
        ffffffffa211efb7 cpuidle_enter_state+0x87 ([kernel.kallsyms])
        ffffffffa211f33c cpuidle_enter+0x2c ([kernel.kallsyms])
        ffffffffa1b16ff4 do_idle+0x234 ([kernel.kallsyms])
        ffffffffa1b171ef cpu_startup_entry+0x6f ([kernel.kallsyms])
        ffffffffa3601262 start_kernel+0x518 ([kernel.kallsyms])
        ffffffffa1a00107 secondary_startup_64_no_verify+0xc2 ([kernel.kallsyms])
  1. 使用脚本生成火焰图,以下是官网例图:

可以发现,后续的分析步骤都基于步骤一采集得到的 perf.data。显然,只有获取到足够精准的调用栈信息,后续才能准确定位到性能瓶颈。可惜的是,获取函数调用栈并没有一个通用解,导致我们需要额外了解一些小知识。

choose your unwinder

获取函数调用栈过程又称栈回溯(unwind),栈回溯的方法被称为 unwinder,常见的 unwinder 有:

  1. fp:perf 默认选项,ARM 和 X86 都支持,消耗低;

  2. dwarf:通过 --call-graph=dwarf 指定,ARM 和 X86 都支持,对CPU和磁盘消耗高;

  3. lbr:通过 --call-graph=lbr 指定,仅 Intel 新型号支持,消耗低,但可回溯的栈深度有限;

  4. orc:内核 unwinder,无需指定。 在 perf record 中,若不通过 --call-graph 指定 unwinder,默认使用 fp 作为用户态栈的 unwinder;至于内核态的 unwinder,不由 perf 参数指定,由内核编译选项控制,低版本内核使用 fp,高版本内核使用 orc。

因此问题转化为:用户态使用哪个 unwinder 是更合适的?结论先行,以下是可供参考的方案:

  1. Intel CPU:优先使用 lbr,lbr 的好处是硬件实现,精准可靠,大部分情况下深度够用;

  2. ARM 架构:优先使用 fp,因为 ARM 架构寄存器比较多,保留了寄存器记录栈基址;

  3. X86 上没有 lbr 时:优先使用 dwarf,虽然 X86 架构也把栈基址保存在 %rbp,但只要编译优化大于等于 -O1 ,%rbp 寄存器基本作为通用寄存器使用,使得在 X86 上用 fp 获取用户态栈大部分时候不可靠。有以下注意点:
    1. 在 linux 5.19 版本以下,dwarf 可能采样不到动态链接库的栈(参考提交 perf unwind: Fix egbase for ld.lld linked objects);

    2. dwarf 需要复制保存每一个采样点的用户栈,因此采样期间 CPU 消耗较高,生成的采样数据也远大于其它 unwinder;

    3. 如果 dwarf 无法满足需求,可以 gcc 编译时添加选项 -fno-omit-frame-pointer 放弃复用 %rbp 寄存器的编译优化,重新编译应用后使用 fp。虽然该选项无法百分百保证 %rbp 一定可靠,但总体可信。

让我们通过在 X86 架构上观测应用程序 fio,对这些 unwinder 有个初步的了解:

$ perf record -a --user-callchains --call-graph=dwarf -p `pidof fio` -o perf.data.dwarf -- sleep 2
$ perf report --no-ch --stdio -i perf.data.dwarf
    10.69%  fio      [kernel.kallsyms]  [k] iowrite16
            |
            ---syscall
               io_submit
               0x55a0a986682e # <- 我们会在下下节解决符号问题
               td_io_commit
               td_io_queue
               0x55a0a985945a <-
               0x55a0a985b7d0 <- 
               start_thread
               __GI___clone (inlined)
$ perf record -a --user-callchains --call-graph=fp -p `pidof fio` -o perf.data.fp -- sleep 2
$ perf report --no-ch --stdio -i perf.data.fp
     8.27%  fio      [kernel.kallsyms]  [k] iowrite16
            |
            ---syscall
               |--0.75%--0x70700000707
               |--0.75%--0x62d0000062d
               |--0.75%--0x5e1000005e1
               |--0.75%--0x55b0000055a
               |--0.75%--0x54800000548
               |--0.75%--0x52f0000052f
               |--0.75%--0x51000000510
               |--0.75%--0x44f0000044f
               |--0.75%--0x3cb000003cb
               |--0.75%--0x39800000398
                --0.75%--0x37c0000037b

以上采集数据的命令中使用 --user-callchains 选项指定了 perf 采样时只采集用户栈,排除掉我们暂时不关心的内核栈。输出中可以看到虽然 dwarf 采集得到的栈没有被完全翻译,但正确地回溯到了进程刚诞生的函数 __GI___clone,这表明 dwarf 采样得到了完整的栈;反观 fp,只得到了些奇怪的地址。我们的方案三是有效的!

what do dwarf do

为叙述完整,该节补充一点 dwarf 栈回溯原理,不影响 perf 使用,不涉及的朋友可以跳转下一节解决符号问题。

在编译过程中 gcc 无论是否指定 -g 选项, 默认都会生成 .eh_frame 和 .eh_frame_hdr 段. gcc 在翻译代码为汇编代码时, 会帮忙插上一些 CFI 伪指令, 如

$ gcc -S test.c           # c语言生成汇编代码
$ vim test.s              # 查看汇编代码
$ cat test.s
        .cfi_startproc # 刚进函数, 当前我们处于 callee 栈帧的起始处, 更新 CFA = rsp + 8
        pushq %rbp
        # 每次 push 寄存器到栈上, 需要将 CFA += 8, 因为相比上一状态需要多往前走一个单位才是 caller 的栈帧
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16 # 并且更新该寄存器关于 CFA 的偏移, 使回溯过程可以恢复该寄存器的值
        # ...
         movq %rsp %rbp # 将 rsp 寄存器赋值给 rbp
        .cfi_def_cfa_register 6 # 将寄存器 6 (rbp) 定义为 CFA 寄存器, 之后 CFA 的计算都基于 rbp
        # ...
        leave
        .cfi_def_cfa 7, 8 # leave 中将 rbp 寄存器的值赋值给 rsp, 即 rsp 此时指向 callee 栈帧开始处, 此时 CFA = rsp + 8
        .cfi_endproc
$ readelf -wF test.o # 查看对应的 .eh_frame 印证
0000000000000661 rsp+8    u     c-8   
0000000000000662 rsp+16   c-16  c-8   
0000000000000665 rbp+16   c-16  c-8   
00000000000006a6 rsp+8    c-16  c-8 

其中 CFA (Canonical Frame Address, which is the address of %rsp in the caller frame) 指上一级调用者的堆栈指针.

如上所示, 汇编器会将这些 CFI 伪指令收集到可执行文件中的 .eh_frame 段. 典型形式如下:

$ readelf -wF a.out 
Contents of the .eh_frame section:00000000 0000000000000014 00000000 CIE "zR" cf=1 df=-8 ra=16LOC           CFA      ra    
0000000000000000 rsp+8    u     ...000000c8 0000000000000044 0000009c FDE cie=00000030 pc=00000000000006b0..0000000000000715LOC           CFA      rbx   rbp   r12   r13   r14   r15   ra    
00000000000006b0 rsp+8    u     u     u     u     u     u     c-8   
00000000000006b2 rsp+16   u     u     u     u     u     c-16  c-8   
00000000000006b4 rsp+24   u     u     u     u     c-24  c-16  c-8   
00000000000006b9 rsp+32   u     u     u     c-32  c-24  c-16  c-8   
00000000000006bb rsp+40   u     u     c-40  c-32  c-24  c-16  c-8   
00000000000006c3 rsp+48   u     c-48  c-40  c-32  c-24  c-16  c-8   
00000000000006cb rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
00000000000006d8 rsp+64   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070a rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070b rsp+48   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070c rsp+40   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070e rsp+32   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000710 rsp+24   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000712 rsp+16   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000714 rsp+8    c-56  c-48  c-40  c-32  c-24  c-16  c-8  

可以看到 .eh_frame 总体架构由 CIE 和 FDE 组成。通常一个 CIE 代表一个文件, 一个 FDE 代表一个函数. 其中核心的是 FDE 的组织:

利用 .eh_frame 进行栈 unwind 时候, 遵循以下步骤:

  1. 根据当前的PC在.eh_frame中找到对应的条目,根据条目提供的各种偏移计算其他信息。

  2. 首先根据CFA = rsp+4,把当前rsp+4得到CFA的值。再根据CFA的值计算出通用寄存器和返回地址在堆栈中的位置。

  3. 通用寄存器栈位置计算。例如:rbx = CFA-56。

  4. 返回地址ra的栈位置计算。ra = CFA-8。

  5. 根据ra的值,重复步骤1到4,就形成了完整的栈回溯。

handle missing symbols

函数调用栈本质是一串地址,perf 会尽量将地址翻译人类可读的符号。在以下样本点中,可以看到 IP 寄存器保存的地址属于 libc 库,它被正确翻译为 syscall+0x1d,但再往下回溯,我们只知道 syscall 函数是由 libaio 库某不知名函数调用的。这里出现 [unknown] 通常由于可执行程序的符号被裁剪所致,裁剪符号是有效减小可执行程序体积的做法。

$ perf script -D -i perf.data.dwarf
259594741631398 0x2d840 [0x20f8]: PERF_RECORD_SAMPLE(IP, 0x1): 273245/273258: 0xffffffff89d1869d period: 250000 addr: 0
... FP chain: nr:0
[...]
.... IP    0x00007f3afb87f52d
... ustack: size 8192, offset 0xe0
[...]
fio 273258 259594.741631:     250000 cpu-clock:pppH: 
            7f3afb87f52d syscall+0x1d (/usr/lib64/libc-2.28.so)
            7f3afc50ab7d [unknown] (/usr/lib64/libaio.so.1.0.1)
            55a0a9866a95 [unknown] (/usr/bin/fio)
            55a0a98197a5 td_io_getevents+0x75 (/usr/bin/fio)
            55a0a983b216 io_u_queued_complete+0x66 (/usr/bin/fio)
            55a0a98577d4 [unknown] (/usr/bin/fio)
            55a0a98591fa [unknown] (/usr/bin/fio)
            55a0a985b7d0 [unknown] (/usr/bin/fio)
            7f3afc0db179 start_thread+0xe9 (/usr/lib64/libpthread-2.28.so)
            7f3afb884dc2 __GI___clone+0x42 

那怎么将符号补全呢?我们可以通过安装 -debuginfo-dbgsym 包解决,例如对于 fio:

# centos 上,先使能 yum 的 debuginfo 源,再安装对应应用的 -debuginfo 包即可
$ cat /etc/yum.repos.d/CentOS-Linux-Debuginfo.repo 
[debuginfo]
name=CentOS Linux $releasever - Debuginfo
baseurl=http://debuginfo.centos.org/$releasever/$basearch/
gpgcheck=0
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial
$ yum clean all && yum makecache
$ yum -y install fio-debuginfo.x86_64

# ubuntu 上,先导入调试符号签名密钥,再安装对应应用的 -dbgsym 包即可
$ apt install ubuntu-dbgsym-keyring
$ apt install fio-dbgsym

补全后的栈如下所示:

$ perf script -i perf.data.dwarf
fio  2469  2823.211391:     250000 cpu-clock:pppH: 
            7f03631a89bd syscall+0x1d (/usr/lib64/libc-2.28.so)
            7f0363ef1c14 io_submit+0x34 (/usr/lib64/libaio.so.1.0.1)
            555976f418ce fio_libaio_commit+0xde (/usr/bin/fio)
            555976ef4a98 td_io_commit+0x58 (/usr/bin/fio)
            555976ef4fb5 td_io_queue+0x3f5 (/usr/bin/fio)
            555976f344ea do_io+0x71a (/usr/bin/fio)
            555976f36880 thread_main+0x18b0 (/usr/bin/fio)
            555976f38561 run_threads+0xcb1 (/usr/bin/fio)

后记

当你面对一个性能问题,如果选择使用 perf 观测,那么问题就变成了三个,另外两个是在解决性能问题前,必须先解决栈回溯和符号解析,前者影响观测准确性,后者影响观测可读性。perf 大部分时候都帮忙做好了,但如果遇到了些小困难,希望本文能有幸帮上一点忙。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1333159.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【期末考试】计算机网络、网络及其计算 考试重点

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 计算机网络及其计算 期末考点 &#x1f680;数…

Netty Review - 深入理解Netty: ChannelHandler的生命周期与事件处理机制

文章目录 概述CodeLifeCycleInBoundHandler 验证 概述 Netty的ChannelHandler是处理网络事件&#xff08;如数据读取、数据写入、连接建立、连接关闭等&#xff09;的核心组件。 在Netty中&#xff0c;ChannelHandler的生命周期与Channel的状态紧密相关&#xff0c;主要涉及到…

从mice到missForest:常用数据插值方法优缺点

一、引言 数据插值方法在数据处理和分析中扮演着至关重要的角色。它们可以帮助我们处理缺失数据&#xff0c;使得数据分析更加准确和可靠。数据插值方法被广泛应用于金融、医疗、社会科学等领域&#xff0c;以及工程和环境监测等实际应用中。 在本文中&#xff0c;我们将探讨三…

制作小米导航实验

一、实验题目 制作一个小米"手机"的标签&#xff0c;当鼠标移动到"手机"上后&#xff0c;显示右边的白色菜单&#xff0c;当鼠标离开"手机"时&#xff0c;菜单消失。 二、实验代码 <!DOCTYPE html> <html lang"en"> <…

高级人工智能之群体智能:蚁群算法

群体智能 鸟群&#xff1a; 鱼群&#xff1a; 1.基本介绍 蚁群算法&#xff08;Ant Colony Optimization, ACO&#xff09;是一种模拟自然界蚂蚁觅食行为的优化算法。它通常用于解决路径优化问题&#xff0c;如旅行商问题&#xff08;TSP&#xff09;。 蚁群算法的基本步骤…

python分割字符串 split函数

split函数用于字符串的分割&#xff0c;可以完成基于特定字符将字符串分割成若干部分并存于列表中 url "http://localhost:5000/static/images/DICOMDIR 5.26-6.1/10283674_GOUT_5_0_2.png" # 获取最后一个_的前一个数字 parts url.split(/) print(parts)在这里讲…

Kali Linux—借助 SET+MSF 进行网络钓鱼、生成木马、获主机shell、权限提升、远程监控、钓鱼邮件等完整渗透测试(二)

远控木马 SET 同时集成了木马生成工具&#xff0c;可以生成木马并调用MSF框架对远程主机进行控制。直接使用MSF生成木马并控制主机的可参考之前另一篇博文&#xff1a;渗透测试-Kali入侵Win7主机。 控制主机 1、运行 SET&#xff0c;选择创建攻击载荷和监听器&#xff1a; 2…

Tomcat日志乱码了怎么处理?

【前言】 tomacat日志有三个地方&#xff0c;分别是Output(控制台)、Tomcat Localhost Log(tomcat本地日志)、Tomcat Catalina Log。 启动日志和大部分报错日志、普通日志都在output打印;有些错误日志&#xff0c;在Tomcat Localhost Log。 三个日志显示区&#xff0c;都可能…

ETN21与CJ2M-CPU33通讯

实验设备:CJ2M-CPU33,CJ1W-EIP21,交叉网线 实验目的:手动建立数据链接表,建立TAG通讯。 实验步骤: IP地址设置:①usb线连上电脑,打开I/O表,将ETN21模块的ip地址与CJ2M-CPU33设置为同一个网段不同节点,节点号跟硬件上的node number一样,下载重启模块,如下: 配置n…

百分百能遇到的接口自动化测试面试题,看完的现在已经在办理入职了...

1. 什么是接口自动化测试&#xff1f; 答&#xff1a;接口自动化测试是指使用自动化工具对接口进行测试&#xff0c;验证接口的正确性、稳定性和性能等方面的指标。 2. 为什么要进行接口自动化测试&#xff1f; 答&#xff1a;接口自动化测试可以提高测试效率&#xff0c;减…

Redis源码精读:准备工作

文章目录 前言拉取源码项目结构源码阅读技巧最后 前言 我是醉墨居士&#xff0c;未来的一段时间里面我准备写一些关于Redis源码的文章&#xff0c;来帮助大家深入浅出Redis&#xff0c;希望大家多多支持&#x1fae0; 拉取源码 git clone https://github.com/redis/redis项目…

Spring5底层原理之BeanFactory与ApplicationContext

目录 BeanFactory与ApplicationContext BeanFactory ApplicationContext 容器实现 BeanFactory实现 ApplicationContext实现 ClassPathXmlApplicationContext的实现 AnnotationConfigApplicationContext的实现 AnnotationConfigServletWebServerApplicationContext的实…

驱动开发-1

一、驱动课程大纲 内核模块字符设备驱动中断 二、ARM裸机代码和驱动有什么区别&#xff1f; 1、共同点&#xff1a; 都能够操作硬件 2、不同点&#xff1a; 1&#xff09;裸机就是用C语言给对应的寄存器里面写值&#xff0c;驱动是按照一定的套路往寄存器里面写值 2&#xff09…

为什么有的开关电源需要加自举电容?

一、什么是自举电路&#xff1f; 1.1 自举的概念 首先&#xff0c;自举电路也叫升压电路&#xff0c;是利用自举升压二极管&#xff0c;自举升压电容等电子元件&#xff0c;使电容放电电压和电源电压叠加&#xff0c;从而使电压升高。有的电路升高的电压能达到数倍电源电压。…

阶段十-物业项目

可能遇到的错误&#xff1a; 解决jdk17javax.xml.bind.DatatypeConverter错误 <!--解决jdk17javax.xml.bind.DatatypeConverter错误--><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>…

git 使用方法自用(勿进)本地开发分支推上线上开发分支

一、//查看状态 1.git status 二、//查看改了哪个文件夹 1.git diff 2.//会出现改了哪个文件夹src/components/partials/Slider.js 三、//查看改了的文件夹里面具体改了啥内容 1.git diff src/components/partials/Slider.js 四、提交所有 1. git add . 五、写备注…

连锁便利店管理系统有什么用

连锁便利店管理系统对于连锁便利店的运营和管理非常有用。以下是一些常见的用途&#xff1a; 1. 库存管理&#xff1a;连锁便利店通常需要管理多个门店的库存&#xff0c;管理系统可以帮助实时掌握各个门店的库存情况&#xff0c;包括商品数量、进货记录、库存调拨等。这样可以…

【Linux系统基础】(2)在Linux上部署MySQL、RabbitMQ、ElasticSearch、Zookeeper、Kafka、NoSQL等各类软件

实战章节&#xff1a;在Linux上部署各类软件 前言 为什么学习各类软件在Linux上的部署 在前面&#xff0c;我们学习了许多的Linux命令和高级技巧&#xff0c;这些知识点比较零散&#xff0c;同学们跟随着课程的内容进行练习虽然可以基础掌握这些命令和技巧的使用&#xff0c;…

为什么c++的开源库那么少?

为什么c的开源库那么少&#xff1f; 在开始前我有一些资料&#xff0c;是我根据自己从业十年经验&#xff0c;熬夜搞了几个通宵&#xff0c;精心整理了一份「 C的资料从专业入门到高级教程工具包」&#xff0c;点个关注&#xff0c;全部无偿共享给大家&#xff01;&#xff01;…

Skywalking 中 Agent 自动同步配置源码解析

文章目录 前言正文实现架构实现模型OAP 同步 ApolloConfigWatcherRegisterConfigChangeWatcher Agent 侧 前言 本文代码 OAP 基于 v9.7&#xff0c;Java Agent 基于 v9.1&#xff0c;配置中心使用 apollo。 看本文需要配合代码“食用”。 正文 Skywalking 中就使用这种模型…