【glib】GMainLoop的实现原理和代码模型

原文:https://blog.csdn.net/jack0106/article/details/6258422
作者联系方式:冯牮 fengjian0106@yahoo.com.cn

Glib 源码https://download.gnome.org/sources/glib/
GLib APIhttps://developer.gnome.org/glib/stable/



做linux程序开发有一段时间了,也使用过好几个UI库,包括gtk,qt,还有clutter。其中感觉最神秘的,就是所谓的“主事件循环"。在qt中,就是QApplication,gtk中是gtk_main(),clutter中则是clutter_main()。这些事件循环对象,都被封装的很“严密",使用的时候,代码都很简单。而我们在编写应用程序的过程中,通常也只需要重载widget的event处理函数(或者是处理event对应的信号),至于event是怎样产生和传递的,这就是个谜。


引入

最近时间比较充裕,仔细研究了一下事件循环,参考的代码是glib中的GMainLoopgtk_main()clutter_main()都是基于GMainLoop的。另外,其实事件循环的概念,也不仅仅使用在UI编程中,在网络编程中,同样大量的使用。可以这样说,event loop 是编程模型中,最基本的一个概念。可惜在大学教材中,从来没有看到过这个概念,玩单片机的时候,也用不到这个概念,只有在有操作系统的环境下,才会有event loop

event loop的代码基础,还要用到一个概念 —— I/O的多路复用。目前常用的api接口,有3个,selectpoll以及epoll。glib是一个跨平台的库,在linux上,使用的是poll函数,在window上,使用的是select。而epoll这个接口,在linux2.6中才正式推出,它的效率比前两者更高,在网络编程中大量使用。而本质上,这三个函数,其实是相同的。

如果对I/O多路复用还不了解,请先自行google学习。下面,仅仅给出一个使用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; /* 实际发生了的事件 */
};
  1. 执行poll,等待事件发生(文件描述符对应的文件可读/可写/或可执行其他操作等)或者是函数超时返回。

  2. 遍历文件集合(struct pollfd结构体的集合),判断具体是哪些文件有“事件"发生,并且进一步判断是何种“事件"。然后,根据需求,执行对应的操作(上面的代码中,用…表示的对应操作)。

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


再次提醒一下,请先把上面这段代码看懂,最好是有过实际的使用经验,这样更有助于理解。


下面开始讨论重点。这段代码仅仅是演示,所以它很简单。但是,从另外一个角度来看,这个代码片段又很死板,尤其是对于新手或者是没有I/O多路复用实际使用经验的朋友来说,很容易被这段代码模型“框住"。它还能变得更灵活吗?怎样才能变得更灵活?详细解释之前,先提几个小问题。

  1. 前面的代码,仅打开了2个文件,并且传递给poll函数。如果,在程序运行过程中,想动态的增加或者删除poll函数监控的文件,怎么办?

  2. 前面的代码,设置的超时时间,是固定的。假设,某个时刻有100个文件需要被监控,而针对这100个不同的文件,每个文件期望设置的超时时间都不一样,怎么办?

  3. 前面的代码,当poll函数返回,对文件集合进行遍历的时候,是逐个进行判断并且执行“对应的操作"。如果,有100个文件被监控,当poll返回时,这100个文件,都满足条件,可以进行“对应的操作",其中的50个文件的“对应的操作"很耗时间,但是并不是这么紧急(可以稍后再处理,比如等到下一轮poll返回时再处理),而另外50个文件的“对应的操作”需要立即执行,并且很快(在下一次poll的时候)又会有新的事件发生并且满足判断时的条件,怎么办?

对第1个问题,可以想到,需要对 所有的文件struct pollfd)做一个统一的管理,需要有添加和删除文件的功能。用面向对象的思想来看,这就是一个类,暂且叫做类A

对第2个问题,可以想到,还需要对 每一个被监控的文件struct pollfd)做更多的控制。也可以用一个类来包装被监控的文件,对这个文件进行管理,在该对象中,包含了struct pollfd结构体,该类还可以提供对应的文件所期望的超时时间。暂且叫做类B

对第3个问题,可以考虑为每一个被监控的文件设置一个优先级,然后就可以根据优先级优先执行更“紧急"的“对应的操作"。这个优先级信息,也可以存储在类B中。设计出了类B之后,类A就不再是直接统一管理文件了,而是变成统一管理类B,可以看成是类B的一个容器类。

有了这3个解答之后,就可以对这个代码片段添油加醋,重新组装,让它变得更灵活了。glib中的GMainLoop,做的就是这样的事情,而且,它做的事情,除了这3个解答中描述的内容外,还有更让人“吃惊的惊喜"。

😃,这里又要提醒一下了,下面将对GMainLoop进行描述,所以,最好是先使用一下GMainLoop,包括其中的g_timeout_source_new(guint interval)g_idle_source_new(void)以及g_child_watch_source_new(GPid pid)。顺便再强调一下,学习编程的最好的办法,就是看代码,而且是看高质量的代码。


