一文了解perf script中[unknwon]出现的原因

2023-11-01 17:04:17 浏览数 (1)

大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。

今天我们来聊一聊perf的相关命令,更进一步的了解perf。本文是perf系列的第七篇文章,后续会继续介绍perf,包括用法、原理和相关的经典文章。

在前面的文章中,我们介绍了可以通过perf script命令解析perf.data,可以通过perf report命令查看函数的调用占比,可以通过perf annotate命令查看热点汇编,那么这些能力perf究竟是怎么实现的呢?

为了解答这个问题,笔者尝试过去阅读源码,但是源码阅读需要非常多的时间,容易在一些细枝末节的问题上纠结。因此,笔者尝试通过strace和对比实验的方法来尝试猜测以下几个问题的答案:

  • perf是如何将perf.data中的地址转换成函数名的?为什么解析出来经常出现[unknown]
  • perf report是如何进行函数调用占比的计算的?
  • perf annotate是如何得到函数的热点汇编的?

今天我们主要尝试解答第一个问题。

工欲善其事必先利其器

首先我们来简单的回顾一下perf scriptperf reportperf annotate

perf script可以帮助我们把perf.data转换成可以阅读的形式:

代码语言:javascript复制
[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选项:

代码语言:javascript复制
g   -g -o test test.c

追踪转换一条龙:

代码语言:javascript复制
sudo perf record -ag -F 999 -- sleep 2
sudo perf script

可以看到成功的抓到了我们的负载:

成功解析的调用栈

尝试用strace追踪perf script的过程并将结果保存到文件中:

代码语言:javascript复制
 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查看负载文件信息:

代码语言:javascript复制
[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

我们再去通过straceperf 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]

0 人点赞