深入理解qemu事件循環 —— 基本框架

文章系列:
深入理解qemu事件循環 —— 基本框架
深入理解qemu事件循環 ——下半部

Glib 事件循環

簡述

  • Glib事件循環機制提供了一套事件分發接口,使用這套接口註冊事件源(source)和對應的回調,可以開發基於事件觸發的應用。Glib的核心是poll機制,通過poll檢查用戶註冊的事件源,並執行對應的回調,用戶不需要關注其具體實現,只需要按照要求註冊對應的事件源和回調
  • Glib事件循環機制管理所有註冊的事件源,主要類型有:fd,pipe,socket,和 timer。不同事件源可以在一個線程中處理,也可以在不同線程中處理,這取決於事件源所在的上下文( GMainContext)。一個上下文只能運行在一個線程中,所以如果想要事件源在不同線程中併發被處理,可以將其放在不同的上下文
  • qemu是事件觸發設計架構,實現基礎就是glib事件循環,本文主要介紹Glib事件循環的機制,以此爲基礎實現一個基本的qemu事件循環框架

事件循環狀態機

在這裏插入圖片描述

  • Glib對一個事件源的處理分爲4個階段:初始化,準備,poll,和調度。如上圖的狀態機所示。
  • Glib狀態機的每個階段都提供了接口供用戶註冊自己的處理函數。分別如下:
  1. prepare: gboolean (*prepare) (GSource *source, gint *timeout_);
    Glib初始化完成後會調用此接口,此接口返回TRUE表示事件源都已準備好,告訴Glib跳過poll直接檢查判斷是否執行對應回調。返回FALSE表示需要poll機制監聽事件源是否準備好,如果有事件源沒準備好,通過參數timeout指定poll最長的阻塞時間,超時後直接返回,超時接口可以防止一個fd或者其它事件源阻塞整個應用
  2. query:gint g_main_context_query (GMainContext *context, gint max_priority, gint *timeout_, GPollFD *fds, gint n_fds);
    Glib在prepare完成之後,可以通過query查詢一個上下文將要poll的所有事件,這個接口需要用戶主動調用
  3. check:gboolean (*check) (GSource *source);
    Glib在poll返回後會調用此接口,用戶通過註冊此接口判斷哪些事件源需要被處理,此接口返回TRUE表示對應事件源的回調函數需要被執行,返回FALSE表示不需要被執行
  4. dispatch:gboolean (*dispatch) (GSource *source, GSourceFunc callback, gpointer user_data);
    Glib根據check的結果調用此接口,參數callbackuser_data是用戶通過g_source_set_callback註冊的事件源回調和對應的參數,用戶可以在dispatch中選擇直接執行callback,也可以不執行

demo

定製事件源,回調函數

#include <glib.h>
/*
函數打印標準輸入中讀到內容的長度
*/
gboolean io_watch(GIOChannel *channel,
                  GIOCondition condition,
                  gpointer data)
{
    gsize len = 0;
    gchar *buffer = NULL;

    g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);

    if(len > 0)
        g_print("%d\n", len);

    g_free(buffer);

    return TRUE;
}

int main(int argc, char* argv[])
{
    GMainLoop *loop = g_main_loop_new(NULL, FALSE);	// 獲取一個上下文的事件循環實例,context爲NULL則獲取默認的上下文循環
    GIOChannel* channel = g_io_channel_unix_new(1);	// 將標準輸入描述符轉化成GIOChannel,方便操作

    if(channel) {
        g_io_add_watch(channel, G_IO_IN, io_watch, NULL);	
        // 將針對channel事件源的回調註冊到默認上下文,告訴Glib自己對channel的輸入(G_IO_IN)感興趣
        // 當輸入準備好之後,調用自己註冊的回調io_watch,並傳入參數NULL。
        g_io_channel_unref(channel);
    }

    g_main_loop_run(loop);	// 執行默認上下文的事件循環
    g_main_context_unref(g_main_loop_get_context(loop));
    g_main_loop_unref(loop);

    return 0;
}

運行效果:
在這裏插入圖片描述

定製事件源,回調函數和狀態機回調函數

#include <glib.h>                                       
                                                                                                                                                                                                               
