本篇介绍QEMU的事件循环机制、QOM和线程模型,从而为更深入地了解QEMU的具体部件做准备。
1. 事件循环机制
Windows平台上,QEMU需要从三个地方进行poll:
- 全局变量
GArray* gpollfds,其中的每一个元素是GPollFD; - glib提供的完整框架。glib是一个跨平台的底层库,提供的接口均以g打头;
- 全局变量
WaitObjects wait_objects,是一个自定义的结构,包含事件类型和回调函数等。
但不管是什么机制,思路都是一样的:
- 确定要poll的fd和event集合;
- 执行poll(或者select);
- 处理所有poll到的事件,调用其回调函数。
下面简单分析。
1.1. main_loop初始化
在qemu_init_main_loop中完成初始化。
首先完成的是时钟和信号的初始化。信号是Linux机制,QEMU通过signalfd系统调用将一个信号集合变成可读的fd,进而用qemu_set_fd_handler设置该fd的回调函数,并加入到iohandler_ctx事件源里。在Windows上这一函数直接返回0。
int ret;
init_clocks(qemu_timer_notify_cb);
ret = qemu_signal_init(errp);
if (ret) {
return ret;
}
接下来就是和事件机制直接相关的了,首先是qemu_aio_context:
qemu_aio_context = aio_context_new(&local_error);
if (!qemu_aio_context) {
error_propagate(errp, local_error);
return -EMFILE;
}
src = aio_get_g_source(qemu_aio_context);
g_source_set_name(src, "aio-context");
g_source_attach(src, NULL);
g_source_unref(src);
然后是iohandler_ctx,它没有像上面一样专门new一下,因为iohandler_get_g_source和qemu_set_fd_handler(如果是Linux平台,在设置信号时就会调用这个函数)都会判断iohandler_ctx是否已经初始化,否则调用aio_context_new。
src = iohandler_get_g_source();
g_source_set_name(src, "io-handler");
g_source_attach(src, NULL);
g_source_unref(src);
上面两个东西都被称为事件源,它们是AioContext类型,是GSource的子类(不是QOM里面的继承机制!),通过g_source_attach注册到给定的上下文中。传入的上下文为NULL,则注册到默认上下文。
这些都是用于glib的概念。glib库中设计了主事件循环(GMainLoop)机制,每一个主事件循环都有其主上下文(GMainContext)。glib还提供了一个默认的主上下文,它可以通过g_main_context_default获得。在不指定GMainLoop时,这一上下文就是默认采用的上下文。
1.2. 三种事件机制
注意下面是围绕Windows平台来讲的,Linux平台将有所不同,特别是gpollfds的部分。
首先介绍对gpollfds的poll操作。gpollfds数组中存放的是GPollFD结构体,它包含fd、events和revents三个变量,fd不用多说,events用来表示该fd上有什么样的事件需要poll,而revents表示已经发生的事件的类型。
pollfds_fill将gpollfds中的所有需要监听的事件加入监听列表,随后用select系统调用检查是否有就绪的fd。如果有,pollfds_poll会将就绪的fd在gpollfds对应的表项上设revents位。
要我说这些代码还是封装在一个额外的函数更好,这样就和glib的框架API是一个等级了,看起来比较清楚。
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_ZERO(&xfds);
nfds = pollfds_fill(gpollfds, &rfds, &wfds, &xfds);
if (nfds >= 0) {
select_ret = select(nfds + 1, &rfds, &wfds, &xfds, &tv0);
if (select_ret != 0) {
timeout = 0;
}
if (select_ret > 0) {
pollfds_poll(gpollfds, nfds, &rfds, &wfds, &xfds);
}
}
可是后面找不到对应执行回调的代码了,我也不知道为什么。
接下来介绍glib框架机制。通过g_main_context_acquire来获取默认上下文context的所有权,以便后面使用prepare、query、check、dispatch等函数。acquire是可以看成对一个可递归锁的非阻塞获取。接下来下面调用g_main_context_prepare进行准备,然后调用g_main_context_query获取需要poll的fds,它们放在poll_fds数组中。
g_main_context_acquire(context);
/* ... */
g_main_context_prepare(context, &max_priority);
n_poll_fds = g_main_context_query(context, max_priority, &poll_timeout,
poll_fds, ARRAY_SIZE(poll_fds));
下面设置超时时间。
if (poll_timeout < 0) {
poll_timeout_ns = -1;
} else {
poll_timeout_ns = (int64_t)poll_timeout * (int64_t)SCALE_MS;
}
poll_timeout_ns = qemu_soonest_timeout(poll_timeout_ns, timeout);
接下来打开QEMU全局锁qemu_global_mutex,它也称QEMU大锁(BQL),在后面会有介绍。然后打开重放锁replay,它用于向日志中写入接下来将要发生的读写事件。然后就是poll读写事件,poll完后顺次关上两把锁。
qemu_poll_ns函数和g_poll是一个功能,只不过时间粒度精确到纳秒。poll_fds数组中存放的是GPollFD结构体,g_poll会将所有已发生事件写入到对应结构体的revents域中。
qemu_mutex_unlock_iothread();
replay_mutex_unlock();
g_poll_ret = qemu_poll_ns(poll_fds, n_poll_fds + w->num, poll_timeout_ns);
replay_mutex_lock();
qemu_mutex_lock_iothread();
然后调用g_main_context_check,将上次query到的结果传回给主循环,再通过g_main_context_dispatch完成事件分发,最后通过g_main_context_release释放上下文。猜测WaitObjects和g_main_context_dispatch分别调用不同的回调函数;WaitObjects是为支持Windows所需要的特性。
if (g_main_context_check(context, max_priority, poll_fds, n_poll_fds)) {
g_main_context_dispatch(context);
}
g_main_context_release(context);
最后介绍WaitObjects机制。它的待poll集合是由上面的qemu_poll_ns统一poll的,所以用下面的操作append到poll_fds:
for (i = 0; i < w->num; i++) {
poll_fds[n_poll_fds + i].fd = (DWORD_PTR)w->events[i];
poll_fds[n_poll_fds + i].events = G_IO_IN;
}
poll完之后,各回各家,各找各妈。这些事件源没有加入glib框架,所以只能手动回调。
if (g_poll_ret > 0) {
for (i = 0; i < w->num; i++) {
w->revents[i] = poll_fds[n_poll_fds + i].revents;
}
for (i = 0; i < w->num; i++) {
if (w->revents[i] && w->func[i]) {
w->func[i](w->opaque[i]);
}
}
}
但是从debug的过程看这三种机制:
gpollfds中仅有一个事件,但从未发生过,难怪不执行回调函数也没有问题。WaitObjects从整个repo中看只有一个函数win_stdio_wait_func会调用。- 其它应该都是glib事件框架管理的。
所以后面还是会重点学习glib事件机制。
2. QEMU Object Model
本节以edu设备为例,讲解各种C语言大型项目都有的东西——面向对象设计。
2.1. 类型注册
pci_edu_register_types构造了TypeInfo类型的edu_info。它用来描述一个新类:
- 类名是
TYPE_PCI_EDU_DEVICE; - 继承自
TYPE_PCI_DEVICE; - 给定占用空间和类初始化、实例初始化的函数;
- 接口,就是让你想起多继承的那个接口。
用type_init注册类型。
static void pci_edu_register_types(void)
{
static InterfaceInfo interfaces[] = {
{INTERFACE_CONVENTIONAL_PCI_DEVICE},
{},
};
static const TypeInfo edu_info = {
.name = TYPE_PCI_EDU_DEVICE,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(EduState),
.instance_init = edu_instance_init,
.class_init = edu_class_init,
.interfaces = interfaces,
};
type_register_static(&edu_info);
}
type_init(pci_edu_register_types)
不妨把type_init宏打开一看:
static void __attribute__((constructor))
do_qemu_init_pci_edu_register_types(void) {
register_module_init(pci_edu_register_types, MODULE_INIT_QOM);
}
constructor是编译器属性,这些函数的执行将早于main,即所有QOM类型在main之前即完成注册。看一下注册函数:
void register_module_init(void (*fn)(void), module_init_type type)
{
ModuleEntry *e;
ModuleTypeList *l;
e = g_malloc0(sizeof(*e));
e->init = fn;
e->type = type;
l = find_type(type);
QTAILQ_INSERT_TAIL(l, e, node);
}
QOM类型的type都是MODULE_INIT_QOM,传入e->type;而e->init是构造函数,未来在module_call_init中被调用。注册结束前将该QOM类型加入类型链表。module_call_init会顺着类型链表往下,逐个初始化。
void module_call_init(module_init_type type)
{
ModuleTypeList *l;
ModuleEntry *e;
if (modules_init_done[type]) {
return;
}
l = find_type(type);
QTAILQ_FOREACH(e, l, node) {
e->init();
}
modules_init_done[type] = true;
}
执行初始化函数的时候,type_register_static最终调用到type_register_internal,在其中根据传入的TypeInfo创建一个TypeImpl结构体,并加入到的哈希表中,哈希表接口由glib提供。
static void type_table_add(TypeImpl *ti)
{
assert(!enumerating_types);
g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}
static TypeImpl *type_register_internal(const TypeInfo *info)
{
TypeImpl *ti;
ti = type_new(info);
type_table_add(ti);
return ti;
}
2.2. 类层次结构
下面讲述QOM是怎样实现类和继承概念的。edu设备及其父类的内容我就不写了,继承关系是这样:
TYPE_PCI_EDU_DEVICE("edu")
->TYPE_PCI_DEVICE("pci-device")
->TYPE_DEVICE("device")
->TYPE_OBJECT("object")
TYPE_OBJECT是所有可实例化类的祖先,这一点和Python、Java等面向对象语言很像。那么它们是如何在QEMU中组织起来的呢?
在类的初始化函数type_initialize中,分配类的存储结构:
ti->class = g_malloc0(ti->class_size);
Object存储结构如下,可以看到属性是用哈希表存放的。
/**
* ObjectClass:
*
* The base for all classes. The only thing that #ObjectClass contains is an
* integer type handle.
*/
struct ObjectClass
{
/*< private >*/
Type type;
GSList *interfaces;
const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];
ObjectUnparent *unparent;
GHashTable *properties;
};
然后对于device类:
typedef struct DeviceClass {
/*< private >*/
ObjectClass parent_class;
/*< public >*/
DECLARE_BITMAP(categories, DEVICE_CATEGORY_MAX);
const char *fw_name;
const char *desc;
Property *props_;
bool user_creatable;
bool hotpluggable;
DeviceReset reset;
DeviceRealize realize;
DeviceUnrealize unrealize;
/* device state */
const VMStateDescription *vmsd;
/* Private to qdev / bus. */
const char *bus_type;
} DeviceClass;
继承链就这样一直往下,再到PCI类。edu类没有显式地定义一个新类,而是表现为一个特殊的PCI类,见如下初始化代码。在初始化该类之前,其父类和接口已经递归地被初始化,就像DFS。

