QEMU的事件循环-01 glib事件循环机制

89次阅读
没有评论

共计 10719 个字符,预计需要花费 27 分钟才能阅读完成。

01 glib事件循环机制

Linux系统中“一切皆文件”,包括具体文件、设备、网络、Socket等都被抽象为文件。Linux通过fd来访问一个文件,应用程序也可以调用select、poll、epoll系统调用来监听文件的变化。QEMU程序的运行即是基于各类文件fd事件的,QEMU在运行过程中会将自己感兴趣的文件fd添加到其监听列表上并定义相应的处理函数,在其主线程中,有一个循环用来处理这些文件fd的事件,如来自用户的输入、来自VNC的连接、虚拟网卡对应tap设备的收包等。QEMU程序的事件机制是基于glib的,我们需要了解一下glib。

glib是什么?

GLib是一种开源的通用C语言库,提供了许多常用的数据类型、数据结构、线程支持、文件操作、内存管理、字符串处理、网络编程等功能,方便开发者进行跨平台的应用程序开发。GLib最初是GTK+项目的一部分,但现在已经成为一个独立的开源项目,其设计目标是为了提供一个高效、可移植、易于使用的C语言库。

其官方网址:GLib – 2.0 (gtk.org)

事件循环机制

我们先说glib事件循环机制一般有什么用?glib实现了一个功能完整的事件循环分发处理的机制,这些事件来源包括各种文件描述符(文件、管道或者socket)、超时和idle事件等,当然也可以自定义事件源。使用这套接口注册事件源以及对应的处理回调方法,可以方便开发基于事件触发的应用,如QEMU。

glib的事件循环机制是基于poll的,这又是牵扯到一个叫做 I/O多路复用 的概念。 IO多路复用是一种同步 IO 模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出CPU。这样就可以减少系统开销,避免创建过多进程或者线程维护文件句柄。而实现IO多路复用的方式主要是这三种:selectpollepoll

glib是一个跨平台的库,在Linux上,使用的是poll函数,在window上,使用的是select。而epoll这个接口,在linux2.6中才正式推出,它的效率比前两者更高,在网络编程中大量使用。而本质上,这三个函数,其实是相同的。如果要看详细的对比,可以看这个博主的文章: IO 多路复用_io多路复用_JFS_Study的博客-CSDN博客

poll机制

以poll为例,简单介绍的poll使用如下:

#include <poll.h>
...
struct pollfd fds[2];
int timeout_msecs = 500;
int ret;
int i;

/* Open STREAMS device. */
fds[0].fd = open("/dev/dev0", ...);
fds[1].fd = open("/dev/dev1", ...);
fds[0].events = POLLOUT | POLLWRBAND; //等待发生的事件
fds[1].events = POLLOUT | POLLWRBAND; //等待发生的事件

while(1) {
    ret = poll(fds, 2, timeout_msecs);
    if (ret > 0) {
        /* An event on one of the fds has occurred. */
        for (i=0; i<2; i++) {
            //检查实际发生的事件
            if (fds[i].revents & POLLWRBAND) {
            /* Priority data may be written on device number i. */
            ...
            }
            if (fds[i].revents & POLLOUT) {
            /* Data may be written on device number i. */
            ...
            }
            if (fds[i].revents & POLLHUP) {
            /* A hangup has occurred on device number i. */
            ...
            }
        }
    }
}
...

上面这个代码,我们可以把它拆分成3部分:

  1. 准备要检测的文件集合(不是简单的准备“文件描述符”的集合,而是准备struct pollfd结构体的集合。这就包括了文件描述符,以及希望监控的事件,如可读/可写/或可执行其他操作等)。
    struct pollfd {
    int fd;        /* 文件描述符 */
    short events;  /* 等待的事件 */
    short revents; /* 实际发生了的事件 */
    };
  2. 执行poll,等待事件发生(文件描述符对应的文件可读/可写/或可执行其他操作等)或者是函数超时返回。
  3. 遍历文件集合(struct pollfd结构体的集合),判断具体是哪些文件有“事件”发生,并且进一步判断是何种“事件”。然后,根据需求,执行对应的操作(上面的代码中,用…表示的对应操作)。

其中23对应的代码,都放在一个while循环中。而在3中所谓的“对应的操作”,还可以包括一种“退出”操作,这样的话,就可以从while循环中退出,这样的话,整个进程也有机会正常结束。

这段代码比较简单,但是存在一些使用上的问题:

  1. 这段代码里仅仅打开了两个文件,传递给poll函数,如果程序运行过程中,想要动态的增加poll函数监控的文件,怎么实现?
  2. 这段代码里面,设置的超时时间是固定的,加入某个时刻有100个文件需要监控,那么怎么针对100个文件设置不同的超时时间?
  3. 这段代码里面,当poll函数返回时,对监控的每个fd进行遍历,逐个判断并执行对应的擦欧总。那如果有100个文件被监控,其中某个文件优先级很高,非常紧急怎么办?

