[置顶] GMainLoop的实现原理和代码模型

转载时请注明出处和作者联系方式
文章出处:
http://blog.csdn.net/jack0106 
作者联系方式:冯牮 
[email protected]

 

 

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

 

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

 

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

 

如果对I/O多路复用还不了解,请先自行google学习。下面,仅仅给出一个使用poll接口的代码模型片段。

 

... 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结构体的集合。这就包括了文件描述符,以及希望监控的事件,如可读/可写/或可执行其他操作等)。

2. 执行poll,等待事件发生(文件描述符对应的文件可读/可写/或可执行其他操作等)或者是函数超时返回。

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

其中2和3对应的代码,都放在一个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的实现机制,并不是代码的情景分析。代码的详细阅读,还是需要自己老老实实的去实践的。后面的这些介绍,只是为了帮助大家更容易的理解源代码。

 

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

 

1.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函数,就是要在第一部分被调用的,check和dispathch函数,就是在第3部分被调用的。有一点区别是,prepare函数,也要放到while循环中,而不是在循环之外(因为要动态的增加或者删除poll函数监控的文件)。

 

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

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

dispatch函数,在执行poll和check函数之后被调用,并且,仅当对应的check函数返回true的时候,对应的dispatch函数才会被调用,dispatch函数,就相当于“对应的操作"。

 

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

 

下面给出关键的代码片段,其中的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); 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_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,调用每个GSource的prepare函数,选出一个最高的优先级max_priority,函数内部其实还计算出了一个最短的超时时间。

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

 

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

 

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

 

3. 第三部分,遍历文件集合(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函数,然后把满足条件的GSource(check函数返回true的GSource),添加到一个内部链表中。

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

 

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

 

 

你可能感兴趣的:(编程,linux,struct,query,callback,gtk)