这个函数是在type_initialize中调用的。可以看到需要设置一些父类的属性,用DEVICE_CLASS和PCI_DEVICE_CLASS即可实现从ObjectClass类的转换。这实际上是dynamic cast,转换时有类型检查,最终调用到object_class_dynamic_cast以及type_is_ancestor。检查方式很简单,就是逐级往上查找。
2.3. 构建实例
传入参数-device edu,查看初始化情况。函数调用如下:

在qdev_device_add中,通过如下方式获取设备名,之后创建设备:
driver = qemu_opt_get(opts, "driver");
/* ... */
/* create device */
dev = DEVICE(object_new(driver));
object_new会根据设备名搜索到类信息TypeImpl,再传入object_new_with_type进行解析(Type就是TypeImpl *),且分配实例空间。颇有反射的味道。
一直调用到类信息的构造函数instance_init。
static void edu_instance_init(Object *obj)
{
EduState *edu = EDU(obj);
edu->dma_mask = (1UL << 28) - 1;
object_property_add_uint64_ptr(obj, "dma_mask",
&edu->dma_mask, OBJ_PROP_FLAG_READWRITE);
}
EDU是指针类型转换宏,依然是dynamic cast。EduState是一个显式定义的类,继承链为PCIDevice、DeviceState、ObjectClass。
2.4. 属性
在这一句代码之后,edu设备才可用:
object_property_set_bool(OBJECT(dev), true, "realized", &err);
QOM依然提供了反射机制,用于添加类属性或者成员属性,以及修改其值。查看上面这句代码的调用栈:

