文章系列:
深入理解qemu事件循环 —— 基本框架
深入理解qemu事件循环 ——下半部
文章目录
Glib 事件循环
简述
- Glib事件循环机制提供了一套事件分发接口,使用这套接口注册事件源(source)和对应的回调,可以开发基于事件触发的应用。Glib的核心是poll机制,通过poll检查用户注册的事件源,并执行对应的回调,用户不需要关注其具体实现,只需要按照要求注册对应的事件源和回调
- Glib事件循环机制管理所有注册的事件源,主要类型有:fd,pipe,socket,和 timer。不同事件源可以在一个线程中处理,也可以在不同线程中处理,这取决于事件源所在的上下文( GMainContext)。一个上下文只能运行在一个线程中,所以如果想要事件源在不同线程中并发被处理,可以将其放在不同的上下文
- qemu是事件触发设计架构,实现基础就是glib事件循环,本文主要介绍Glib事件循环的机制,以此为基础实现一个基本的qemu事件循环框架
事件循环状态机
- Glib对一个事件源的处理分为4个阶段:初始化,准备,poll,和调度。如上图的状态机所示。
- Glib状态机的每个阶段都提供了接口供用户注册自己的处理函数。分别如下:
- prepare:
gboolean (*prepare) (GSource *source, gint *timeout_);
Glib初始化完成后会调用此接口,此接口返回TRUE
表示事件源都已准备好,告诉Glib跳过poll直接检查判断是否执行对应回调。返回FALSE
表示需要poll机制监听事件源是否准备好,如果有事件源没准备好,通过参数timeout
指定poll最长的阻塞时间,超时后直接返回,超时接口可以防止一个fd或者其它事件源阻塞整个应用 - query:
gint g_main_context_query (GMainContext *context, gint max_priority, gint *timeout_, GPollFD *fds, gint n_fds);
Glib在prepare完成之后,可以通过query查询一个上下文将要poll的所有事件,这个接口需要用户主动调用 - check:
gboolean (*check) (GSource *source);
Glib在poll返回后会调用此接口,用户通过注册此接口判断哪些事件源需要被处理,此接口返回TRUE
表示对应事件源的回调函数需要被执行,返回FALSE
表示不需要被执行 - dispatch:
gboolean (*dispatch) (GSource *source, GSourceFunc callback, gpointer user_data);
Glib根据check的结果调用此接口,参数callback
和user_data
是用户通过g_source_set_callback
注册的事件源回调和对应的参数,用户可以在dispatch中选择直接执行callback,也可以不执行
demo
定制事件源,回调函数
#include <glib.h>
/*
函数打印标准输入中读到内容的长度
*/
gboolean io_watch(GIOChannel *channel,
GIOCondition condition,
gpointer data)
{
gsize len = 0;
gchar *buffer = NULL;
g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);
if(len > 0)
g_print("%d\n", len);
g_free(buffer);
return TRUE;
}
int main(int argc, char* argv[])
{
GMainLoop *loop = g_main_loop_new(NULL, FALSE); // 获取一个上下文的事件循环实例,context为NULL则获取默认的上下文循环
GIOChannel* channel = g_io_channel_unix_new(1); // 将标准输入描述符转化成GIOChannel,方便操作
if(channel) {
g_io_add_watch(channel, G_IO_IN, io_watch, NULL);
// 将针对channel事件源的回调注册到默认上下文,告诉Glib自己对channel的输入(G_IO_IN)感兴趣
// 当输入准备好之后,调用自己注册的回调io_watch,并传入参数NULL。
g_io_channel_unref(channel);
}
g_main_loop_run(loop); // 执行默认上下文的事件循环
g_main_context_unref(g_main_loop_get_context(loop));
g_main_loop_unref(loop);
return 0;
}
运行效果:
定制事件源,回调函数和状态机回调函数
#include <glib.h>
typedef struct _MySource MySource;
/* 自定义事件源,继承自Glib的GSource类型*/
struct _MySource
{
GSource _source; // 基类
GIOChannel *channel;
GPollFD fd;
};
/*事件源回调函数,读出iochannel中的内容,打印其长度*/
static gboolean watch(GIOChannel *channel)
{
gsize len = 0;
gchar *buffer = NULL;
g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);
if(len > 0)
g_print("%d\n", len);
g_free(buffer);
return TRUE;
}
/*
状态机prepare回调函数,timeout等于-1告诉poll如果IO没有准备好,一直等待,即阻塞IO
返回FALSE指示需要poll来检查事件源是否准备好,如果是TRUE表示跳过poll
*/
static gboolean prepare(GSource *source, gint *timeout)
{
*timeout = -1;
return FALSE;
}
/*
状态机check回调函数,检查自己感兴趣的fd状态(events)是否准备好
用户通过设置events标志设置感兴趣的fd状态(包括文件可读,可写,异常等)
revents是poll的返回值,由内核设置,表明fd哪些状态是准备好的
函数功能:
当感兴趣的状态和poll返回的状态不相同,表示fd没有准备好,返回FALSE,Glib不发起调度
反之返回TRUE,Glib发起调度
*/
static gboolean check(GSource *source)
{
MySource *mysource = (MySource *)source;
if(mysource->fd.revents != mysource->fd.events)
return FALSE;
return TRUE;
}
/*
状态机dispatch回调函数,prepare和check其中只要由一个返回TRUE,Glib就会直接调用此接口
函数逻辑是执行用户注册的回调函数
*/
static gboolean dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
{
MySource *mysource = (MySource *)source;
if(callback)
callback(mysource->channel);
return TRUE;
}
/*
当事件源不再被引用时,这个接口被回调
*/
static void finalize(GSource *source)
{
MySource *mysource = (MySource *)source;
if(mysource->channel)
g_io_channel_unref(mysource->channel);
}
int main(int argc, char* argv[])
{
GError *error = NULL;
GMainLoop *loop = g_main_loop_new(NULL, FALSE); // 从默认上下文获取事件循环实例
GSourceFuncs funcs = {prepare, check, dispatch, finalize}; // 声明用户定义的状态机回调
/*
Glib允许用户自己定义事件源,但需要把Glib的事件源作为"基类",具体实现是把GSource
作为自定义事件源的第一个成员,在创建事件源时传入状态机回调函数和自定义事件源的结构体大小
*/
GSource *source = g_source_new(&funcs, sizeof(MySource));
MySource *mysource = (MySource *)source;
/*
创建一个文件类型的GIOChannel,GIOChannel就是Glib对文件描述符的封装,实现其平台可移植性
GIOChannel在所有Unix平台上都可移植,在Windows平台上部分可移植
GIOChannel的fd类型可以是文件,pipe和socket
*/
if (!(mysource->channel = g_io_channel_new_file("test", "r", &error))) {
if (error != NULL)
g_print("Unable to get test file channel: %s\n", error->message);
return -1;
}
/*获取GIOChannel的fd,放到GPollFD的fd域中*/
mysource->fd.fd = g_io_channel_unix_get_fd(mysource->channel);
/*设置感兴趣的文件状态,这里时文件可读状态*/
mysource->fd.events = G_IO_IN;
/*
传给poll的文件描述符结构体
struct GPollFD {
gint fd; // 文件描述符
gushort events; // 感兴趣的文件状态
gushort revents; // 返回值,由内核设置
};
*/
g_source_add_poll(source, &mysource->fd); // 将文件描述符添加到事件源中
g_source_set_callback(source, (GSourceFunc)watch, NULL, NULL); // 设置事件源的回调函数
/*
设置事件源优先级,如果多个事件源在同一个上下文,这个事件源都准备好了,优先级高的事件源会被Glib优先调度
*/
g_source_set_priority(source, G_PRIORITY_DEFAULT_IDLE);
g_source_attach(source, NULL); //将事件源添加到Glib的上下文,此处上下文为NULL,表示默认的上下文
g_source_unref(source);
g_main_loop_run(loop); // Glib开始执行默认上下文的事件循环
g_main_context_unref(g_main_loop_get_context(loop));
g_main_loop_unref(loop);
return 0;
}
运行效果:
QEMU事件循环
事件循环初始化
- qemu事件循环初始化遵循Glib接口,和普通应用初始化流程类似,在
qemu_init_main_loop
中实现,简化版代码如下:
static void
qemu_init_main_loop()
{
GSource *src;
qemu_aio_context = aio_context_new(); // 创建qemu定制的事件源qemu_aio_context
gpollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD));
src = aio_get_g_source(qemu_aio_context); // 从定制事件源中获取Glib原始的事件源
g_source_set_name(src, "aio-context"); // 设置事件源名称
g_source_attach(src, NULL); // 将事件源添加到Glib默认事件循环上下文
g_source_unref(src);
src = iohandler_get_g_source(); // 创建另一个定制的事件源 iohandler_ctx
g_source_set_name(src, "io-handler");
g_source_attach(src, NULL); // 将事件源添加到Glib默认事件循环上下文
g_source_unref(src);
}
从上面代码可知,qemu事件初始化创建了两个事件源,分别是qemu_aio_context
和iohandler_ctx
,这两个事件源被加在了Glib默认的事件循环上下文default GMainContext
,我们知道一个上下文只在一个线程中运行,这两个事件源同属一个上下文,因此可以确认运行在同一个线程中。
状态机回调函数的定制
- qemu丰富了Glib的事件源,状态机回调的实现逻辑变的复杂,但基本框架还是遵循Glib的接口。
aio_context_new
函数中的aio_source_funcs
就是状态机回调函数的声明
static GSourceFuncs aio_source_funcs = {
aio_ctx_prepare,
aio_ctx_check,
aio_ctx_dispatch,
aio_ctx_finalize
};
事件源的定制
- qemu定制的事件源,不仅实现了对描述符的监听,还实现了事件通知,时钟事件源监听和下半部。这些实现都体现在了描述事件源的结构体
AioContext
中。描述符的监听由Glib的GSource
实现,notifier
实现了事件通知,tlg
实现了时钟事件源监听,first_bh
实现了下半部等,简化版本的qemu事件源结构体如下:
struct AioContext {
GSource source;
struct QEMUBH *first_bh;
EventNotifier notifier;
QEMUTimerListGroup tlg;
}
- qemu主线程在
qemu_init_main_loop
函数里创建了运行在默认上下文的事件源qemu_aio_context
,这个事件源,包含了qemu中绝大部分服务的fd,比如VNC server和QMP monitor服务端的socket等,事件源创建函数如下:
AioContext *aio_context_new(Error **errp)
{
int ret;
AioContext *ctx;
ctx = (AioContext *) g_source_new(&aio_source_funcs, sizeof(AioContext)); // 事件源的创建,传入的size是AioContext的大小
aio_context_setup(ctx);
/*初始化事件源的其它部分*/
ret = event_notifier_init(&ctx->notifier, false); // 初始化实现事件通知的成员notifier
if (ret < 0) {
error_setg_errno(errp, -ret, "Failed to initialize event notifier");
goto fail;
}
......
}
poll机制的定制
- 应用程序对文件描述符的读写,要么设置为阻塞,如果fd的某个状态(比如可读,可写)没有准备好,进程将永远阻塞在这个IO上;要么设置为非阻塞IO(open时设置O_NONBLOCK),进程IO立即执行,如果fd没有准备好,直接返回错误,表示如果继续执行IO将阻塞。通常,这两种方式满足了大多数应用,但在更复杂的场景,不允许永远阻塞的进程可能需要读写一个可能永远阻塞进程的fd,这时可以使用内核提供的poll接口,它不直接读写IO,而是先探测fd的IO状态是否准备好,这个探测可以设置超时时间,如果这段时间内IO状态准备好了,poll接口立即返回,指示应用程序可以进行接下来的读写了,如果poll超过了设置的超时时间,但应用感兴趣的IO状态没有准备好,也返回,同时把那些准备好的IO状态返回给应用,应用程序可以根据内核返回的poll信息确定接下要做的事情,两种情况,都不会使应用程序永远阻塞在IO上,防止进程出现D状态
- qemu基于Glib事件循环的框架,定制了Glib事件循环的几乎所有接口。最重要一点,qemu没有调用Glib的event loop接口
g_main_loop_run
执行事件循环,而是直接调用了Glib提供的g_poll函数实现poll的功能,g_poll函数具有可移植性,在有poll接口的平台上由poll模拟,在没有poll接口的平台上由select模拟。当用户不想执行整个Glib的事件循环,又想实现阻塞一段时间的高级IO,就可以直接使用g_poll。 - qemu代码中可以看到它没有调用
g_main_loop_run
执行Glib事件循环,而是显式调用了while循环,再直接调用g_poll接口。这个高级定制使得qemu对Glib事件循环的使用看起来和普通应用程序有很大区别,至少,普通应用程序使用Glib时不会显式执行while循环,只需要简单地执行g_main_loop_run
就可以了,g_poll传入的是一个fd的数组和数组元素,因此它能够同时poll多个fd,poll的时候是休眠的,一旦其中一个fd准备好,poll就会被唤醒,同时GPollFD.revent会被内核设置,表明fd的状态。
- qemu主线程的while循环
while (!main_loop_should_exit()) {
main_loop_wait(false);
}
- while循环中对g_poll的显式调用
/*
如果qemu配置了PPOLL,使用PPOLL实现poll探测,否则使用g_poll实现poll探测
*/
int qemu_poll_ns(GPollFD *fds, guint nfds, int64_t timeout)
{
#ifdef CONFIG_PPOLL
if (timeout < 0) { // 如果timeout小于0,如果IO没有准备好,永远阻塞
return ppoll((struct pollfd *)fds, nfds, NULL, NULL);
} else { // 否则,阻塞一段时间后返回
struct timespec ts;
int64_t tvsec = timeout / 1000000000LL;
/* Avoid possibly overflowing and specifying a negative number of
* seconds, which would turn a very long timeout into a busy-wait.
*/
if (tvsec > (int64_t)INT32_MAX) {
tvsec = INT32_MAX;
}
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的事件源回调,抛弃了Glib提供的接口,而是通过事件源中自定义的
aio_handlers
部分实现的,aio_handlers
是个链表头,指向一个链表,这个链表的每个节点是个AioHandler
结构体,维护了一个事件源的fd和对应的读写回调函数,如图
简化版本的AioHandler
定义如下:
struct AioHandler {
GPollFD pfd; // 要poll的一个fd
IOHandler *io_read; // fd可读时对应的回调
IOHandler *io_write;// fd可写时对应的回调
void *opaque; // 调用回调函数时传入的参数
QLIST_ENTRY(AioHandler) node; // 链接到aio_handlers指向的链表
};
- Glib的poll完成后需要执行check,检查哪些事件源的fd准备好,只要aio_handlers指向的链表中,有一个fd准备好了,check就返回true,表示需要发起接下来的调度,简化代码如下:
- qemu定制的Glib check回调
static gboolean
aio_ctx_check(GSource *source)
{
AioContext *ctx = (AioContext *) source;
return aio_pending(ctx);
}
- 检查链表中是否有准备好的fd,如果有,返回TRUE
static gboolean
aio_pending(AioContext *ctx)
{
AioHandler *node;
gboolean result = FALSE;
QLIST_FOREACH(node, &ctx->aio_handlers, node) {
int revents;
revents = node->pfd.revents & node->pfd.events;
if (revents & (G_IO_IN | G_IO_HUP | G_IO_ERR) && node->io_read) {
result = TRUE; // qemu关注的读状态已准备好
break;
}
if (revents & (G_IO_OUT | G_IO_ERR) && node->io_write) {
result = TRUE; // qemu关注的写状态已准备好
break;
}
}
return result;
}
- Glib中如果check到有fd需要调度,直接执行qemu定值的dispatch接口,简化的dispatch接口如下:
- qemu定制的Glib dispatch回调
static void
aio_dispatch(AioContext *ctx)
{
aio_dispatch_handlers(ctx);
}
- 检查链表中哪些node需要执行dispatch,调用相应的读写回调函数
static gboolean
aio_dispatch_handlers(AioContext *ctx)
{
AioHandler *node, *tmp;
QLIST_FOREACH(node, &ctx->aio_handlers, node) {
int revents;
revents = node->pfd.revents & node->pfd.events;
node->pfd.revents = 0;
if ((revents & (G_IO_IN | G_IO_HUP | G_IO_ERR)) &&
node->io_read) {
node->io_read(node->opaque);
}
if ((revents & (G_IO_OUT | G_IO_ERR)) &&
node->io_write) {
node->io_write(node->opaque);
}
}
return TRUE;
}
简化版本代码
- 简化版本的代码基于qemu-3.1.91,实现了对一个test普通文件的监听,当test文件fd的输入状态准备好时,demo检查其是否有输入,如果有输入,读出其内容,打印内容和内容的长度
static void
fd_read_cb(void *opaque)
{
GIOChannel *channel = opaque;
gsize len = 0;
gchar *buffer = NULL;
g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);
if(len > 0) {
g_print("len: %d, buffer: %s\n", len, buffer);
}
g_free(buffer);
}
代码地址:qemu_main_loop
- demo运行效果: