QEMU跟踪 / QEMU文档翻译: Tracing

原文出处: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.objstrace-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_tuint64_t,用定长变量而不是大小会随Host操作系统(32bit或64bit)改变的原始变量,这样trace events就不会截断变量值或打断编译。

  • void *来定义结构体或数组。trace.h头文件无法包含所有的用户定义的结构体,所以用void *来定义指向结构体的指针。

  • 其他的变量,用恰当的原始变量(char, int, long)。

格式化字符串应该反应trave event中定义的类型。特别注意分别用PRId64PRIu64来代替int64_tuint64_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的一些提示

  1. 追踪代码的变化。代码中值得关注的点经常涉及到状态的变化,比如starting, stopping, allocating, freeing。关注状态变化的追踪是好的追踪,因为它们可以被用来理解系统的运行流程。

  2. 追踪Guest的操作。Guest的I/O操作,比如读设备寄存器是好的追踪点,因为这可以用来理解Guest的交互过程。

  3. 用相关的字段,这样有相对独立上下文的的追踪输出更易被理解。比如,追踪一个malloc返回的将在free中被释放的指针,可以将成对的malloc和free联系起来。没有上下文的trace events通常没什么用。

  4. 在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的syslogAPI发送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>_transtrace_<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变量)。

tcgvcpu属性当前只保证在根目录的./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

Leave a Reply

Your email address will not be published. Required fields are marked *