从device_set_realized函数的调用可以看出,这是专门为realized属性准备的回调函数,它是BoolProperty的set成员:
typedef struct BoolProperty
{
bool (*get)(Object *, Error **);
void (*set)(Object *, bool, Error **);
} BoolProperty;
而property_set_bool又是专门准备的回调函数,是ObjectProperty的成员。可以想到这些函数句柄的赋值都是在添加属性时完成的。
其实device_set_realized又向下调用PCI的realize函数,然后再调用edu的realize函数。这些都是在Device、PCI、edu类初始化时绑定上去的。很像动态绑定,但因为C语言的灵活性,能够做更多与设备相关的事情,理解透彻了属于是。
realized(具现化)是一种bool属性。除bool外,QOM属性还有child、link等。前者表示设备的从属关系,后者表示设备的引用关系。
学到这里对QOM的作用已经有了一个简单的认识;面向对象的实现细节很多,更具体的内容后面碰到了再说。
3. 线程模型
一个QEMU进程对应一个虚拟机。线程的作用比较多了,首先主线程当然是线程,负责调用glib的事件监听和分发接口;每个VCPU都是一个线程,通过KVM、WHPX、HAXM之类的接口创建;还有VNC线程、I/O线程等。
3.1. QBL
线程之间用QEMU大锁同步,第一节有讲主线程在poll时释放了锁(休眠当然要放锁),dispatch时获取锁。
3.2. WHPX VCPU线程

