深入理解qemu事件循环 —— 基本框架

文章系列:
深入理解qemu事件循环 —— 基本框架
深入理解qemu事件循环 ——下半部

Glib 事件循环

简述

  • Glib事件循环机制提供了一套事件分发接口,使用这套接口注册事件源(source)和对应的回调,可以开发基于事件触发的应用。Glib的核心是poll机制,通过poll检查用户注册的事件源,并执行对应的回调,用户不需要关注其具体实现,只需要按照要求注册对应的事件源和回调
  • Glib事件循环机制管理所有注册的事件源,主要类型有:fd,pipe,socket,和 timer。不同事件源可以在一个线程中处理,也可以在不同线程中处理,这取决于事件源所在的上下文( GMainContext)。一个上下文只能运行在一个线程中,所以如果想要事件源在不同线程中并发被处理,可以将其放在不同的上下文
  • qemu是事件触发设计架构,实现基础就是glib事件循环,本文主要介绍Glib事件循环的机制,以此为基础实现一个基本的qemu事件循环框架

事件循环状态机

在这里插入图片描述

  • Glib对一个事件源的处理分为4个阶段:初始化,准备,poll,和调度。如上图的状态机所示。
  • Glib状态机的每个阶段都提供了接口供用户注册自己的处理函数。分别如下:
  1. prepare: gboolean (*prepare) (GSource *source, gint *timeout_);
    Glib初始化完成后会调用此接口,此接口返回TRUE表示事件源都已准备好,告诉Glib跳过poll直接检查判断是否执行对应回调。返回FALSE表示需要poll机制监听事件源是否准备好,如果有事件源没准备好,通过参数timeout指定poll最长的阻塞时间,超时后直接返回,超时接口可以防止一个fd或者其它事件源阻塞整个应用
  2. query:gint g_main_context_query (GMainContext *context, gint max_priority, gint *timeout_, GPollFD *fds, gint n_fds);
    Glib在prepare完成之后,可以通过query查询一个上下文将要poll的所有事件,这个接口需要用户主动调用
  3. check:gboolean (*check) (GSource *source);
    Glib在poll返回后会调用此接口,用户通过注册此接口判断哪些事件源需要被处理,此接口返回TRUE表示对应事件源的回调函数需要被执行,返回FALSE表示不需要被执行
  4. dispatch:gboolean (*dispatch) (GSource *source, GSourceFunc callback, gpointer user_data);
    Glib根据check的结果调用此接口,参数callbackuser_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_contextiohandler_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的状态。
  1. qemu主线程的while循环
    while (!main_loop_should_exit()) {
    	main_loop_wait(false);
    }
  1. 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,表示需要发起接下来的调度,简化代码如下:
  1. qemu定制的Glib check回调
static gboolean
aio_ctx_check(GSource *source)
{
    AioContext *ctx = (AioContext *) source;
    return aio_pending(ctx);
}
  1. 检查链表中是否有准备好的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接口如下:
  1. qemu定制的Glib dispatch回调
static void
aio_dispatch(AioContext *ctx) 
{
    aio_dispatch_handlers(ctx);
}
  1. 检查链表中哪些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运行效果:
    在这里插入图片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章