typedef struct _MySource MySource;
/* 自定義事件源,繼承自Glib的GSource類型*/
struct _MySource
{
    GSource _source;	// 基類
    GIOChannel *channel;
    GPollFD fd;
};
/*事件源回調函數,讀出iochannel中的內容,打印其長度*/
static gboolean watch(GIOChannel *channel)
{
    gsize len = 0;
    gchar *buffer = NULL;

    g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);
    if(len > 0)
    	g_print("%d\n", len);
    g_free(buffer);

    return TRUE;
}
/*
狀態機prepare回調函數,timeout等於-1告訴poll如果IO沒有準備好,一直等待,即阻塞IO
返回FALSE指示需要poll來檢查事件源是否準備好,如果是TRUE表示跳過poll
*/
static gboolean prepare(GSource *source, gint *timeout)
{
    *timeout = -1;
    return FALSE;
}
/*
狀態機check回調函數,檢查自己感興趣的fd狀態(events)是否準備好
用戶通過設置events標誌設置感興趣的fd狀態(包括文件可讀,可寫,異常等)
revents是poll的返回值,由內核設置,表明fd哪些狀態是準備好的
函數功能:
當感興趣的狀態和poll返回的狀態不相同,表示fd沒有準備好,返回FALSE,Glib不發起調度
反之返回TRUE,Glib發起調度
*/
static gboolean check(GSource *source)
{
    MySource *mysource = (MySource *)source;

    if(mysource->fd.revents != mysource->fd.events)
    	return FALSE;

    return TRUE;
}
/*
狀態機dispatch回調函數,prepare和check其中只要由一個返回TRUE,Glib就會直接調用此接口
函數邏輯是執行用戶註冊的回調函數
*/
static gboolean dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
{
    MySource *mysource = (MySource *)source;

    if(callback)
      callback(mysource->channel);

    return TRUE;
}
/*
當事件源不再被引用時,這個接口被回調
*/
static void finalize(GSource *source)
{
    MySource *mysource = (MySource *)source;

    if(mysource->channel)
      g_io_channel_unref(mysource->channel);
}

int main(int argc, char* argv[])
{
    GError *error = NULL;
    GMainLoop *loop = g_main_loop_new(NULL, FALSE);	// 從默認上下文獲取事件循環實例
    GSourceFuncs funcs = {prepare, check, dispatch, finalize};	// 聲明用戶定義的狀態機回調
    /*
	Glib允許用戶自己定義事件源,但需要把Glib的事件源作爲"基類",具體實現是把GSource
	作爲自定義事件源的第一個成員,在創建事件源時傳入狀態機回調函數和自定義事件源的結構體大小
	*/
    GSource *source = g_source_new(&funcs, sizeof(MySource));
    MySource *mysource = (MySource *)source;
	/*
	創建一個文件類型的GIOChannel,GIOChannel就是Glib對文件描述符的封裝,實現其平臺可移植性
	GIOChannel在所有Unix平臺上都可移植,在Windows平臺上部分可移植
	GIOChannel的fd類型可以是文件,pipe和socket
	*/
    if (!(mysource->channel = g_io_channel_new_file("test", "r", &error))) {
        if (error != NULL)
            g_print("Unable to get test file channel: %s\n", error->message);

        return -1;
    }
	/*獲取GIOChannel的fd,放到GPollFD的fd域中*/
    mysource->fd.fd = g_io_channel_unix_get_fd(mysource->channel);
    /*設置感興趣的文件狀態,這裏時文件可讀狀態*/
    mysource->fd.events = G_IO_IN;
    /*
    傳給poll的文件描述符結構體
	struct GPollFD {
  		gint		fd;			// 文件描述符
  		gushort 	events;		// 感興趣的文件狀態
  		gushort 	revents;	// 返回值,由內核設置
	};
	*/
    g_source_add_poll(source, &mysource->fd);	// 將文件描述符添加到事件源中
    g_source_set_callback(source, (GSourceFunc)watch, NULL, NULL);	// 設置事件源的回調函數
    /* 
    設置事件源優先級,如果多個事件源在同一個上下文,這個事件源都準備好了,優先級高的事件源會被Glib優先調度
    */
    g_source_set_priority(source, G_PRIORITY_DEFAULT_IDLE);	
    g_source_attach(source, NULL);	//將事件源添加到Glib的上下文,此處上下文爲NULL,表示默認的上下文
    g_source_unref(source);

    g_main_loop_run(loop);	// Glib開始執行默認上下文的事件循環

    g_main_context_unref(g_main_loop_get_context(loop));
    g_main_loop_unref(loop);

    return 0;
}

運行效果:
在這裏插入圖片描述

QEMU事件循環

事件循環初始化

  • qemu事件循環初始化遵循Glib接口,和普通應用初始化流程類似,在qemu_init_main_loop中實現,簡化版代碼如下:
static void
qemu_init_main_loop()
{
    GSource *src;

    qemu_aio_context = aio_context_new();		// 創建qemu定製的事件源qemu_aio_context
    gpollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD));                                                                                                                                                                                   

    src = aio_get_g_source(qemu_aio_context);	// 從定製事件源中獲取Glib原始的事件源
    g_source_set_name(src, "aio-context");		// 設置事件源名稱
    g_source_attach(src, NULL);    // 將事件源添加到Glib默認事件循環上下文
    g_source_unref(src);

    src = iohandler_get_g_source();	// 創建另一個定製的事件源 iohandler_ctx
    g_source_set_name(src, "io-handler");
    g_source_attach(src, NULL);    // 將事件源添加到Glib默認事件循環上下文
    g_source_unref(src);      
}

