原文:https://blog.csdn.net/jack0106/article/details/6258422
作者聯繫方式:馮牮 [email protected]
Glib 源碼:https://download.gnome.org/sources/glib/
GLib API:https://developer.gnome.org/glib/stable/
做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 loop的代碼基礎,還要用到一個概念 —— I/O的多路複用。目前常用的api接口,有3個,select
,poll
以及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部分:
- 準備要檢測的文件集合(不是簡單的準備“文件描述符"的集合,而是準備
struct pollfd
結構體的集合。這就包括了文件描述符,以及希望監控的事件,如可讀/可寫/或可執行其他操作等)。
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 實際發生了的事件 */
};
-
執行
poll
,等待事件發生(文件描述符對應的文件可讀/可寫/或可執行其他操作等)或者是函數超時返回。 -
遍歷文件集合(
struct pollfd
結構體的集合),判斷具體是哪些文件有“事件"發生,並且進一步判斷是何種“事件"。然後,根據需求,執行對應的操作(上面的代碼中,用…表示的對應操作)。
其中2和3對應的代碼,都放在一個while
循環中。而在3中所謂的“對應的操作",還可以包括一種“退出"操作,這樣的話,就可以從while
循環中退出,這樣的話,整個進程也有機會正常結束。
再次提醒一下,請先把上面這段代碼看懂,最好是有過實際的使用經驗,這樣更有助於理解。
下面開始討論重點。這段代碼僅僅是演示,所以它很簡單。但是,從另外一個角度來看,這個代碼片段又很死板,尤其是對於新手或者是沒有I/O多路複用實際使用經驗的朋友來說,很容易被這段代碼模型“框住"。它還能變得更靈活嗎?怎樣才能變得更靈活?詳細解釋之前,先提幾個小問題。
-
前面的代碼,僅打開了2個文件,並且傳遞給poll函數。如果,在程序運行過程中,想動態的增加或者刪除
poll
函數監控的文件,怎麼辦? -
前面的代碼,設置的超時時間,是固定的。假設,某個時刻有100個文件需要被監控,而針對這100個不同的文件,每個文件期望設置的超時時間都不一樣,怎麼辦?
-
前面的代碼,當
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個類來實現,GMainLoop
,GMainContext
和GSource
,其中的GMainLoop
僅僅是GMainContext
的一個外殼,最重要的,還是GMainContext
和GSource
。GMainContext
就相當於前面提到的類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
函數,就是要在第一部分被調用的,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
函數,就相當於“對應的操作"。
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);
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部分對應上。
- 第一部份,準備要檢測的文件集合
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)
就是最典型的例子。
- 第二部份,執行
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
函數的一個簡單封裝。
- 第三部分,遍歷文件集合(
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
的源代碼實現,這樣纔有助於理解。