可以从面向对象的角度来看:

  1. 对第1个问题,可以想到,需要对 所有的文件struct pollfd)做一个统一的管理,需要有添加和删除文件的功能。用面向对象的思想来看,这就是一个类,暂且叫做类A
  2. 对第2个问题,可以想到,还需要对 每一个被监控的文件struct pollfd)做更多的控制。也可以用一个类来包装被监控的文件,对这个文件进行管理,在该对象中,包含了struct pollfd结构体,该类还可以提供对应的文件所期望的超时时间。暂且叫做类B
  3. 对第3个问题,可以考虑为每一个被监控的文件设置一个优先级,然后就可以根据优先级优先执行更“紧急”的“对应的操作”。这个优先级信息,也可以存储在类B中。设计出了类B之后,类A就不再是直接统一管理文件了,而是变成统一管理类B,可以看成是类B的一个容器类。

有了这些思想,在glib中的GMainLoop等就是做的这些事,接下来就看看glib中是怎么按照这个思想实现的。

glib事件循环涉及的重要数据结构

要深入理解glib事件循环机制,还是要看:Glib源码

glib的主事件循环框架,由3个类来实现,GMainLoopGMainContextGSource

其中对应关系:一个GMainLoop只包含一个GMainContext,一个GMainContext可以对应多个GSource。这里面最主要的还是GMainContextGSource,分别对应前面提到的类A类B

GSource

GSource相当于前面提到的类B,它里面会保存优先级信息以及对应的需要监控的文件,并且GSource是一个链表的结构,保存了下一个GSource的引用。

struct _GSource
{
  /*< private >*/
  gpointer callback_data; //回调函数的参数。
  GSourceCallbackFuncs *callback_funcs; //回调函数的指针。

  const GSourceFuncs *source_funcs; //GSource 的函数指针,用于实现 GSource 的事件处理逻辑。
  guint ref_count;

  GMainContext *context; //GSource 所属的 GMainContext。

  gint priority; //GSource 的优先级。
  guint flags; //GSource 的标志位,表示 GSource 的状态和类型。
  guint source_id; //GSource 的唯一标识符。

  GSList *poll_fds; //GSource 关心的文件描述符列表。

  GSource *prev; //GSource 在 GMainContext 中的链表中的前驱和后继。
  GSource *next; 

  char    *name; //GSource 的名称。

  GSourcePrivate *priv; //GSource 的私有数据。
};

GSource 的函数指针指向的结构体定义了用于实现 GSource 的事件处理逻辑。(从面向对象的角度看,GSource是一个抽象类,而且有4个重要的纯虚函数,需要子类来具体实现),这4个函数就是:其中最重要的还是prepare、check、dispatch。

struct _GSourceFuncs
{
  gboolean (*prepare)  (GSource    *source,
                        gint       *timeout_);/* Can be NULL */
  gboolean (*check)    (GSource    *source);/* Can be NULL */
  gboolean (*dispatch) (GSource    *source,
                        GSourceFunc callback,
                        gpointer    user_data);
  //释放资源的
  void     (*finalize) (GSource    *source); /* Can be NULL */
};

我们对比poll机制中示例代码中的3部分,这个prepare函数,就是要在第一部分被调用的,checkdispathch函数,就是在第3部分被调用的。有一点区别是,prepare函数也要放到while循环中,而不是在循环之外(因为要动态的增加或者删除poll函数监控的文件)。

  1. prepare函数,会在执行poll之前被调用。该GSource中的struct pollfd是否希望被poll函数监控,就由prepare函数的返回值来决定。
  2. check函数,在执行poll之后被调用。该GSource中的struct pollfd是否有事件发生,就由check函数的返回值来描述(在check函数中可以检测struct pollfd结构体中的返回信息)。
  3. dispatch函数,在执行pollcheck函数之后被调用,并且,仅当对应的check函数返回true的时候,对应的dispatch函数才会被调用,dispatch函数,就相当于“对应的操作”。

QEMU的事件循环-01 glib事件循环机制

GMainContext

GMainContextGSource的容器,GSource可以添加到GMainContext里面(间接的就把GSource中的struct pollfd也添加到GMainContext里面了),GSource也可以从GMainContext中移除(间接的就把GSource中的struct pollfdGMainContext中移除了)。GMainContext可以遍历GSource,自然就有机会调用每个GSourceprepare/check/dispatch函数,可以根据每个GSourceprepare函数的返回值来决定,是否要在poll函数中,监控该GSource管理的文件。当然可以根据GSource的优先级进行排序。当poll返回后,可以根据每个GSourcecheck函数的返回值来决定是否需要调用对应的dispatch函数。