從上面代碼可知,qemu事件初始化創建了兩個事件源,分別是qemu_aio_contextiohandler_ctx,這兩個事件源被加在了Glib默認的事件循環上下文default GMainContext,我們知道一個上下文只在一個線程中運行,這兩個事件源同屬一個上下文,因此可以確認運行在同一個線程中。

狀態機回調函數的定製

  • qemu豐富了Glib的事件源,狀態機回調的實現邏輯變的複雜,但基本框架還是遵循Glib的接口。aio_context_new函數中的aio_source_funcs就是狀態機回調函數的聲明
static GSourceFuncs aio_source_funcs = {
    aio_ctx_prepare,
    aio_ctx_check,         
    aio_ctx_dispatch,      
    aio_ctx_finalize       
};

事件源的定製

  • qemu定製的事件源,不僅實現了對描述符的監聽,還實現了事件通知,時鐘事件源監聽和下半部。這些實現都體現在了描述事件源的結構體AioContext中。描述符的監聽由Glib的GSource實現,notifier實現了事件通知,tlg實現了時鐘事件源監聽,first_bh實現了下半部等,簡化版本的qemu事件源結構體如下:
struct AioContext { 
	GSource source;
	struct QEMUBH *first_bh; 
	EventNotifier notifier; 
	QEMUTimerListGroup tlg; 
}
  • qemu主線程在qemu_init_main_loop函數裏創建了運行在默認上下文的事件源qemu_aio_context,這個事件源,包含了qemu中絕大部分服務的fd,比如VNC server和QMP monitor服務端的socket等,事件源創建函數如下:
AioContext *aio_context_new(Error **errp)
{
    int ret;
    AioContext *ctx;

    ctx = (AioContext *) g_source_new(&aio_source_funcs, sizeof(AioContext));	// 事件源的創建,傳入的size是AioContext的大小
    aio_context_setup(ctx);
	/*初始化事件源的其它部分*/
    ret = event_notifier_init(&ctx->notifier, false);	// 初始化實現事件通知的成員notifier
    if (ret < 0) {
        error_setg_errno(errp, -ret, "Failed to initialize event notifier");
        goto fail;
    }
	......
}

poll機制的定製

  • 應用程序對文件描述符的讀寫,要麼設置爲阻塞,如果fd的某個狀態(比如可讀,可寫)沒有準備好,進程將永遠阻塞在這個IO上;要麼設置爲非阻塞IO(open時設置O_NONBLOCK),進程IO立即執行,如果fd沒有準備好,直接返回錯誤,表示如果繼續執行IO將阻塞。通常,這兩種方式滿足了大多數應用,但在更復雜的場景,不允許永遠阻塞的進程可能需要讀寫一個可能永遠阻塞進程的fd,這時可以使用內核提供的poll接口,它不直接讀寫IO,而是先探測fd的IO狀態是否準備好,這個探測可以設置超時時間,如果這段時間內IO狀態準備好了,poll接口立即返回,指示應用程序可以進行接下來的讀寫了,如果poll超過了設置的超時時間,但應用感興趣的IO狀態沒有準備好,也返回,同時把那些準備好的IO狀態返回給應用,應用程序可以根據內核返回的poll信息確定接下要做的事情,兩種情況,都不會使應用程序永遠阻塞在IO上,防止進程出現D狀態
  • qemu基於Glib事件循環的框架,定製了Glib事件循環的幾乎所有接口。最重要一點,qemu沒有調用Glib的event loop接口g_main_loop_run執行事件循環,而是直接調用了Glib提供的g_poll函數實現poll的功能,g_poll函數具有可移植性,在有poll接口的平臺上由poll模擬,在沒有poll接口的平臺上由select模擬。當用戶不想執行整個Glib的事件循環,又想實現阻塞一段時間的高級IO,就可以直接使用g_poll。
  • qemu代碼中可以看到它沒有調用g_main_loop_run執行Glib事件循環,而是顯式調用了while循環,再直接調用g_poll接口。這個高級定製使得qemu對Glib事件循環的使用看起來和普通應用程序有很大區別,至少,普通應用程序使用Glib時不會顯式執行while循環,只需要簡單地執行g_main_loop_run就可以了,g_poll傳入的是一個fd的數組和數組元素,因此它能夠同時poll多個fd,poll的時候是休眠的,一旦其中一個fd準備好,poll就會被喚醒,同時GPollFD.revent會被內核設置,表明fd的狀態。
  1. qemu主線程的while循環
    while (!main_loop_should_exit()) {
    	main_loop_wait(false);
    }
  1. while循環中對g_poll的顯式調用
