【glib】GMainLoop的實現原理和代碼模型

原文:https://blog.csdn.net/jack0106/article/details/6258422
作者聯繫方式:馮牮 [email protected]

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的源代碼實現,這樣纔有助於理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章