GMainContext 定义如下:

/**
 * GMainContext:
 *
 * The `GMainContext` struct is an opaque data
 * type representing a set of sources to be handled in a main loop.
 */
typedef struct _GMainContext GMainContext;

//以下结构体省略了很多字段,多线程之间同步互斥等字段都以省略,
struct _GMainContext {
  GThread *owner; /* 拥有该 GMainContext 的线程 */

  GHashTable *sources;     /* 存储所有的 GSource 对象 */

  GList *source_lists;        /* 存储不同优先级的 GSource 对象列表 */
  GPollRec *poll_records;     /* 存储需要监听的文件描述符及其对应的事件类型 */

  guint next_id;              /* 下一个 GSource 对象的 ID */
  GPollFunc poll_func;        /* 用于监听文件描述符状态变化的函数 */
};

GMainLoop

GMainLoop结构一般调用下面的方法g_main_loop_new创建,需要传入GMainContext,这个参数可以为NULL,将使用默认的上下文,这个结构体代表着一个事件循环。

GMainLoop* g_main_loop_new (
  GMainContext* context,
  gboolean is_running
)

glib中g_main_context_iterate()关键函数理解

g_main_loop_run 函数是 GLib 中用于运行主循环的函数之一。它的作用是启动主循环并运行,直到被调用的 g_main_loop_quit 函数被调用或者发生错误(比如被信号中断)。

我们使用时一般都是按照如下步骤:

int main(int argc, char *argv[]) {
    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
    // 注册事件源到主循环中
    ...
    // 启动主循环并运行
    g_main_loop_run(loop);
    // 主循环结束,清理资源
    ...
    return 0;
}

接下来我们看一下源码:g_main_loop_run 函数中的g_main_context_iterate()函数,其实就相当于poll机制中代码片段中的循环体中要做的动作。循环的退出,则是靠loop->is_running这个标记变量来标识的。

void g_main_loop_run (GMainLoop *loop)
{
    ...
    //该标志会出现错误等情况被设置为false,终止循环。
    loop->is_running = TRUE;
    while (loop->is_running)
       g_main_context_iterate (loop->context, TRUE, TRUE, self);
    ...
}

static gboolean g_main_context_iterate (GMainContext *context, 
        gboolean block, gboolean dispatch, GThread *self) 
{
    gint max_priority;
    gint timeout;
    gboolean some_ready;
    gint nfds, allocated_nfds;
    GPollFD *fds = NULL;

    UNLOCK_CONTEXT (context);
    ...
    if (!context->cached_poll_array) {
        context->cached_poll_array_size = context->n_poll_records;
        context->cached_poll_array = g_new (GPollFD, context->n_poll_records);
    }
    allocated_nfds = context->cached_poll_array_size;
    fds = context->cached_poll_array;

    UNLOCK_CONTEXT (context);

    //prepare实现方法,准备需要被监控的fd
    g_main_context_prepare(context, &max_priority);
    while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,
                                        allocated_nfds)) > allocated_nfds) {
        LOCK_CONTEXT (context);
        g_free(fds);
        context->cached_poll_array_size = allocated_nfds = nfds;
        context->cached_poll_array = fds = g_new (GPollFD, nfds);
        UNLOCK_CONTEXT (context);
    }
    if (!block)
        timeout = 0;
    //poll函数简单封装
    g_main_context_poll(context, timeout, max_priority, fds, nfds);
    //检查有没有需要处理的fd
    some_ready = g_main_context_check(context, max_priority, fds, nfds);
    if (dispatch)
        //调用对应的处理方法。
        g_main_context_dispatch(context);

    LOCK_CONTEXT (context);
    return some_ready;
}

第一部分 准备要监控的文件集合

g_main_context_prepare(context, &max_priority);

while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,
                                    allocated_nfds)) > allocated_nfds) {
    LOCK_CONTEXT (context);
    g_free(fds);
    context->cached_poll_array_size = allocated_nfds = nfds;
    context->cached_poll_array = fds = g_new (GPollFD, nfds);
    UNLOCK_CONTEXT (context);
}

首先是调用g_main_context_prepare(context, &max_priority),这个就是遍历每个GSource,调用每个GSourceprepare函数,选出一个最高的优先级max_priority,函数内部其实还计算出了一个最短的超时时间。

然后调用g_main_context_query,其实这是再次遍历每个GSource,把优先级等于max_priorityGSource中的struct pollfd,添加到poll的监控集合中。

第二部分 执行poll,等待事件发生

if (!block)
    timeout = 0;
g_main_context_poll(context, timeout, max_priority, fds, nfds);

