我们应该怎么分析QEMU代码中某段代码的性能呢?除了比较复杂的trace-event功能(我的博客中翻译过qemu tracing的文档),其实在QEMU自带有一个简单的profiler实现,它是一个简单的计时器封装。这篇博客主要介绍怎么在编译时开启、使用QEMU profiler,并说明怎么利用这个功能添加一个自己的计时器来分析QEMU中某段代码的性能。
1. 编译
我的代码是QEMU 2.12.0。要开启profiler功能,在编译前进行运行configure
的时候,只要加入--enable-profiler
选项就可以了,它会加入CONFIG_PROFILER
这个宏定义。比如我用如下选项进行编译:
1 2 3 4 5 |
cd QEMU_SRC_PATH ./configure --prefix=/PATH/TO/QEMU_BIN_DIR --target-list=x86_64-softmmu --enable-profiler make -j make install |
将其编译到QEMU后,无须改动启动参数,它是默认启用的,但是我们一般需要用QMP shell来查看它帮我们计时的信息。
2. 使用
在启动QEMU虚拟机的命令中,我们需要加入一个QMP socket,用于一会接入我们的QMP shell:
1 2 3 |
# -qmp后边接我们要创建的QMP socket文件的信息,这里我们将它创建在本目录(./qmp-sock文件) /PATH/TO/QEMU_BIN_DIR/bin/qemu-system-x86_64 ... -qmp unix:./qmp-sock,server,nowait |
在虚拟机启动后,用QEMU源码中自带的QMP shell脚本连接QMP socket:
1 2 3 4 |
# 这个脚本就是QEMU源码中的scripts/qmp/qmp-shell文件 # 加-H是为了以HMP命令进行交互,否则就得使用json格式,不方便 /PATH/TO/QEMU_SRC/scripts/qmp/qmp-shell -H ./qmp-sock |
之后出现以下欢迎界面:
1 2 3 4 5 |
Welcome to the HMP shell! Connected to QEMU 2.12.0 (QEMU) |
我们在(QEMU)
后面输入info profile
就可以看到QEMU自带的两个计时器的数值,每次查看完这个数值,数值会清零。
1 2 3 4 5 6 7 |
Welcome to the HMP shell! Connected to QEMU 2.12.0 (QEMU) info profile async time 18524863312 (18.525) qemu time 0 (0.000) |
3. 实现
QEMU源码中,计时器功能的实现,主要与monitor.c
和include/qemu/timer.h
这两个文件有关文件实现。而使用这个profiler进行计时的只有两个变量:一个是vl.c
的dev_time
(即上面的async time);一个是cpus.c
的tcg_time
(即上面的qemu time),由于我们使用的是KVM,没有用TCG,所以这个值应该一直为0。
首先,计时器变量都定义在monitor.c
,就是和QMP实现相关的代码文件中:
1 2 3 4 |
// monitor.c int64_t tcg_time; int64_t dev_time; |
查询计时器的代码也在这附近,可以看到,的确和我们刚才用info profile
查询时打印的信息一致,而且打印后进行了清零操作:
1 2 3 4 5 6 7 8 9 10 11 |
// monitor.c static void hmp_info_profile(Monitor *mon, const QDict *qdict) { monitor_printf(mon, "async time %" PRId64 " (%0.3f)\n", dev_time, dev_time / (double)NANOSECONDS_PER_SECOND); monitor_printf(mon, "qemu time %" PRId64 " (%0.3f)\n", tcg_time, tcg_time / (double)NANOSECONDS_PER_SECOND); tcg_time = 0; dev_time = 0; } |
include/qemu/timer.h
中实现了计时器相关的函数,所以实际进行计时的代码段会包含它,其中也有计时器变量的外部声明:
1 2 3 4 5 6 7 8 9 10 11 |
// include/qemu/timer.h #ifdef CONFIG_PROFILER static inline int64_t profile_getclock(void) { return get_clock(); } extern int64_t tcg_time; extern int64_t dev_time; #endif |
我们可以以dev_time
这个计时器为例看一下,开始和结束计时之间的代码就是被计时的代码,很自然的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// vl.c #include "qemu/timer.h" ... static void main_loop(void) { #ifdef CONFIG_PROFILER int64_t ti; #endif while (!main_loop_should_exit()) { #ifdef CONFIG_PROFILER ti = profile_getclock(); //开始计时 #endif main_loop_wait(false); // 被计时的就是这行代码 #ifdef CONFIG_PROFILER dev_time += profile_getclock() - ti; //结束计时并累加到计时器 #endif } } |
4. 扩展应用
根据这种思路,我们也很容易添加自己的计时器,只要以下几步:
- 在
include/qemu/timer.h
进行计时器变量声明; - 在
monitor.c
进行计时器变量定义,并在hmp_info_profile
函数加入自己喜欢的打印格式,在打印后也可以选择将计时器变量清零; - 在要计时的代码段所在文件中包含
qemu/timer.h
头文件; - 最后,在要计时的代码段前后加入
profile_getclock()
函数获取时间并累加到我们新定义的计时器变量上就可以了。
[1] 我的wiki–关于QMP, https://github.com/zhangjaycee/real_tech/wiki/virtual_018#接入qemu-monitor
[2] QEMU Docs, https://qemu.weilnetz.de/doc/qemu-doc.html