原文出处:QEMU当前的文档 qemu/docs/devel/tracing.txt
一、介绍
这篇文档描述了QEMU追踪的整体架构和如何使用追踪功能来进行debug、性能分析或者监测运行状况。
二、快速开始
步骤1. 编译QEMU时带上’simple’trace追踪后端:
./configure --enable-trace-backends=simple
make
步骤2. 创建一个写有你想追踪的事件(events)的文件
echo memory_region_ops_read >/tmp/events
步骤3. 运行虚拟机来产生一个追踪文件
qemu -trace events=/tmp/events ... #加上其他你需要的qemu选项参数
步骤4. 以美观的形式打印出二进制的追踪文件Pretty-print the binary trace file:
./scripts/simpletrace.py trace-events-all trace-* #用QEMU的进程号(pid)来代替"*"
三、Trace events
1. 设置子文件夹 Sub-directory setup
在源码目录的每个文件夹中都可以在trace-events
文件中声明一组静态的trace events。所有包含有trace-events
文件的子文件夹必须被列在源码根目录Makefile.objs
的trace-events-subdirs
项中。这样在编译时,被列出的子文件夹的trace-events
文件就会被tracetool
脚本处理来生成相关的追踪代码。
单独的trace-events
文件会被merged到一个trace-events-all
文件中,trace-events-all
文件也将会被安装到/usr/share/qemu
目录中,并改名为trace-events
,这个文件最终会被simpletrace.py
脚本用作simpletrace数据格式的追踪分析。
在子文件夹中,以下的文件会被自动生成
- trace.c – the trace event state declarations
- trace.h – the trace event enums and probe functions
- trace-dtrace.h – DTrace event probe specification
- trace-dtrace.dtrace – DTrace event probe helper declaration
- trace-dtrace.o – binary DTrace provider (generated by dtrace)
- trace-ust.h – UST event probe helper declarations
子文件夹中的源码文件应该include本地的trace.h
文件,同时不用任何其他子文件夹的路径前缀,比如io/channel-buffer.c
中这样写来找到io/trace.h
文件:
#include "trace.h"
尽管从其他子文件夹也能找到trace.h
文件,但是一般不鼓励这样做,强烈推荐所有的events都在对应的子文件夹直接声明。唯一的例外是有一些共享的trace events,它们会在顶层文件夹的trace-events文件中被定义。顶层文件夹生成的trace文件的前缀是trace-root
而不是trace
,这是为了防止顶层文件夹和当前文件夹中的trace.h
文件产生歧义。
2. 使用trace events
Trace events会直接被如下源码唤起:
#include "trace.h" /* needed for trace event prototype */
void *qemu_vmalloc(size_t size)
{
void *ptr;
size_t align = QEMU_VMALLOC_ALIGN;
if (size < align) {
align = getpagesize();
}
ptr = qemu_memalign(align, size);
trace_qemu_vmalloc(size, ptr);
return ptr;
}
3. 声明trace events
tracetool
脚本生成被所有使用trace events功能的源文件包含的trace.h
头文件。由于很多的源文件都包含trace.h
,所以它尽量使用最少的数据类型,包含最少的其他的头文件,来保证命名空间的简洁化和编译所需时间、依赖的最小化。
Trace events这样使用数据类型:
- 定长类型使用stdint.h的类型。很多偏移量和Guest内存地址最好用
uint32_t
或uint64_t
,用定长变量而不是大小会随Host操作系统(32bit或64bit)改变的原始变量,这样trace events就不会截断变量值或打断编译。 -
用
void *
来定义结构体或数组。trace.h
头文件无法包含所有的用户定义的结构体,所以用void *
来定义指向结构体的指针。 -
其他的变量,用恰当的原始变量(char, int, long)。
格式化字符串应该反应trave event中定义的类型。特别注意分别用PRId64
和PRIu64
来代替int64_t
和uint64_t
类型。这会保证32和64位平台间的移植性。
每个event声明会以event的名字开始,然后是它的参数,最后是一个易于阅读的格式化字符串。比如:
qemu_vmalloc(size_t size, void *ptr) "size %zu ptr %p"
qemu_vfree(void *ptr) "ptr %p"
4. 添加新trace events的一些提示
- 追踪代码的变化。代码中值得关注的点经常涉及到状态的变化,比如starting, stopping, allocating, freeing。关注状态变化的追踪是好的追踪,因为它们可以被用来理解系统的运行流程。
-
追踪Guest的操作。Guest的I/O操作,比如读设备寄存器是好的追踪点,因为这可以用来理解Guest的交互过程。
-
用相关的字段,这样有相对独立上下文的的追踪输出更易被理解。比如,追踪一个malloc返回的将在free中被释放的指针,可以将成对的malloc和free联系起来。没有上下文的trace events通常没什么用。
-
在trace event的函数名后加入一些其他命名记号。如果一个函数有多个trace events,应该在trace event函数后追加一些用于区别的命名。
四、一般接口和监视器指令
你可以通过头文件trace/control.h
提供的不同的后端程序接口,编写query程序来控制trace events的状态。
注意一些后端没有实现部分接口,这会导致QEMU打印一条警告(please refer to
header “trace/control.h” to see which routines are backend-dependent)。
events的状态也可以通过监视器指令(monitor commands)请求或修改:
info trace-events #查看当前可用的trave events及它们的状态,状态1是开启状态,状态0是关闭状态。
trace-event NAME on|off #打开或关闭一个或一组(使用通配符)指定的trace event。
-trace events=<file>
命令行参数可以用来在qemu程序开始时制定<file>
中列出的events,这个文件中的每行都包含一个event名字。
如果-trace events=<file>
中的行以-
开始,这个event就默认为关闭状态。在使用通配符来开启一组events同时又希望关闭其中某个烦人的event时,这种方法很有用。
通配符匹配在监视器指令trave-event
和events列表文件中都可以使用。这意味着你可以批量开启或关闭有相同前缀的一组events。比如,有关virtio-blk的所有trace events可以用以下监视器指令开启:
trace-event virtio_blk_* on
五、Trace后端
“tracetool”脚本负责自动生成了冗长的trace event代码,也保证了trace event的声明和trace backend无关。每个event并没有被绑定到某个特定的trace后端(比如LTTng或SystemTap)。扩展tracetool
脚本可以添加对trace后端的支持。
trace后端是在configure的时候被指定的:
./configure --enable-trace-backends=simple
./configure --help
命令或下文中有支持trace后端的列表。如果多个后端被同时开始,trace信息会被同时发送给所有开启的后端。
如果没有选择某个特定的后端,configure会默认指定log
为后端。
下文将分别介绍目前支持的trace后端。
Nop
“nop”后端生成空的trace event函数,所以编译器会把trace events全部优化掉,这样可以做到没有性能的损失。
注意,不论选择什么后端,带有disable
属性的events都会使用”nop”后端生成。
Lof
“log”后端直接将trace events输出到标准错误(stderr),这就相当于把trace events都转化为了debug的printf。
这是最简单的后端,而且可以和原本的使用DPRINTF()的代码一起使用。
Simpletrace
“simple”后端支持一般的使用场景,并且就在QEMU的源码树中。它可能不像特定平台或者第三方追踪后端那样强大,但是它一定是可移植的。除非你有特殊的高阶需求,否则这是最被推荐的trace后端。
Ftrace
“ftrace”后端将trace数据写到ftrace marker中。这相当于将trace events发送到ftrace环状缓冲区中,然后你可以拿qemu的trace数据和内核的trace数据(尤其是应用KVM时的kvm.ko内核模块)作比照来看了。
如果你用KVM,在ftrace中开启kvm events:
# echo 1 > /sys/kernel/debug/tracing/events/kvm/enable
在用root用户运行qemu后,你就可以得到trace了:
# cat /sys/kernel/debug/tracing/trace
局限性:”ftrace”后端只能在Linux平台使用。
Syslog
“syslog”后端用POSIX的syslog
API发送trace events,日志被以特定的LOG_DAEMON
设备或LOG_PID
选项打开(所以events会被打上生成它们的QEMU进程的pid标签)。所有的events会被日志记录在LOG_INFO
级别。
注意:syslog可能会因压缩连续重复的trace events或进行速率限制。
局限性:“syslog”后端只能在支持POSIX的系统中使用。
监视器命令
# 打开或关闭或刷新trace文件或者设置trace文件名。
trace-file on|off|flush|set <path>
分析trace文件
“simple”后端产生可以被simpletrace.py
脚本格式化输出的二进制trace文件。这个脚本需要”trace-events-all”文件和二进制trace两个参数:
./scripts/simpletrace.py trace-events-all trace-12345
你必须确认编译QEMU时用的就是同一个”trace-events-all”文件,否则trace event的声明可能会被改变,进而导致生成不连续的输出。
LTTng 用户空间 Tracer
“ust”后端用LTTng Userspace Tracer库。这个库没有编译到QEMU的监视器命令,所以要用UST工具来进行代替。UST工具可以进行list,
enable/disable, 和dump traces等操作。
对于用户空间的追踪,lttng-tools
包被需要。你必须确保当前的用户属于”tracing”用户组,或者在运行任意的QEMU实例前手动运行lttng-sessiond
守护进程。
当运行配置适当的QEMU,LTTng就可以进行event操作:
lttng list -u # 列出所有可用的events
lttng create mysession # 创建tracing session
lttng enable-event qemu:g_malloc -u # 开启events, events可以用逗号隔开,或者用-a表示开启所有的events
lttng start # 开启 lltng
lttng stop # 关闭 lltng
lttng view # 查看 lltng
lttng destroy #注销tracing session
babeltrace
工具可以用来在之后查看trace:
babeltrace $HOME/lttng-traces/mysession-<date>-<time>
SystemTap
“dtrace”后端用”DTrace sdt probes”,但是只被和SystemTap进行了测试。当SystemTap支持被检测到时,一个带有probes包装的.stp
文件会被生成来用于脚本中。这步也可以在编译后手动执行,来改变”.stp probes”中二进制文件的名字:
scripts/tracetool.py --backends=dtrace --format=stap \
--binary path/to/qemu-binary \
--target-type system \
--target-name x86_64 \
<trace-events-all >qemu.stp
Trace event属性
每个”trace-events-all”文件中的event项,都可以有0个或多个如下属性的前缀,属性由空格分开。
“disable”
如果某个特定的trace event会被调用很多次,这很可能引起肉眼可辨的性能损失,即使evnet是可以编程来关闭了。
这种情况下你可以声明一个有disable
属性的event。这会有效地在编译时关闭event(通过使用”nop”后端),因此就没有性能上的影响了(除非你又编辑了trace-events-all
文件)。
而且,有些情况下相对复杂的计算会被执行,用来专门产生trace函数的参数。在这些情况下,可以利用编辑TRACE_${EVENT_NAME}_ENABLED
这类宏变量,来避免event关闭后由这些计算带来的额外编译,例如:
#include "trace.h" /* needed for trace event prototype */
void *qemu_vmalloc(size_t size)
{
void *ptr;
size_t align = QEMU_VMALLOC_ALIGN;
if (size < align) {
align = getpagesize();
}
ptr = qemu_memalign(align, size);
if (TRACE_QEMU_VMALLOC_ENABLED) { /* 预处理宏 */
void *complex;
/* 一些复杂的计算来产生相对复杂的参数complex */
trace_qemu_vmalloc(size, ptr, complex);
}
return ptr;
}
通过用trace_event_get_state_backends
程序,你可以同时检查event是不是被禁止了或者是不是被动态开启了(头文件trace/control.h
可以看到更多相关信息)。
“tcg”
通过TCG产生的Guest代码可以用带有tcg
事件属性的定义来进行追踪。在内部,这个属性会生成两个event:<eventname>_trans
来追踪翻译时间,<eventname>_exec
来追踪运行时间。
也就是说,在TCG代码翻译生成期间,你应该用trace_<eventname>_tcg
函数而不是用两个events,这个函数会在Guest代码生成期间动态调用trace_<eventname>_trans
,并在生成必要的TCG代码来在Guest代码执行期间调用trans_<eventname>_exec
。
带有tcg
属性的events可以在trace-events
文件中被声明,并且混合有原始类型和TCG类型,而且trace_<eventname>_tcg
会优雅地把它们转发到trace_<eventname>_trans
和trace_<eventname>_exec
中。由于翻译阶段,TCG的变量值无法被得知,它们会被trace_<eventname>_trans
阶段忽略。因此,在trace-events
文件的入口需要两种打印格式(以逗号隔开):
tcg foo(uint8_t a1, TCGv_i32 a2) "a1=%d", "a1=%d a2=%d"
例如:
#include "trace-tcg.h"
void some_disassembly_func (...)
{
uint8_t a1 = ...;
TCGv_i32 a2 = ...;
trace_foo_tcg(a1, a2);
}
它会马上过调用:
void trace_foo_trans(uint8_t a1);
然后会生成TCG代码来调用:
and will generate the TCG code to call:
void trace_foo(uint8_t a1, uint32_t a2);
“vcpu”
有些events追踪特定vCPU,它含蓄地添加一个CPUState*
参数,并且扩展追踪打印格式来展示vCPU信息。如果和tcg
属性一起用,会添加第二个TCGv_env
参数,这个参数必须指向(指向Guest代码运行时的vCPU的)单目标中的全局TCG寄存器(通常是cpu_env
变量)。
tcg
和vcpu
属性当前只保证在根目录的./trace-events
文件中能用。
以下例子event,
foo(uint32_t a) "a=%x"
vcpu bar(uint32_t a) "a=%x"
tcg vcpu baz(uint32_t a) "a=%x", "a=%x"
可以被用作:
#include "trace-tcg.h"
CPUArchState *env;
TCGv_ptr cpu_env;
void some_disassembly_func(...)
{
/* trace emitted at this point */
trace_foo(0xd1);
/* trace emitted at this point */
trace_bar(ENV_GET_CPU(env), 0xd2);
/* trace emitted at this point (env) and when guest code is executed (cpu_env) */
trace_baz_tcg(ENV_GET_CPU(env), cpu_env, 0xd3);
}
如果翻译vCPU地址为0xc1,而代码会被执行在0xc2上,以下是示例输出:
// at guest code translation
foo a=0xd1
bar cpu=0xc1 a=0xd2
baz_trans cpu=0xc1 a=0xd3
// at guest code execution
baz_exec cpu=0xc2 a=0xd3