就是调用g_main_context_poll(context, timeout, max_priority, fds, nfds)g_main_context_poll只是对poll函数的一个简单封装。

第三部分 遍历被监控的文件集合,执行对应操作

some_ready = g_main_context_check(context, max_priority, fds, nfds);
if (dispatch)
    g_main_context_dispatch(context);

实际上,glib的处理方式是,先遍历所有的GSource,执行g_main_context_prepare(context, &max_priority),调用每个GSource的check函数,然后把满足条件的GSourcecheck函数返回trueGSource),添加到一个内部链表中。

然后执行g_main_context_dispatch(context),遍历刚才准备好的内部链表中的GSource,调用每个GSourcedispatch函数。

总结

所以可以看出,glib实现方式与poll机制使用思想是一致的,只是实现上更加复杂了,上述我们没有讨论到多线程等情况,如果讨论了会更加复杂。

glib事件循环机制使用示例

经过上面的了解,大概也知道了glib事件循环机制主要的结构和实现的思路,我们再给出一个例子可以更好的了解glib事件循环机制。

#include <stdio.h>
#include <stdlib.h>
#include <glib.h>
#include <fcntl.h>

// 文件描述符的回调函数,用于处理文件描述符的可读事件
static gboolean on_fd_events(GIOChannel *channel, GIOCondition condition, gpointer data) {
    char buffer[1024];
    gsize bytes_read;
    // 从文件描述符中读取数据
    g_io_channel_read_chars(channel, buffer, sizeof(buffer), &bytes_read, NULL);
    // 处理读取到的数据
    printf("Received data: %s\n", buffer);
    return TRUE;
}

int main(int argc, char *argv[]) {
    GMainLoop *loop;
    GIOChannel *channel;
    GSource *source;
    int fd;

    // 打开文件
    fd = open("test.txt", O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 创建 GIOChannel 对象,用于封装文件描述符
    channel = g_io_channel_unix_new(fd);

    // 创建 GSource 对象,用于监听文件描述符的可读事件
    source = g_io_create_watch(channel, G_IO_IN);

    // 设置 GSource 的回调函数
    g_source_set_callback(source, (GSourceFunc)on_fd_events, NULL, NULL);

    // 创建 GMainLoop 对象,用于启动事件循环
    loop = g_main_loop_new(NULL, FALSE);

    // 将 GSource 对象添加到事件循环中
    g_source_attach(source, NULL);

    // 启动事件循环
    g_main_loop_run(loop);

    // 清理资源
    g_source_destroy(source);
    g_main_loop_unref(loop);
    g_io_channel_unref(channel);
    close(fd);

    return 0;
}

先查看glib在哪儿不然直接编译有可能出错。

root@test-ubuntu-no-vgpu:~# pkg-config --cflags --libs glib-2.0
-I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -lglib-2.0

root@test-ubuntu-no-vgpu:~# gcc glib_test.c -o glib_test -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -lglib-2.0
root@test-ubuntu-no-vgpu:~# ./glib_test
Received data: 123456789
7F
Received data: 123456789
7F
Received data: 123456789
...

可以看到在上述代码中,我们先创建了GSource,添加了需要监听的fd,然后将GSource加入了默认GMainContext中,然后启动事件循环。

如何自定义事件源

glib还支持自定义我们的事件源GSource,可以参考这个博主的liyansong2018/glib_demo: glib 主事件循环的案例 (github.com)

基本上就是定义事件源结构,实现对应的Gsource的抽象接口,QEMU中也实现了自己的自定义事件源AioContext。

如:

/* 自定义事件源 */
typedef struct MyGSource {
    GSource source;
    ...
    其他字段
    GPollFD fd;
} MyGSource;

//实现如下几个方法
gboolean g_source_myself_prepare(GSource * source, gint * timeout);
gboolean g_source_myself_check(GSource * source);
gboolean g_source_myself_dispatch(GSource * source,
                GSourceFunc callback, gpointer user_data);
void g_source_myself_finalize(GSource * source);

//自定义函数集合
GSourceFuncs g_source_myself_funcs = {
        g_source_myself_prepare,
        g_source_myself_check,
        g_source_myself_dispatch,
        g_source_myself_finalize,
    };

//使用方法:
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
//事件源容器创建,事件源上下文
GMainContext *context = g_main_loop_get_context(loop);
//事件源创建,绑定对应的事件处理函数
GSource *source = g_source_new(&g_source_myself_funcs, sizeof(MyGSource));
MyGSource *source_myself = (MyGSource *) source;
//绑定要监听的fd
g_source_add_poll(source, &source_myself->fd);
//添加事件源
g_source_attach(source, context);
//开启主循环
g_main_loop_run(loop);

参考文献

正文完
 
landery
版权声明:本站原创文章,由 landery 2023-06-08发表,共计10719字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)