GMainLoop的实现机制

后面的讲解,主要是从原理上来介绍GMainLoop的实现机制,并不是代码的情景分析。代码( gmain.c)的详细阅读,还是需要自己老老实实的去实践的。后面的这些介绍,只是为了帮助大家更容易的理解源代码。

glib的主事件循环框架,由3个类来实现,GMainLoopGMainContextGSource,其中的GMainLoop仅仅是GMainContext的一个外壳,最重要的,还是GMainContextGSourceGMainContext就相当于前面提到的类A,而GSource就相当于前面提到的类B。从原理上讲,g_main_loop_run(GMainLoop *loop)这个函数的内部实现,和前面代码片段中的while循环,是一致的(还有一点要说明的,在多线程的环境下,GMainLoop的代码实现显得比较复杂,为了学习起来更容易些,可以先不考虑GMainLoop中线程相关的代码,这样的话,整体结构就和前面的代码片段是一致的。后面的讲解以及代码片段,都略去了线程相关的代码,这并不影响对event loop的学习和理解)。

GSource

GSource相当于前面提到的类B,它里面会保存优先级信息。同时,GSource要管理对应的文件(保存struct pollfd结构体的指针,而且是以链表的形式保存),而且,GSource和被管理的文件的对应关系,不是 1对1,而是 1对n。这个n,甚至可以是0(这就是一个“吃惊的惊喜",后面会有更详细的解释)。GSource还必须提供3个重要的函数(从面向对象的角度看,GSource是一个抽象类,而且有三个重要的纯虚函数,需要子类来具体实现),这3个函数就是:

  gboolean (*prepare)(GSource *source, gint *timeout_);

  gboolean (*check)(GSource *source);

  gboolean (*dispatch)(GSource *source, GSourceFunc callback, gpointer user_data);

再看一下前面代码片段中的3部分,这个prepare函数,就是要在第一部分被调用的,checkdispathch函数,就是在第3部分被调用的。有一点区别是,prepare函数也要放到while循环中,而不是在循环之外(因为要动态的增加或者删除poll函数监控的文件)。

prepare函数,会在执行poll之前被调用。该GSource中的struct pollfd是否希望被poll函数监控,就由prepare函数的返回值来决定。同时,该GSource希望的超时时间,也由参数timeout_返回。

check函数,在执行poll之后被调用。该GSource中的struct pollfd是否有事件发生,就由check函数的返回值来描述(在check函数中可以检测struct pollfd结构体中的返回信息)。

dispatch函数,在执行pollcheck函数之后被调用,并且,仅当对应的check函数返回true的时候,对应的dispatch函数才会被调用,dispatch函数,就相当于“对应的操作"。
状态转化关系图

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函数。

下面给出关键的代码片段,其中的g_main_context_iterate()函数,就相当于前面代码片段中的循环体中要做的动作。循环的退出,则是靠loop->is_running这个标记变量来标识的。

void g_main_loop_run (GMainLoop *loop)
{
    GThread *self = G_THREAD_SELF;
    g_return_if_fail (loop != NULL);
    g_return_if_fail (g_atomic_int_get (&loop->ref_count) > 0);

	if (!g_main_context_acquire (loop->context)){
      ...
    } else
    LOCK_CONTEXT (loop->context);

    g_atomic_int_inc (&loop->ref_count);
    loop->is_running = TRUE;
    while (loop->is_running)
       g_main_context_iterate (loop->context, TRUE, TRUE, self);
       
    UNLOCK_CONTEXT (loop->context);
    g_main_context_release (loop->context);
    g_main_loop_unref (loop);
}

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);
	
	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;
	g_main_context_poll(context, timeout, max_priority, fds, nfds);
	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_iterate()函数,也可以把它划分成3个部分,和前面代码片段的3部分对应上。

  1. 第一部份,准备要检测的文件集合
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的监控集合中。

这个优先级,也是一个“吃惊的惊喜"。按照通常的想法,文件需要被监控的时候,会立刻把它放到监控集合中,但是有了优先级这个概念后,我们就可以有一个“隐藏的后台任务", g_idle_source_new(void)就是最典型的例子。

  1. 第二部份,执行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函数的一个简单封装。

  1. 第三部分,遍历文件集合(struct pollfd结构体的集合),执行对应的操作。
some_ready = g_main_context_check(context, max_priority, fds, nfds);
if (dispatch)
	g_main_context_dispatch(context);

通常的想法,可能会是这种伪代码形式(这种形式也和前面代码片段的形式是一致的)

foreach(all_gsouce) {
    if (gsourc->check) {
     	gsource->dispatch();
    }
}

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

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


ok,分析到此结束,总结一下,重点,首先是要先理解poll函数的使用方法,建立I/O多路复用的概念,然后,建议看一下GMainContext的源代码实现,这样才有助于理解。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页