Spice網絡事件處理模型

作者:“達沃時代”    原文鏈接:http://www.cnblogs.com/D-Tec/archive/2013/03/21/2973339.html

〇、概述

網絡事件處理是libspice設計中最關鍵的部分,可以說是整個Spice的骨架,用以支撐Spice的運行,是理解Spice運作方式的切入口之一(VDI是另一個閱讀代碼的切入口)。Spice的server和client通信方式採用了三種框架:

1、 Qemu的main函數中採用非阻塞select方式輪訓網絡事件

2、 Libspice中有一個專門的線程,採用非阻塞epoll模型監聽網絡事件

3、 Qemu中採用定時器方式進行網絡數據發送

一、select模型處理

   Spice中最基本的網絡事件處理均採用select模型,即大部分的網絡事件是在Qemu的主函數中進行捕獲的。直接看代碼:

void main_loop_wait(int nonblocking)

{

    IOHandlerRecord *ioh;

    fd_set rfds, wfds, xfds;

    int ret, nfds;

 

    nfds = -1;

    FD_ZERO(&rfds);

    FD_ZERO(&wfds);

    FD_ZERO(&xfds);

 

    // FD_SET 對隊列中的所有節點進行處理

    QLIST_FOREACH(ioh, &io_handlers, next) {

        if (ioh->deleted)

            continue;

        FD_SET(ioh->fd, &rfds);

        FD_SET(ioh->fd, &wfds);

    }

 

       // select

    ret = select(nfds + 1, &rfds, &wfds, &xfds, &tv);

 

    // 調用節點對應的回調函數進行網絡事件處理

    if (ret > 0) {

        IOHandlerRecord *pioh;

 

        QLIST_FOREACH_SAFE(ioh, &io_handlers, next, pioh) {

            if (ioh->fd_read && FD_ISSET(ioh->fd, &rfds)) {

                ioh->fd_read(ioh->opaque);

            }

            if (ioh->fd_write && FD_ISSET(ioh->fd, &wfds)) {

                ioh->fd_write(ioh->opaque);

            }

        }

}

qemu_run_all_timers();

}

以上代碼遵循了select模型的基本處理步驟:FD_SET、select、process,所以非常容易理解。該代碼的獨特之處在於其實現方式支持動態管理網絡連接,思想很簡單:通過維護一個全局的網絡連接列表io_handlers,每次select前都遍歷此列表來獲取需要查詢的網絡連接套接字。同時,該列表的每個元素還記錄了針對該套接字的讀寫處理函數,其元素類型聲明如下:

typedef void IOReadHandler(void *opaque, const uint8_t *buf, int size);

typedef int IOCanReadHandler(void *opaque);

typedef void IOHandler(void *opaque);

typedef struct IOHandlerRecord {

    int fd;                         // socket 描述符

    IOCanReadHandler *fd_read_poll; 

    IOHandler *fd_read;             // read 事件處理回調函數

    IOHandler *fd_write;            // write 事件處理回調函數

    int deleted;                    // 刪除標記

    void *opaque;

    struct pollfd *ufd;

    QLIST_ENTRY(IOHandlerRecord) next; // 鏈表實現

} IOHandlerRecord;

 

io_handlers是一個IOHandlerRecord類型的元素的List頭指針。

    當有新的網絡連接建立後,只需要初始化一個IOHandlerRecord對象,將其插入到列表中即可。Qemu實現了一個共用函數來完成新連接對象的初始化和插入隊列的動作:

int qemu_set_fd_handler2(int fd, IOCanReadHandler *fd_read_poll,

IOHandler *fd_read, IOHandler *fd_write, void *opaque)

{  

    // 新建一個節點對象,將其插入到List中

IOHandlerRecord *ioh;

    ioh = qemu_mallocz(sizeof(IOHandlerRecord));

    QLIST_INSERT_HEAD(&io_handlers, ioh, next);

        ioh->fd = fd;

        ioh->fd_read_poll = fd_read_poll;

        ioh->fd_read = fd_read;

        ioh->fd_write = fd_write;

        ioh->opaque = opaque;

        ioh->deleted = 0;

 

    return 0;

}

    通過以上封裝,就可以將網絡事件套接字的管理和網絡事件的處理分離開來,管理的部分如上所述是一個統一的流程,不會因爲具體業務的改變而改變。以Spice爲例,Qemu中只需要負責網絡事件的監聽,具體的事件處理則交由此事件的註冊者負責實現。

    網絡事件的註冊則又經過一層封裝,最終我們看到的就是CoreInterface初始化中被賦值給core->watch_add函數指針的對應函數,封裝如下:

    static SpiceWatch *watch_add(int fd, int event_mask, SpiceWatchFunc func, void *opaque)

