准备工作——安装perf
我是在github的codespace上搞的,可以按下面的方式安装perf:
sudo apt install linux-tools-generic
sudo apt install linux-cloud-tools-azure
参考在WSL2中使用perf性能剖析工具
测试程序——简单的C程序
弄一个无限循环的C程序进行测试:
// main.c
#include<stdio.h>
int add(int a, int b){ return a + b; }
int sub(int a, int b){ return a - b; }
int main(){
while(1){
sub(add(3, 4), 5);
}
return 0;
}
编译成可执行文件gcc main.c -o main
并运行。
获取这个进程的pid,用htop命令查看一下./main
对应的进程:
这里对应的pid是19647
监测并生成火焰图
使用perf程序进行监测:
sudo perf record -F 99 -p 19647 -a -g -- sleep 10
这里将采样频率设置为99次/秒,最高好像是100次,为保证准确,一般设置为99次。
该命令会生成perf.data文件,记录了进程的函数调用情况,随后从github将FlameGraph克隆到本地,执行如下命令:
sudo perf script > out.perf
../FlameGraph/stackcollapse-perf.pl out.perf > out.folded
./FlameGraph/flamegraph.pl out.folded > "./flamegraph.svg"
生成的./flamegraph.svg
可以在vscode中使用扩展SVG进行预览,右击./flamegraph.svg
进行预览就出来了,还是很方便的。
生成火焰图如下:
我们的主要函数依然是main,add,sub,在编译的过程中,他们的名称可能改变,比如C++中可能会添加前缀或下划线什么的。其中add和sub函数占用最多的计算资源,两者的比例分别为24.69%和30.04%,采样次数分别为2,293,939,370次和3,494,949,460次,在以效率著称的C语言中这很明显。
后面我测了一下python,相同的代码,add和sub分别被改名为long_add和long_sub,而两者占用的时间比例仅为1.12%和1.22%!!!采样次数分别为111,111,110次和131,313,130次。这里采样的时间是相同的,均为10s。python做了挺多额外工作的,毕竟是解释型语言(不过也可以用编译的方式来运行)。
python程序用perf生成的火焰图里面的名字会变得很奇怪,甚至不知道对应的是什么函数,用py-spy工具会好一些。这个工具很方便,通过pip install py-spy
即可安装,然后通过py-spy record --pid 130526 -o out.svg
就可以直接生成火焰图,很便捷。
这里要注意以下采样次数的意思,类似于频率的意思,采样是每隔一小段时间观测一下,如果这次观测中监测到了sub函数,则sub函数的次数加一。sub(3,494,949,460 samples, 30.04%),表示在10s内遇到过这个函数3,494,949,460次。由于是隔一段时间采样一下,所以难免有遗漏,但在大量的采用中还是具有统计意义的。
这在add和sub函数之后还有一些冒尖的函数,放大左右两边冒尖的函数情况如下:
虽然我们的C测试程序主要是main,add,sub三个函数,但编译链接的过程中,会将其他相关的包链接到程序中,使程序中的符号增加。所以程序执行的过程中,还会涉及到其他的一些函数,比如上面的asm_sysvec_reschedule_ipi
用于处理系统调度的中断处理函数。它负责在特定的时间间隔内触发调度器,使得其他进程有机会运行。
exit_to_user_mode_prepare
用于准备切换到用户模式的操作。在操作系统中,内核和用户模式之间的切换需要进行一些准备工作,这个函数就负责执行这些准备操作。
因为这个进程是开始一段时间后再监测的,所以有一些函数没有抓取到,通过readelf命令,我们可以看到main可执行文件中涉及到的所有符号:
$ readelf main -s
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 64 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000318 0 SECTION LOCAL DEFAULT 1
2: 0000000000000338 0 SECTION LOCAL DEFAULT 2
...
58: 0000000000004010 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
59: 0000000000001157 37 FUNC GLOBAL DEFAULT 14 main
60: 0000000000004010 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
61: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
62: 0000000000001141 22 FUNC GLOBAL DEFAULT 14 sub
63: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
如果要了解进程更详细的情况,还可以去/proc
文件夹下面看看,每个进程都建立了对应的一个文件夹,文件夹名称对应进程的pid。
比如通过ll /proc/19647/cwd
可以查看该进程的工作目录,通过ll /proc/19647/exe
可以查看该进程对应的执行文件。
最后,为便于下次使用,我将生成火焰图的过程整理成脚本perf.sh
,直接执行bash perf.sh pid
就好了,生成的火焰图文件名为flamegraph_pid_$pid.svg
,perf.sh
脚本内容如下:
#!/bin/bash
# Check if the number of arguments is correct
if [ $# -ne 1 ]; then
echo "Usage: $0 <pid>"
exit 1
fi
pid=$1
mkdir -p temp
# Check if FlameGraph folder exists
if [ ! -d "FlameGraph" ]; then
# Clone FlameGraph repository
git clone https://github.com/brendangregg/FlameGraph.git
fi
cd temp || exit
sudo perf record -F 99 -p $pid -a -g -- sleep 10
sudo perf script > out.perf
current_time=$(date + "%Y%m%d%H%M%S")
mv perf.data "perf_pid_${pid}_$current_time.data"
../FlameGraph/stackcollapse-perf.pl out.perf > out.folded
../FlameGraph/flamegraph.pl out.folded > "../flamegraph_pid_$pid.svg"