QEMU事件循环机制
关注微信公众号:Linux内核拾遗
QEMU程序的运行是基于各类文件fd事件的,QEMU在运行过程中会将自己感兴趣的文件fd添加到其监听列表上并定义相应的处理函数,在其主线程中,有一个循环用来处理这些文件fd的事件,如来自用户的输入、来自VNC的连接、虚拟网卡对应tap设备的收包等。QEMU的事件循环机制基于glib,glib是一个跨平台的、用C语言编写的若干底层库的集合。
1 glib事件循环机制
glib实现了完整的事件循环分发机制,在这个机制中有一个主循环负责处理各种事件,事件通过事件源描述,事件源包括各种文件描述符(文件、管道或者socket)、超时和idle事件等,每种事件源都有一个优先级,idle事件源在没有其他高优先级的事件源时会被调度运行。
glib使用GMainLoop结构体来表示一个事件循环,每一个GMainLoop都对应有一个主上下文GMainContext。事件源使用GSource表示,每个GSource可以关联多个文件描述符,每个GSource会关联到一个GMainContext,一个GMainContext可以关联多个GSource。
glib的一个重要特点是能够定义新的事件源类型,可以通过定义一组回调函数来将新的事件源添加到glib的事件循环框架中。因此应用程序可以利用glib的这套机制来实现自己的事件监听与分发处理。
glib主上下文的一次循环包括prepare、query、check、dispatch四个过程,分别对应glib的g_main_context_prepare()、g_main_context_query()、g_main_context_check()以及g_main_context_dispatch()四个函数,其状态转换如下图所示。
- prepare:通过g_main_context_prepare()会调用事件对应的prepare回调函数,做一些准备工作,如果事件已经准备好进行监听了,返回true。
- query:通过g_main_context_query()可以获得实际需要调用poll的文件fd。
- check:当query之后获得了需要进行监听的fd,那么会调用poll对fd进行监听,当poll返回的时候,就会调用g_main_context_check()将poll的结果传递给主循环,如果fd事件能够被分派就会返回true。
- dispatch:通过g_main_context_dispatch()可以调用事件源对应事件的处理函数。
根据这个glib事件循环机制的处理流程,应用程序需要做的就是把新的事件源加入到这个处理流程中,glib会负责处理事件源上注册的各种事件。
2 QEMU中的事件循环机制
QEMU的事件循环机制如下图所示,QEMU在运行过程中会注册一些感兴趣的事件,设置其对应的处理函数。图示的QEMU主循环中添加了了来自tap设备、qmp以及VNC等的事件源:
- 当监听到VNC有连接到来时glib框架就会调用vnc_client_io函数来处理。
- 当网卡设备的后端tap设备接收到网络包后QEMU调用tap_send将包路由到虚拟机网卡前端。
- 当用户发送qmp命令之后glib会调用tcp_chr_accept来处理qmp命令。
通过如下命令启动虚拟机,并结合该虚拟机来介绍QEMU中的事件循环机制:
root@ubuntu:~# qemu-system-x86_64 -m 1024 -smp 4 -hda /home/test/test.img --enable-kvm -vnc :0
该命令行启动的QEMU程序,它包含了如图所示的3种/5个事件源:
- AioContext自定义事件源:qemu_aio_context和iohander_ctx,前者用于处理QEMU中块设备相关的异步I/O请求通知,后者用于处理QEMU中各类事件通知,包括信号处理fd、tap设备的fd以及VFIO设备对应的中断通知等等。
- glib标准事件源:vnc: GSource这两个VNC事件。
- glib内部事件fd。
glib中事件源可以添加多个事件fd,每一个事件源本身都会有一个fd,当添加一个fd到事件源时,整个glib主循环都会监听该fd,并且任何一个fd准备好事件之后都可以唤醒主循环。当前例子中,QEMU主循环总共会监听6个fd,其中5个是事件源本身的fd,还有一个是通过系统调用SYS_signalfd创建的用来处理信号的fd。
QEMU主循环对应的最重要的几个函数如下图所示。QEMU的main函数定义在vl.c中,在进行好所有的初始化工作之后会调用函数main_loop来开始主循环。
main_loop及其调用的main_loop_wait的主要代码如下:
// vl.c
static void main_loop(void) {
...
do {
...
last_io = main_loop_wait(nonblocking);
...
} while (!main_loop_should_exit());
}
// main-loop.c
void main_loop_wait(int nonblocking) {
...
// 计算最小timeout值
timeout_ns = qemu_soonest_timeout(timeout_ns,
timerlistgroup_deadline_ns(
&main_loop_tlg));
ret = os_host_main_loop_wait(timeout_ns);
...
}
static int os_host_main_loop_wait(int64_t timeout) {
int ret;
static int spin_counter;
// 主循环函数1
glib_poolfds_fill(&timeout);
...
if (timeout) {
spin_counter = 0;
qemu_mutex_unlock_iothread();
} else {
spin_counter++;
}
// 主循环函数2
ret = qemu_poll_ns((GPollFD *)gpollfds->data, gpollfds->len, timeout);
if (timeout) {
qemu_mutex_lock_iothread();
}
// 主循环函数3
glib_pollfds_poll();
return ret;
}
main_loop_wait在调用os_host_main_loop_wait前,会调用qemu_soonest_timeout函数先计算一个最小的timeout值,该值是从定时器列表中获取的,表示监听事件的时候最多让主循环阻塞的时间,timeout使得QEMU能够及时处理系统中的定时器到期事件。
2.1 glib_pollfds_fill
该函数的主要工作是获取所有需要进行监听的fd,并且计算一个最小的超时时间。
// main-loop.c
static void glib_pollfds_fill(int64_t *cur_timeout) {
// 1. 调用准备函数
g_main_context_prepare(context, &max_priority);
n = glib_n_poll_fds;
do {
GPollFD* pfds;
glib_n_poll_fds = n;
g_array_set_size(gpollfds, glib_pollfds_idx + glib_n_poll_fds);
pfds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx);
// 2. 获取需要监听的fd,并返回fd时间最小的timeout
n = g_main_context_query(context, max_priority, &timeout, pfds,
glib_n_poll_fds);
} while (n != glib_n_poll_fds);
...
*cur_timeout = qemu_soonest_timeout(timeout_ns, *cur_timeout);
}
- 首先调用g_main_context_prepare开始为主循环的监听做准备。
- 接着在一个循环中调用g_main_context_query获取需要监听的fd,所有fd保存在全局变量gpollfds数组中,需要监听的fd的数量保存在glib_n_poll_fds中,g_main_context_query还会返回fd时间最小的timeout,该值用来与传过来的cur_timeout(定时器的timeout)进行比较,选取较小的一个,表示主循环最大阻塞的时间。
2.2 qemu_poll_ns
glib_pollfds_fill调用完成后,此时已经有了所有需要监听的fd了,然后会调用qemu_mutex_unlock_iothread释放QEMU大锁(Big Qemu Lock,BQL)。
接着os_host_main_loop_wait函数会调用qemu_poll_ns,它接受3个参数:
- 要监听的fd数组。
- fds数组的长度。
- timeout值,表示g_poll最多阻塞的时间,这是一个跨平台的poll函数,用来监听文件上发生的事件。如果QEMU配置了CONFIG_PPOLL,那么就qemu_poll_ns会调用ppoll而不是g_poll。
// qemu-timer.c
int qemu_poll_ns(GPollFD* fds, guint nfds, int64_t timeout) {
#ifdef CONFIG_POLL
if (timeout < 0) {
return ppoll((struct pollfd*)fds, nfds, NULL, NULL);
} else {
struct timespec ts;
int64_t tvsec = timeout / 1000000000LL;
...
ts.tv_sec = tvsec;
ts.tv_nsec = timeout % 1000000000LL;
return ppoll((struct pollfd *)fds, nfds, &ts, NULL);
}
#else
return g_poll(fds, nfds, qemu_timeout_ns_to_ms(timeout));
#endif
}
qemu_poll_ns的调用会阻塞主线程,当该函数返回之后,要么表示有文件fd上发生了事件,要么表示一个超时,不管怎么样,这都将进入第三步,即调用glib_pollfds_poll函数。
2.3 glib_pollfds_poll
glib_pollfds_poll函数负责对事件进行分发处理。
// main-lool.c
static void glib_pollfds_poll(void) {
GMainContext* context = g_main_context_default();
GPollFD* fds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx);
// 检测事件并分发事件
if (g_main_context_check(context, max_priority, pfds, glib_n_poll_fds)) {
g_main_context_dispatch(context);
}
}
它首先调用了glib框架的g_main_context_check检测事件,然后调用g_main_context_dispatch进行事件的分发。
3 QEMU自定义事件源
QEMU自定义了一个新的事件源AioContext,有两种类型的AioContext:
- 第一类用来监听各种各样的事件,比如iohandler_ctx。
- 第二类是用来处理块设备层的异步I/O请求,比如QEMU默认的qemu_aio_context或者模块自己创建的AioContext。
这里只关注第一种情况,即事件相关的AioContext。
AioContext结构体定义如下:
// include/block/aio.h
struct AioContext {
GSource source;
/* Used by AioContext users to protect from multi-threaded access. */
QemuRecMutex lock;
/* The list of registered AIO handlers */
QLIST_HEAD(, AioHandler) aio_handlers;
...
uint32_t notify_me;
/* lock to protect between bh's adders and deleter */
QemuLockCnt bh_lock;
/* Anchor of the list of Bottom Halves belonging to the context */
...
bool notified;
EventNotifier notifier;
...
/* TimerLists for calling timers - one per clock type */
QEMUTimerListGroup tlg;
...
};
- source:glib中的GSource,每一个自定义的事件源第一个成员都是GSource结构的成员。
- lock:QEMU中的互斥锁,用来保护多线程情况下对AioContext中成员的访问。
- aio_handlers:一个链表头,其链表中的数据类型为AioHandler,所有加入到AioContext事件源的文件fd的事件处理函数都挂到这个链表上。
- notify_me和notified都与aio_notify相关,主要用于在块设备层的I/O同步时处理QEMU下半部(Bottom Halvs,BH)。
- first_bh:QEMU下半部链表,用来连接挂到该事件源的下半部,QEMU的BH默认挂在qemu_aio_context下。
- notifier:事件通知对象,类型为EventNotifier,在块设备进行同步且需要调用BH的时候需要用到该成员。
- tlg:管理挂到该事件源的定时器。
剩下的结构与块设备层的I/O同步相关,此处略过。
AioContext拓展了glib中source的功能,不但支持fd的事件处理,还模拟内核中的下半部机制,实现了QEMU中的下半部以及定时器的管理。
接下来介绍AioContext的相关接口,这里只以文件fd的事件处理为主。
3.1 aio_context_new
aio_context_new用于创建一个AioContext:
// async.c
AioContext *aio_context_new(Error **errp) {
int ret;
AioContext *ctx;
ctx = (AioContext *) g_source_new(&aio_source_funcs, sizeof(AioContext));
aio_context_setup(ctx);
ret = event_notifier_init(&ctx->notifier, false);
...
g_source_set_can_recurse(&ctx->source, true);
aio_set_event_notifier(ctx, &ctx->notifier,
false,
(EventNotifierHandler *)
event_notifier_dummy_cb);
...
timerlistgroup_init(&ctx->tlg, aio_timerlist_notifer, ctx);
return ctx;
}
- aio_context_new函数首先创建分配了一个AioContext结构ctx。
- 然后初始化代表该事件源的事件通知对象ctx->notifier。
- 接着调用了aio_set_event_notifier用来设置ctx->notifier对应的事件通知函数。
- 最后初始化ctx中其他的成员。
3.2 aio_set_fd_hander
AioContext的创建函数中,aio_set_event_notifer函数调用了aio_set_fd_handler函数,后者用于添加或者删除AioContext事件源中的一个fd,如果是添加则会设置fd对应的读写函数。添加事件源中fd监听处理的步骤如下:
// aio-posix.c
void aio_set_fd_handler(AioContext *ctx,
int fd,
bool is_external,
IOHandler* io_read,
IOHandler* io_write,
void* opaque) {
AioHandler* node;
bool is_new = false;
bool is_deleted = false;
node = find_aio_handler(ctx, fd);
/* Are we deleting the fd handler? */
if (!io_read && !io_write) {
...
} else {
if (node == NULL) {
/* Alloc and insert if it's not already there */
node = g_new0(AioHandler, 1);
node->pfd.fd = fd;
QLIST_INSERT_HEAD(&ctx->aio_handlers, node, node);
g_source_add_poll(&ctx->source, &node->pfd);
is_new = true;
}
/* Update handler with latest information */
node->io_read = io_read;
node->io_write = io_write;
node->opaque = opaque;
node->is_external = is_external;
node->pfd.events = (io_read ? G_IO_IN | G_IO_HUP | G_IO_ERR : 0);
node->pfd.events |= (io_write ? G_IO_OUT | G_IO_ERR : 0);
}
aio_epoll_update(ctx, node, is_new);
aio_notify(ctx);
if (deleted) {
g_free(node);
}
}
该函数的参数说明如下:
- 第一个参数ctx表示需要添加fd到哪个AioContext事件源。
- 第二个参数fd表示添加的fd是需要在主循环中进行监听的。
- 第三个参数is_external用于块设备层,对于事件监听的fd都设置为false。
- 剩余参数中,io_read和io_write都是对应fd的回调函数,opaque会作为参数调用这些回调函数。
该函数的主要流程如下:
- aio_set_fd_handler函数首先调用find_aio_handler查找当前事件源ctx中是否已经有了fd。
- 考虑新加入的情况,这里会创建一个名为node的AioHandler,使用fd初始化node->pfd.fd,并将其插入到ctx->aio_handlers链表上,调用glib接口g_source_add_poll将该fd插入到了事件源监听fd列表中。
- 设置node事件读写函数为io_read,io_write函数,根据io_read和io_write的有无设置node->pfd.events,也就是要监听的事件。
aio_set_fd_handler调用之后,新的fd事件就加入到了事件源的aio_handlers链表上了,如下图所示:
aio_set_fd_handler函数一般被块设备相关的操作直接调用,如果仅仅是添加一个普通的事件相关的fd到事件源,通常会调用其封装函数qemu_set_fd_handler,该函数将事件fd添加到全部变量iohandler_ctx事件源中。
3.3 aio_ctx_dispatch
glib中自定义的事件源需要实现glib循环过程中调用的几个回调函数,QEMU中为AioContext事件源定义了名为aio_source_funcs的GSourceFuns结构,这几个函数都是自定义事件源需要实现的:
// async.c
static GSourceFuncs aio_source_funcs = {
aio_ctx_prepare,
aio_ctx_check,
aio_ctx_dispatch,
aio_ctx_finalize
};
这里介绍一下最重要的事件处理分派函数aio_ctx_dispatch,该函数会调用aio_dispatch,后者要完成3件事:
- 第一是BH的处理。
- 第二是处理文件fd列表中有事件的fd。
- 第三是调用定时器到期的函数。
其中第二步的代码逻辑如下:
// aio-posix.c
bool aio_dispatch(AioContext* ctx) {
AioHandler* node;
bool progress = false;
...
node = QLIST_FIRST(&ctx->aio_handelrs);
while (node) {
AioHandler* tmp;
int revents;
ctx->walking_handlers++;
revents = node->pfd.revents & node->pfd.events;
node->pfd.revents = 0;
if (!node->deleted &&
(revents & (G_IO_IN | G_IO_HUP | G_IO_ERR)) &&
aio_node_check(ctx, node->is_external) &&
node->io_read) {
node->io_read(node->opaque);
/* aio_notify() does not count as progress */
if (node->opaque != &ctx->notifier) {
progress = true;
}
}
if (!node->deleted &&
(revents & (G_IO_OUT | G_IO_ERR)) &&
aio_node_check(ctx, node->is_external) &&
node->io_write) {
node->io_write(node->opaque);
progress = true;
}
tmp = node;
node = QLIST_NEXT(node, node);
ctx->walking_handlers--;
if (!ctx->walking_handlers && tmp->deleted) {
QLIST_REMOVE(tmp, node);
g_free(tmp);
}
}
/* Run our timers */
progress |= timerlistgroup_run_timers(&ctx->tlg);
return progress;
}
aio_dispatch_handlers函数会遍历aio_handlers,遍历监听fd上的事件是否发生了。
- fd发生的事件存在node->pfd.revents中,注册时指定需要接受的事件存放在node->pfd.events中,revents变量保存了fd接收到的事件。
- 对应G_IO_IN可读事件来说,会调用注册的fd的io_read回调,对G_IN_OUT可写事件来说,会调用注册的fd的io_write函数。
- 如果当前的fd已经删除了,则会删除这个节点。
4 QEMU事件处理过程
以signalfd的处理为例介绍QEMU事件处理的过程。signalfd是Linux的一个系统调用,可以将特定的信号与一个fd绑定起来,当有信号到达的时候fd就会产生对应的可读事件。
4.1 signal事件源的初始化
vl.c中的main函数会调用qemu_init_main_loop进行AioContext事件源的初始化,如下:
// main-loop.c
int qume_init_main_loop(Error **errp) {
int ret;
GSource* src;
Error* local_error = NULL;
init_clocks();
ret = qemu_signal_init();
...
qemu_aio_context = aio_context_new(&local_error);
...
qemu_notify_bh = qemu_bh_new(notify_event_cb, NULL);
gpollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD));
src = aio_get_g_source(qemu_aio_context);
g_source_set_name(src, "aio-context");
g_source_attach(src, NULL);
g_source_unref(src);
src = iohandler_get_g_source();
g_source_set_name(src, "io-handler");
g_source_attach(src, NULL);
g_source_unref(src);
return 0;
}
qemu_init_main_loop函数的主要逻辑如下:
-
首先调用qemu_signal_init将一个fd与一组信号关联起来,qemu_signal_init调用qemu_set_fd_handler函数将该signalfd对应的可读回调函数设置为sigfd_handler。
- qemu_set_fd_handler在首次调用时会调用iohandler_init创建一个全局的iohandler_ctx事件源,这个事件源的作用是监听QEMU中的各类事件。
- 最终qemu_signal_init会在iohandlers_ctx的aio_handlers上挂一个AioHandler节点,其fd为这里的signalfd,其io_read函数为这里的sigfd_handler。
-
接着调用aio_context_new创建一个全局的qemu_aio_context事件源,这个事件源主要用于处理BH和块设备层的同步使用。
-
最后调用aio_get_g_source和iohandler_get_g_source分别获取qemu_aio_context和iohandler_ctx的GSource,以GSource为参数调用g_source_attach两个AioContext加入到glib的主循环中去。
4.2 signal事件处理主循环
将信号对应的fd加入事件源以及将事件源加入到glib的主循环之后,QEMU就会进入一个while循环中进行事件监听。
当使用kill向QEMU进程发送SIGALARM信号时,signalfd就会有可读信号,从而导致glib的主循环返回调用g_main_context_dispatch进行事件分发,这会调用到aio_ctx_dispatch,最终会调用到qemu_signal_init注册的可读处理函数sigfd_handler。
参考文献
[1] QEMU/KVM源码解析与应用 - 李强
关注微信公众号:Linux内核拾遗