我们应该怎么分析QEMU代码中某段代码的性能呢?除了比较复杂的trace-event功能(我的博客中翻译过qemu tracing的文档),其实在QEMU自带有一个简单的profiler实现,它是一个简单的计时器封装。这篇博客主要介绍怎么在编译时开启、使用QEMU profiler,并说明怎么利用这个功能添加一个自己的计时器来分析QEMU中某段代码的性能。
1. 编译
我的代码是QEMU 2.12.0。要开启profiler功能,在编译前进行运行configure
的时候,只要加入--enable-profiler
选项就可以了,它会加入CONFIG_PROFILER
这个宏定义。比如我用如下选项进行编译:
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:
# -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:
# 这个脚本就是QEMU源码中的scripts/qmp/qmp-shell文件
# 加-H是为了以HMP命令进行交互,否则就得使用json格式,不方便
/PATH/TO/QEMU_SRC/scripts/qmp/qmp-shell -H ./qmp-sock
之后出现以下欢迎界面:
Welcome to the HMP shell!
Connected to QEMU 2.12.0
(QEMU)
我们在(QEMU)
后面输入info profile
就可以看到QEMU自带的两个计时器的数值,每次查看完这个数值,数值会清零。
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实现相关的代码文件中:
// monitor.c
int64_t tcg_time;
int64_t dev_time;
查询计时器的代码也在这附近,可以看到,的确和我们刚才用info profile
查询时打印的信息一致,而且打印后进行了清零操作:
// 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
中实现了计时器相关的函数,所以实际进行计时的代码段会包含它,其中也有计时器变量的外部声明:
// 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
这个计时器为例看一下,开始和结束计时之间的代码就是被计时的代码,很自然的方式:
// 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