/*
如果qemu配置了PPOLL,使用PPOLL實現poll探測,否則使用g_poll實現poll探測
*/
int qemu_poll_ns(GPollFD *fds, guint nfds, int64_t timeout)
{       
#ifdef CONFIG_PPOLL              
    if (timeout < 0) {	// 如果timeout小於0,如果IO沒有準備好,永遠阻塞
        return ppoll((struct pollfd *)fds, nfds, NULL, NULL);	
    } else {			// 否則,阻塞一段時間後返回
        struct timespec ts;
        int64_t tvsec = timeout / 1000000000LL;
        /* Avoid possibly overflowing and specifying a negative number of
         * seconds, which would turn a very long timeout into a busy-wait.
         */
        if (tvsec > (int64_t)INT32_MAX) {
            tvsec = INT32_MAX;
        }
        ts.tv_sec = tvsec;
        ts.tv_nsec = timeout % 1000000000LL;
        return ppoll((struct pollfd *)fds, nfds, &ts, NULL);
    }
#else   
    return g_poll(fds, nfds, qemu_timeout_ns_to_ms(timeout));
#endif  
} 

事件源回調函數的定製

  • qemu的事件源回調,拋棄了Glib提供的接口,而是通過事件源中自定義的aio_handlers部分實現的,aio_handlers是個鏈表頭,指向一個鏈表,這個鏈表的每個節點是個AioHandler結構體,維護了一個事件源的fd和對應的讀寫回調函數,如圖
    在這裏插入圖片描述
    簡化版本的AioHandler定義如下:
struct AioHandler {
    GPollFD pfd;		// 要poll的一個fd
    IOHandler *io_read;	// fd可讀時對應的回調
    IOHandler *io_write;// fd可寫時對應的回調
    void *opaque;		// 調用回調函數時傳入的參數
    QLIST_ENTRY(AioHandler) node;	// 鏈接到aio_handlers指向的鏈表
};
  • Glib的poll完成後需要執行check,檢查哪些事件源的fd準備好,只要aio_handlers指向的鏈表中,有一個fd準備好了,check就返回true,表示需要發起接下來的調度,簡化代碼如下:
  1. qemu定製的Glib check回調
static gboolean
aio_ctx_check(GSource *source)
{
    AioContext *ctx = (AioContext *) source;
    return aio_pending(ctx);
}
  1. 檢查鏈表中是否有準備好的fd,如果有,返回TRUE
static gboolean
aio_pending(AioContext *ctx)
{
    AioHandler *node;
    gboolean result = FALSE;

    QLIST_FOREACH(node, &ctx->aio_handlers, node) {
        int revents;

        revents = node->pfd.revents & node->pfd.events;
        if (revents & (G_IO_IN | G_IO_HUP | G_IO_ERR) && node->io_read) {
            result = TRUE;    // qemu關注的讀狀態已準備好
            break;            
        }
        if (revents & (G_IO_OUT | G_IO_ERR) && node->io_write) {                                                                                                                                                                             
            result = TRUE;    // qemu關注的寫狀態已準備好
            break;
        }                     
    }

    return result;            
}
  • Glib中如果check到有fd需要調度,直接執行qemu定值的dispatch接口,簡化的dispatch接口如下:
  1. qemu定製的Glib dispatch回調
static void
aio_dispatch(AioContext *ctx) 
{
    aio_dispatch_handlers(ctx);
}
  1. 檢查鏈表中哪些node需要執行dispatch,調用相應的讀寫回調函數
static gboolean
aio_dispatch_handlers(AioContext *ctx)
{
    AioHandler *node, *tmp;

    QLIST_FOREACH(node, &ctx->aio_handlers, node) {
        int revents;

        revents = node->pfd.revents & node->pfd.events;
        node->pfd.revents = 0;

        if ((revents & (G_IO_IN | G_IO_HUP | G_IO_ERR)) &&
            node->io_read) {  
            node->io_read(node->opaque);
        }
        if ((revents & (G_IO_OUT | G_IO_ERR)) &&
            node->io_write) { 
            node->io_write(node->opaque);
        }
    }                         

    return TRUE;
}

簡化版本代碼

  • 簡化版本的代碼基於qemu-3.1.91,實現了對一個test普通文件的監聽,當test文件fd的輸入狀態準備好時,demo檢查其是否有輸入,如果有輸入,讀出其內容,打印內容和內容的長度
static void
fd_read_cb(void *opaque)
{
    GIOChannel *channel = opaque;
    gsize len = 0;
    gchar *buffer = NULL;

    g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);
    if(len > 0) {
        g_print("len: %d, buffer: %s\n", len, buffer);
    }
    g_free(buffer);
}

代碼地址:qemu_main_loop

  • demo運行效果:
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章