轉載時請註明出處和作者連絡方式
文章出處:http://blog.csdn.net/jack0106
作者連絡方式:馮牮 fengjian0106@yahoo.com.cn
做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介面的代碼模型片段。
...<br />struct pollfd fds[2];<br />int timeout_msecs = 500;<br />int ret;<br />int i;<br />/* Open STREAMS device. */<br />fds[0].fd = open("/dev/dev0", ...);<br />fds[1].fd = open("/dev/dev1", ...);<br />fds[0].events = POLLOUT | POLLWRBAND;<br />fds[1].events = POLLOUT | POLLWRBAND;<br />while(1) {<br />ret = poll(fds, 2, timeout_msecs);<br />if (ret > 0) {<br /> /* An event on one of the fds has occurred. */<br /> for (i=0; i<2; i++) {<br />if (fds[i].revents & POLLWRBAND) {<br />/* Priority data may be written on device number i. */<br />...<br />}<br />if (fds[i].revents & POLLOUT) {<br />/* Data may be written on device number i. */<br />...<br />}<br />if (fds[i].revents & POLLHUP) {<br />/* A hangup has occurred on device number i. */<br />...<br />}<br /> }<br />}<br />}<br />...
上面這個代碼,我們可以把它拆分成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)<br />{<br /> GThread *self = G_THREAD_SELF;<br /> g_return_if_fail (loop != NULL);<br /> g_return_if_fail (g_atomic_int_get (&loop->ref_count) > 0);<br /> g_atomic_int_inc (&loop->ref_count);<br /> loop->is_running = TRUE;<br /> while (loop->is_running)<br /> g_main_context_iterate (loop->context, TRUE, TRUE, self);<br /> UNLOCK_CONTEXT (loop->context);<br /> g_main_loop_unref (loop);<br />}<br />static gboolean g_main_context_iterate(GMainContext *context, gboolean block,<br />gboolean dispatch, GThread *self) {<br />gint max_priority;<br />gint timeout;<br />gboolean some_ready;<br />gint nfds, allocated_nfds;<br />GPollFD *fds = NULL;<br />UNLOCK_CONTEXT (context);<br />if (!context->cached_poll_array) {<br />context->cached_poll_array_size = context->n_poll_records;<br />context->cached_poll_array = g_new (GPollFD, context->n_poll_records);<br />}<br />allocated_nfds = context->cached_poll_array_size;<br />fds = context->cached_poll_array;<br />UNLOCK_CONTEXT (context);<br />g_main_context_prepare(context, &max_priority);<br />while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,<br />allocated_nfds)) > allocated_nfds) {<br />LOCK_CONTEXT (context);<br />g_free(fds);<br />context->cached_poll_array_size = allocated_nfds = nfds;<br />context->cached_poll_array = fds = g_new (GPollFD, nfds);<br />UNLOCK_CONTEXT (context);<br />}<br />if (!block)<br />timeout = 0;<br />g_main_context_poll(context, timeout, max_priority, fds, nfds);<br />some_ready = g_main_context_check(context, max_priority, fds, nfds);<br />if (dispatch)<br />g_main_context_dispatch(context);</p><p>LOCK_CONTEXT (context);<br />return some_ready;<br />}
仔細看一下g_main_context_iterate()函數,也可以把它劃分成3個部分,和前面程式碼片段的3部分對應上。
1. 第一部份,準備要檢測的檔案集合
g_main_context_prepare(context, &max_priority);<br />while ((nfds = g_main_context_query(context, max_priority, &timeout, fds,<br />allocated_nfds)) > allocated_nfds) {<br />LOCK_CONTEXT (context);<br />g_free(fds);<br />context->cached_poll_array_size = allocated_nfds = nfds;<br />context->cached_poll_array = fds = g_new (GPollFD, nfds);<br />UNLOCK_CONTEXT (context);<br />}
首先是調用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)<br />timeout = 0;<br />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);<br />if (dispatch)<br />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的原始碼實現,這樣才有助於理解。