大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。
今天我们来聊一聊perf的相关命令,更进一步的了解perf。本文是perf系列的第七篇文章,后续会继续介绍perf,包括用法、原理和相关的经典文章。
引
在前面的文章中,我们介绍了可以通过perf script
命令解析perf.data
,可以通过perf report
命令查看函数的调用占比,可以通过perf annotate
命令查看热点汇编,那么这些能力perf
究竟是怎么实现的呢?
为了解答这个问题,笔者尝试过去阅读源码,但是源码阅读需要非常多的时间,容易在一些细枝末节的问题上纠结。因此,笔者尝试通过strace
和对比实验的方法来尝试猜测以下几个问题的答案:
perf
是如何将perf.data
中的地址转换成函数名的?为什么解析出来经常出现[unknown]
?perf report
是如何进行函数调用占比的计算的?perf annotate
是如何得到函数的热点汇编的?
今天我们主要尝试解答第一个问题。
工欲善其事必先利其器
首先我们来简单的回顾一下perf script
、perf report
和perf annotate
。
perf script
可以帮助我们把perf.data
转换成可以阅读的形式:
[root@VM-16-2-centos ~]# perf script
perf 3376223 [000] 13088049.893744: sched:sched_stat_runtime: comm=perf pid=3376223 runtime=25348 [ns] vruntime=22835234754203 [ns]
perf 3376223 [000] 13088049.893747: sched:sched_waking: comm=migration/0 pid=12 prio=0 target_cpu=000
perf 3376223 [000] 13088049.893748: sched:sched_stat_runtime: comm=perf pid=3376223 runtime=4759 [ns] vruntime=22835234758962 [ns]
perf 3376223 [000] 13088049.893748: sched:sched_switch: prev_comm=perf prev_pid=3376223 prev_prio=120 prev_state=R ==> next_comm=migration/0 next_pid=12 next_prio=0
migration/0 12 [000] 13088049.893750: sched:sched_migrate_task: comm=perf pid=3376223 prio=120 orig_cpu=0 dest_cpu=1
perf report
可以图形化的展示收集到的调用栈信息:
perf report
perf annotate
可以输出perf.data
文件对应的汇编信息:
perf annotate
我们再介绍一下本文中经常用到的strace
工具。strace
是一种Linux系统下的工具,它可以帮助你跟踪和调试进程的系统调用。系统调用是应用程序和操作系统之间的接口,它们允许应用程序访问操作系统提供的各种服务。strace
可以记录这些系统调用,包括它们的参数和返回值,以及调用的时间和持续时间。
举个例子,如果你想了解一个程序为什么崩溃了,你可以使用strace来查看它的系统调用。你只需要在终端中输入"strace <你的程序>"即可开始跟踪。strace
会输出程序执行期间的所有系统调用,你可以通过查看输出来找到导致崩溃的原因。
perf script解析:众里寻他千百度
现在,我们来探索第一个问题:perf script
是如何解析perf.data
的?
在机器上我们通过一个c 程序来制造负载:
代码语言:javascript复制#include <iostream>
#include <cmath>
#include <chrono>
using namespace std;
void function1() {
for (int i = 0; i < 1000000; i ) {
pow(i, 2);
}
}
void function2() {
for (int i = 0; i < 1000000; i ) {
sin(i);
}
}
int main() {
auto start = chrono::high_resolution_clock::now();
for (int i = 0; i < 1000; i ) {
function1();
function2();
}
auto end = chrono::high_resolution_clock::now();
auto duration = chrono::duration_cast<chrono::milliseconds>(end - start);
cout << "Execution time: " << duration.count() << " milliseconds" << endl;
return 0;
}
记得编译的时候带上-g
选项:
g -g -o test test.c
追踪转换一条龙:
代码语言:javascript复制sudo perf record -ag -F 999 -- sleep 2
sudo perf script
可以看到成功的抓到了我们的负载:
成功解析的调用栈
尝试用strace
追踪perf script
的过程并将结果保存到文件中:
sudo strace -o strace perf script
在结果中,我们找到了和这个负载文件有关的一些调用:
strace结果
stat("/root/workplace/test", {st_mode=S_IFREG|0755, st_size=60016, ...}) = 0
:这个系统调用是用来获取指定文件的元数据信息,包括文件的权限、大小等等。在这个例子中,它返回了0,表示获取成功。其中st_mode
表示文件的权限和类型,st_size
表示文件的大小。
openat(AT_FDCWD, "/root/workplace/test", O_RDONLY) = 41
:这个系统调用是用来打开指定的文件,其中O_RDONLY
表示以只读方式打开。在这个例子中,它返回了文件描述符41,表示打开成功。
可以看到perf script
去读了记录路径的文件,那如果这个文件不存在会发生什么呢?我们将负载程序移动走,重新perf script
,依然看到了对应的结果:
成功解析调用栈
这是为什么呢?我们再用strace
抓取一下:
移除源文件后strcae结果
可以看到这里多了几行,后面去读取的什么.debug
文件里的东西是什么?注意到后面有个build-id
,好像perf
中也有和这个相关的功能,我们不妨来看看build-id
是什么:buildid
是一个用于标识可执行文件和共享库的唯一标识符。它是由编译器在编译时生成的,通常包含在ELF格式的可执行文件和共享库中。buildid
可以用来识别不同版本的程序,以及检查程序是否被篡改过。在调试时,它还可以用来定位程序崩溃的原因。我们用file
查看负载文件信息:
[root@VM-16-2-centos workplace]# file test1
test1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=bdb424b13087ec31173954f3be40fa6498d1bc95, with debug_info, not stripped
可以看到后面的buildid
和系统调用中后面去寻找的目录是一致的:
寻找buildid
那如果这里也找不到会怎么样呢?我们通过perf buildid-cache
功能清除掉全部的缓存:
查看buildid-list
清理buildid-cache
我们再去通过strace
和perf script
来进行转换:
解析调用栈失败
可以看到这里出现了多个unknown
,说明此时perf script
已经搞不清楚这里到底是什么了。我们来看看strace
的结果:
清理buildid-cache后的strace结果
可以看出来,当源目录和$HOME/.debug/.build-id
目录下不存在时,perf script
还会去找/usr/lob/debug
下的一些文件,尝试去寻找到源文件,或者说源文件对应的debug信息。
至此,我们可以得出一个结论:perf script
需要依赖源文件的信息进行解析,首先会去寻找源目录下的文件,当找不到时会去寻找$HOME/.debug目录下的文件,最后会去/usr/lib下的信息,当都找不到时,perf script
会解析成[unknown]