{

    SpiceWatch *watch;

 

    watch = qemu_mallocz(sizeof(*watch));

    watch->fd     = fd;

    watch->func   = func;

    watch->opaque = opaque;

    QTAILQ_INSERT_TAIL(&watches, watch, next);

 

    {

      IOHandler *on_read = NULL;

      IOHandler *on_write = NULL;

 

      watch->event_mask = event_mask;

      if (watch->event_mask & SPICE_WATCH_EVENT_READ) {

        on_read = watch_read; //內部調用 func(SPICE_WATCH_EVENT_READ);

      }

      if (watch->event_mask & SPICE_WATCH_EVENT_WRITE) {

        on_read = watch_write; //內部調用 func(SPICE_WATCH_EVENT_WRITE);

      }

// 下面的函數實際上就是封裝了qemu_set_fd_handler2

      qemu_set_fd_handler(watch->fd, on_read, on_write, watch);

        }

    return watch;

}

    經過以上封裝之後,libspice的實現者就可以專心處理自己的事情,不需要再關心網絡事件如何通知給自己的問題了。如果需要增加新的業務流程,比如增加遠程USB設備支持,只需要將所有處理函數在libspice中實現好,客戶端的USB模塊發起網絡連接後,libspice調用CoreInterface的watch_add回調,將此連接以及對應的處理函數註冊到Qemu中即可。

    另外,要將Spice移植到其他平臺,若要保持libSpice代碼可以被重用,Qemu中網絡處理部分是必須移植的。以上封裝的實現使得網絡處理的移植非常簡單。

二、epoll模型處理

    該模型僅在顯示處理線程中使用,用以處理進程內的網絡消息。多次提到,顯示處理在libspice中是通過一個單獨的線程來實現的,這就涉及到多線程之間的通信問題。Spice通過socket pair的方式在進程內部創建了一個通信管道,pair的一端暴露給要與當前線程通信的模塊,這些模塊包括Qemu的虛擬顯卡設備、libspice的消息dispatcher等;另一端則留給當前線程用來進行數據收發。此工作線程實現框架如下:

void *red_worker_main(void *arg)

{

    for (;;) {

        struct epoll_event events[MAX_EPOLL_SOURCES];

        int num_events;

        struct epoll_event *event;

        struct epoll_event *end;

        // 等待網絡event

        num_events = epoll_wait(worker.epoll, events, MAX_EPOLL_SOURCES, worker.epoll_timeout);

 

        worker.epoll_timeout = INF_EPOLL_WAIT;

        // 處理所有的event

for (event = events, end = event + num_events; event < end; event++) {

            EventListener *evt_listener = (EventListener *)event->data.ptr;

 

            if (evt_listener->refs > 1) {

                evt_listener->action(evt_listener, event->events);

                if (--evt_listener->refs) {

                    continue;

                }

            }

            free(evt_listener); // refs == 0 , release it!

        }

 

        if (worker.running) {

            int ring_is_empty;

            red_process_cursor(&worker, MAX_PIPE_SIZE, &ring_is_empty);

            red_process_commands(&worker, MAX_PIPE_SIZE, &ring_is_empty);

        }

        red_push(&worker);

    }

    red_printf("exit");

    return 0;

}

三、Timer定時

    定時器是Qemu的另一個比較關鍵的事件觸發機制,也是影響代碼閱讀的禍端之一。回到上面的main_loop_wait函數,最後有一句qemu_run_all_timers();該函數會遍歷系統中的所有定時器,以執行到時定時器的觸發函數。main_loop_wait函數則被封裝在下面的main_loop函數中:

static void main_loop(void)

{

    for (;;) {

        do {

            bool nonblocking = false;

            main_loop_wait(nonblocking);

        } while (vm_can_run());

 

        // ……

}

    即:系統會不停的調用main_loop_wait函數來輪訓網絡事件和定時器。以上說明了Qemu定時器的觸發機制,下面來看定時器的具體實現和使用方式。

    Qemu的qemu-timer.c專門用來實現定時器的代碼,裏面維護了一個全局的鏈表數組active_timers,該數組用來保存系統中各種不同類型的timer鏈表頭指針,類似一個哈希表,所有timer鏈表都是按照每個timer的被激活時間排序過的,因此可以減少查詢時間,最大限度的提高timer執行精確度。鏈表中timer節點數據結構定義如下:

struct QEMUTimer {

    QEMUClock *clock;     // timer 狀態及類型

    int64_t expire_time;  // timer 激活時間

    QEMUTimerCB *cb;      // timer 激活時要執行的回調函數指針

    void *opaque;         // 用戶數據,用作timer回調函數的入口參數

    struct QEMUTimer *next;

};

通過qemu_new_timer接口增加新的timer,但new操作並不把timer插入到全局數組中,只有當調用qemu_mod_timer時,才真正將timer插入鏈表中。通過以上方式註冊的timer通常只會被執行一次,若要實現週期性定時器,只需要在timer的回調函數實現中將自己再次加入到timer鏈表中即可。CoreInterface的另外一組函數指針就是關於Timer的。這個timer應該是比較低效的,但平臺依賴性要求很低。

 

某些網絡連接建立起來以後,數據發送是通過Timer方式定時處理的,最爲典型的就是音頻數據的產生及往客戶端推送。音頻設備初始化後,會立即註冊一個週期性定時器,將音頻數據通過網絡連接循環發往客戶端。

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