又是设备具现化,神奇吧?这是WHPX启动VCPU的代码:
static void qemu_whpx_start_vcpu(CPUState *cpu)
{
char thread_name[VCPU_THREAD_NAME_SIZE];
cpu->thread = g_malloc0(sizeof(QemuThread));
cpu->halt_cond = g_malloc0(sizeof(QemuCond));
qemu_cond_init(cpu->halt_cond);
snprintf(thread_name, VCPU_THREAD_NAME_SIZE, "CPU %d/WHPX",
cpu->cpu_index);
qemu_thread_create(cpu->thread, thread_name, qemu_whpx_cpu_thread_fn,
cpu, QEMU_THREAD_JOINABLE);
#ifdef _WIN32
cpu->hThread = qemu_thread_get_handle(cpu->thread);
#endif
}
qemu_whpx_cpu_thread_fn函数是VCPU线程的起点。到whpx_cpu_run函数,可以看到一个非常明显的CPU执行循环:pre->run->post->handle exit->pre->run…
whp_dispatch.WHvRunVirtualProcessor负责CPU执行,它来自WinHvPlatform.dll。
do {
if (cpu->vcpu_dirty) {
whpx_set_registers(cpu, WHPX_SET_RUNTIME_STATE);
cpu->vcpu_dirty = false;
}
whpx_vcpu_pre_run(cpu);
if (atomic_read(&cpu->exit_request)) {
whpx_vcpu_kick(cpu);
}
hr = whp_dispatch.WHvRunVirtualProcessor(
whpx->partition, cpu->cpu_index,
&vcpu->exit_ctx, sizeof(vcpu->exit_ctx));
if (FAILED(hr)) {
error_report("WHPX: Failed to exec a virtual processor,"
" hr=%08lx", hr);
ret = -1;
break;
}
whpx_vcpu_post_run(cpu);
switch (vcpu->exit_ctx.ExitReason) {
case WHvRunVpExitReasonMemoryAccess:
ret = whpx_handle_mmio(cpu, &vcpu->exit_ctx.MemoryAccess);
break;
case ...:
break;
default:
/* error */
break;
}
} while (!ret);
下面是调用栈:

我们可以通过硬件断点来观察这个函数句柄是什么时候初始化的。首先设置launch.json中stopAtEntry为true,然后启动时在VSCode的调试台输入命令:
-exec watch whp_dispatch.WHvRunVirtualProcessor
之后会在断点处停下,可以发现是在configure_accelerators中初始化的。这些WHPX的API在<WinHvPlatform.h>中声明。
VSCode将手动设置的监视点视为Exception,问题不大。

下面学习一个奇妙的宏写法:
LIST_WINHVPLATFORM_FUNCTIONS(WHP_LOAD_FIELD)
这一句就完成了所有WHPX函数句柄的初始化。LIST_WINHVPLATFORM_FUNCTIONS是一个以函数为参数的宏,由一个函数列表组成。不妨先看传入的参数WHP_LOAD_FIELD,它的原型是:
#define WHP_LOAD_FIELD(return_type, function_name, signature)
再看LIST_WINHVPLATFORM_FUNCTIONS列表中的一项:
#define LIST_WINHVPLATFORM_FUNCTIONS(X) \
X(HRESULT, WHvGetCapability, (WHV_CAPABILITY_CODE CapabilityCode, VOID* CapabilityBuffer, UINT32 CapabilityBufferSizeInBytes, UINT32* WrittenSizeInBytes)) \
...
所以简单展开,这第一项就变成:
WHP_LOAD_FIELD(HRESULT, WHvGetCapability, (WHV_CAPABILITY_CODE CapabilityCode, VOID* CapabilityBuffer, UINT32 CapabilityBufferSizeInBytes, UINT32* WrittenSizeInBytes))
恰好变成了又一个函数调用,展开就得到从DLL文件加载函数以及为whp_dispatch的函数句柄赋值的代码。这一黑魔法还被用于WHPDispatch这个工厂类的定义。
struct WHPDispatch {
LIST_WINHVPLATFORM_FUNCTIONS(WHP_DECLARE_MEMBER)
LIST_WINHVEMULATION_FUNCTIONS(WHP_DECLARE_MEMBER)
LIST_WINHVPLATFORM_FUNCTIONS_SUPPLEMENTAL(WHP_DECLARE_MEMBER)
};
extern struct WHPDispatch whp_dispatch;
3.3. I/O线程
I/O线程是专门处理I/O事件的线程,它的作用在于减少对BQL的占据,从而把更多时间留给VCPU。为此QEMU创建了一个新的类型叫TYPE_IOTHREAD,其它需要创建I/O线程的设备只需创建一个指向TYPE_IOTHREAD的link属性即可。
static Property virtio_scsi_properties[] = {
DEFINE_...
DEFINE_PROP_LINK("iothread", VirtIOSCSI, parent_obj.conf.iothread,
TYPE_IOTHREAD, IOThread *),
DEFINE_PROP_END_OF_LIST(),
};
后面的文章会结合实际设备谈I/O线程。