共计 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多路复用的方式主要是这三种:select
、poll
、epoll
。
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部分:
- 准备要检测的文件集合(不是简单的准备“文件描述符”的集合,而是准备
struct pollfd
结构体的集合。这就包括了文件描述符,以及希望监控的事件,如可读/可写/或可执行其他操作等)。struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生了的事件 */ };
- 执行
poll
,等待事件发生(文件描述符对应的文件可读/可写/或可执行其他操作等)或者是函数超时返回。 - 遍历文件集合(
struct pollfd
结构体的集合),判断具体是哪些文件有“事件”发生,并且进一步判断是何种“事件”。然后,根据需求,执行对应的操作(上面的代码中,用…表示的对应操作)。
其中2和3对应的代码,都放在一个while
循环中。而在3中所谓的“对应的操作”,还可以包括一种“退出”操作,这样的话,就可以从while
循环中退出,这样的话,整个进程也有机会正常结束。
这段代码比较简单,但是存在一些使用上的问题:
- 这段代码里仅仅打开了两个文件,传递给poll函数,如果程序运行过程中,想要动态的增加poll函数监控的文件,怎么实现?
- 这段代码里面,设置的超时时间是固定的,加入某个时刻有100个文件需要监控,那么怎么针对100个文件设置不同的超时时间?
- 这段代码里面,当poll函数返回时,对监控的每个fd进行遍历,逐个判断并执行对应的擦欧总。那如果有100个文件被监控,其中某个文件优先级很高,非常紧急怎么办?
可以从面向对象的角度来看:
- 对第1个问题,可以想到,需要对 所有的文件(
struct pollfd
)做一个统一的管理,需要有添加和删除文件的功能。用面向对象的思想来看,这就是一个类,暂且叫做类A。 - 对第2个问题,可以想到,还需要对 每一个被监控的文件(
struct pollfd
)做更多的控制。也可以用一个类来包装被监控的文件,对这个文件进行管理,在该对象中,包含了struct pollfd
结构体,该类还可以提供对应的文件所期望的超时时间。暂且叫做类B。 - 对第3个问题,可以考虑为每一个被监控的文件设置一个优先级,然后就可以根据优先级优先执行更“紧急”的“对应的操作”。这个优先级信息,也可以存储在类B中。设计出了类B之后,类A就不再是直接统一管理文件了,而是变成统一管理类B,可以看成是类B的一个容器类。
有了这些思想,在glib中的GMainLoop等就是做的这些事,接下来就看看glib中是怎么按照这个思想实现的。
glib事件循环涉及的重要数据结构
要深入理解glib事件循环机制,还是要看:Glib源码
glib
的主事件循环框架,由3个类来实现,GMainLoop
,GMainContext
和GSource
。
其中对应关系:一个GMainLoop
只包含一个GMainContext
,一个GMainContext
可以对应多个GSource
。这里面最主要的还是GMainContext
和GSource
,分别对应前面提到的类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
函数,就是要在第一部分被调用的,check
和dispathch
函数,就是在第3部分被调用的。有一点区别是,prepare
函数也要放到while
循环中,而不是在循环之外(因为要动态的增加或者删除poll
函数监控的文件)。
prepare
函数,会在执行poll
之前被调用。该GSource
中的struct pollfd
是否希望被poll
函数监控,就由prepare
函数的返回值来决定。check
函数,在执行poll
之后被调用。该GSource
中的struct pollfd
是否有事件发生,就由check
函数的返回值来描述(在check
函数中可以检测struct pollfd
结构体中的返回信息)。dispatch
函数,在执行poll
和check
函数之后被调用,并且,仅当对应的check
函数返回true的时候,对应的dispatch
函数才会被调用,dispatch
函数,就相当于“对应的操作”。
GMainContext
GMainContext
是GSource
的容器,GSource
可以添加到GMainContext
里面(间接的就把GSource
中的struct pollfd
也添加到GMainContext
里面了),GSource
也可以从GMainContext
中移除(间接的就把GSource
中的struct pollfd
从GMainContext
中移除了)。GMainContext
可以遍历GSource
,自然就有机会调用每个GSource
的prepare/check/dispatch
函数,可以根据每个GSource
的prepare
函数的返回值来决定,是否要在poll
函数中,监控该GSource
管理的文件。当然可以根据GSource
的优先级进行排序。当poll
返回后,可以根据每个GSource
的check
函数的返回值来决定是否需要调用对应的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
,调用每个GSource
的prepare
函数,选出一个最高的优先级max_priority
,函数内部其实还计算出了一个最短的超时时间。
然后调用g_main_context_query
,其实这是再次遍历每个GSource
,把优先级等于max_priority
的GSource
中的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
函数,然后把满足条件的GSource
(check
函数返回true
的GSource
),添加到一个内部链表中。
然后执行g_main_context_dispatch(context)
,遍历刚才准备好的内部链表中的GSource
,调用每个GSource
的dispatch
函数。
总结
所以可以看出,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);