作者:“達沃時代” 原文鏈接: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方式定時處理的,最爲典型的就是音頻數據的產生及往客戶端推送。音頻設備初始化後,會立即註冊一個週期性定時器,將音頻數據通過網絡連接循環發往客戶端。