最近因為工作的原因,後台伺服器的壓力越來越大,需要對項目背景伺服器進行重構,對已有的伺服器的通訊模組進行重構,增加其效能. libevent產生的背景 通常我們在建立伺服器的處理模型的時候,主要是下面集中模型; (1) a new Connection 進來,用 fork() 產生一個 Process 處理。 (2) a new Connection 進來,用 pthread_create() 產生一個 Thread 處理。 (3) a new Connection 進來,丟入 Event-based Array,由 Main Process 以 Nonblocking 的方式處理所有的 I/O。 這三種方法當然也都有各自的缺點: 用 fork() 的問題在於每一個 Connection 進來時的成本太高,如果同時接入的並發串連數太多容易進程數量很多,進程之間的切換開銷會很大,同時對於老的核心(Linux)會產生雪崩效應。 用 Multi-thread 的問題在於 Thread-safe 與 Deadlock 問題難以解決,另外有 Memory-leak 的問題要處理,這個問題對於很多程式員來說無異於惡夢,尤其是對於連續伺服器的伺服器程式更是不可以接受。 如果才用 Event-based 的方式在於實做上不好寫,尤其是要注意到事件產生時必須 Nonblocking,於是會需要實做 Buffering 的問題,而 Multi-thread 所會遇到的 Memory-leak 問題在這邊會更嚴重。而在多 CPU 的系統上沒有辦法使用到所有的 CPU resource。 針對上面存在的問題,通常採用的方法有: 以 Poll 的方式解決:當一個 Process 處理完一個 Connection 後,不直接死掉,而繼續回到 accept() 的狀態繼續處理,但這樣會遇到 Memory-leak 的問題,於是採用這種方式的人通常會再加上「處理過 N 個 Connection 後死掉,由 Parent Process 再 fork() 一隻新的」。最有名的例子是 Apache 1.3伺服器,大家可以參考其原始碼的實現。 hread-safe 的問題可以尋找其他 Thread-safe Library 直接使用。Memory-leak 的問題可以試著透過 Garbage Collection Library 分析出來。Apache 2.0 的 Thread MPM 就是使用這個模式。 然而,目前高效率的 Server 都偏好採用 Event-based,一方面是沒有 Create Process/Thread 所造成的 Overhead,另外一方面是不需要透過 Shared Memory 或是 Mutex 在不同的 Process/Thread 之間交換資料。然而,Event-based 在實做上的幾個複雜的地方在於: select() 與 poll() 的效率過慢,造成每次要判斷「有哪些 Event 發生」這件事情的成本很高,這在 BSD 支援 kqueue()、Linux 支援 epoll()、Solaris 支援 /dev/poll 後就解決了,在Windows平台上通過完成連接埠的方式解決了.但這兩組 Function 都不是 Standard,於是在不同的平台上就必須再改一次。 對於非阻塞的IO模型, 因為 Nonblocking,所以在 write() 或是 send() 時滿了需要自己 Buffering。 因為 Nonblocking,所以不能使用 fgets() 或是其他類似的 function,於是需要自己刻一個 Nonblocking 的 fgets()。但是使用者所丟過來的資料又不能保證在一次 read() 或 recv() 就有一行,於是要自己做 Buffering。實際上這三件事情在 libevent 都有 Library 處理掉了. libevent 是一個事件觸發的網路程式庫,適用於windows、linux、bsd等多種平台,內部使用select、epoll、kqueue等系統調用管理事件機制。著名的用於apache的php緩衝庫memcached據說也是libevent based,而且libevent在使用上可以做到跨平台,如果你將要開發的應用程式需要支援以上所列出的平台中的兩個以上,那麼強烈建議你採用這個庫,即使你的應用程式只需要支援一個平台,選擇libevent也是有好處的,因為它可以根據編譯/運行環境切換底層的事件驅動機制,這既能充分發揮系統的效能,又增加了軟體的可移植性。它封裝並且隔離了事件驅動的底層機制,除了一般的檔案描述符讀寫操作外,它還提供有讀寫逾時、定時器和訊號回調,另外,它還允許為事件設定不同的優先順序,目前的版本的libevent還提供dns和http協議的非同步封裝,這一切都讓這個庫尤其適合於事件驅動應用程式的開發。 下面介紹libevent實現的架構 原文請參考:libevent官方網址: http://www.monkey.org/~provos/libevent/比較好的文檔: http://unx.ca/log/category/libevent/ http://tb.blog.csdn.net/TrackBack.aspx?PostId=1808095 libenvent庫的代碼結構可以大概分成幾個模組: 事件處理架構 事件引擎模組 Buffer管理模組 訊號處理模組 1. 事件處理架構
1.1 event_init() 初始化 首先要隆重介紹event_base對象: struct event_base { const struct eventop *evsel; void *evbase; int event_count; /* counts number of total events */ int event_count_active; /* counts number of active events */ int event_gotterm; /* Set to terminate loop */ /* active event management */ struct event_list **activequeues; int nactivequeues; struct event_list eventqueue; struct timeval event_tv; RB_HEAD(event_tree, event) timetree; }; event_base對象整合了事件處理的一些全域變數, 角色是event對象的"總管家", 他包括了事件引擎函數對象(evsel, evbase), 當前入列事件列表(event_count, event_count_active, eventqueue), 全域終止訊號(event_gotterm), 活躍事件列表(avtivequeues), 事件隊列樹(timetree)...初始化時建立event_base對象, 選擇 當前OS支援的事件引擎(epoll, poll, select...)並初始化, 建立全域訊號隊列(signalqueue), 活躍隊列的記憶體配置( 根據設定的priority個數,預設為1). 1.2 event_set() 事件定義 event_set來設定event對象,包括所有者event_base對象, fd, 事件(EV_READ| EV_WRITE), 回掉函數和參數,事件優先順序是當前event_base的中間層級(current_base->nactivequeues/2). event對象的定義見下: struct event { TAILQ_ENTRY (event) ev_next; TAILQ_ENTRY (event) ev_active_next; TAILQ_ENTRY (event) ev_signal_next; RB_ENTRY (event) ev_timeout_node; struct event_base *ev_base; int ev_fd; short ev_events; short ev_ncalls; short *ev_pncalls; /* Allows deletes in callback */ struct timeval ev_timeout; int ev_pri; /* smaller numbers are higher priority */ void (*ev_callback)(int, short, void *arg); void *ev_arg; int ev_res; /* result passed to event callback */ int ev_flags; }; 1.3 event_add() 事件添加: int event_add(struct event *ev, struct timeval *tv) 這個介面有兩個參數, 第一個是要添加的事件, 第二個參數作為事件的逾時值(timer). 如果該值非NULL, 在添加本事件的同時添加逾時事件(EV_TIMEOUT)到時間隊列樹(timetree), 根據事件類型處理如下: EV_READ => EVLIST_INSERTED => eventqueue EV_WRITE => EVLIST_INSERTED => eventqueue EV_TIMEOUT => EVLIST_TIMEOUT => timetree EV_SIGNAL => EVLIST_SIGNAL => signalqueue 1.4 event_base_loop() 事件處理主迴圈 這裡是事件的主迴圈,只要flags不是設定為EVLOOP_NONBLOCK, 該函數就會一直迴圈監聽事件/處理事件. 每次迴圈過程中, 都會處理當前觸發(活躍)事件: (a). 檢測當前是否有訊號處理(gotterm, gotsig), 這些都是全域參數,不適合多線程 (b). 時間更新,找到離當前最近的時間事件, 得到相對逾時事件tv (c). 呼叫事件引擎的dispatch wait事件觸發, 逾時值為tv, 觸發事件添加到activequeues (d). 處理活躍事件, 調用caller的callbacks (event_process_acitve) 2. 事件引擎模組 : Linux下有多種I/O複用機制, .來處理多路事件監聽, 常見的有epoll, poll, select, 按照優先順序排下來為: evport kqueue epoll devpoll rtsig poll select 在event_init()選擇事件引擎時,按照優先順序從上向下檢測, 如果檢測成功,當前引擎被選中.每個引擎需要定義幾個處理函數,以epoll為例: struct eventop epollops = { "epoll", epoll_init, epoll_add, epoll_del, epoll_recalc, epoll_dispatch, epoll_dealloc }; 3. Buffer管理模組: libevent定義了自己的buffer管理機制evbuffer, 支援多種類型資料的read/write功能, 包括不定長字串,buffer中記憶體採用預分配/按需分配結合的方式, 可以比較方便的管理多個資料結構映射到記憶體buffer. 需要拉出來介紹的是evbuffer_expand()函數, 當內部記憶體不夠時,需要expand, 這裡採用預分配的方式,如果需要長度libevent庫的具體使用方法 直接寫一個很簡單的 Time Server 來當作例子:當你連上去以後 Server 端直接提供時間,然後結束連線。event_init() 表示初始化 libevent 所使用到的變數。event_set(&ev, s, EV_READ | EV_PERSIST, connection_accept, &ev) 把 s 這個 File Description 放入 ev (第一個參數與第二個參數),並且告知當事件 (第三個參數的 EV_READ) 發生時要呼叫 connection_accept() (第四個參數),呼叫時要把 ev 當作參數丟進去 (第五個參數)。其中的 EV_PERSIST 表示當呼叫進去的時候不要把這個 event 拿掉 (繼續保留在 Event Queue 裡面),這點可以跟 connection_accept() 內在註冊 connection_time() 的代碼做比較。而 event_add(&ev, NULL) 就是把 ev 註冊到 event queue 裡面,第二個參數指定的是 Timeout 時間,設定成 NULL 表示忽略這項設定。 注:這段代碼來自於網路,雖然很粗糙,但是對libevent的使用方法已經說明的很清楚了. 附源碼:使用方法 #include #include #include #include #include #include void connection_time(int fd, short event, struct event *arg) { char buf[32]; struct tm t; time_t now; time(&now); localtime_r(&now, &t); asctime_r(&t, buf); write(fd, buf, strlen(buf)); shutdown(fd, SHUT_RDWR); free(arg); } void connection_accept(int fd, short event, void *arg) { /* for debugging */ fprintf(stderr, "%s(): fd = %d, event = %d.\n", __func__, fd, event); /* Accept a new connection. */ struct sockaddr_in s_in; socklen_t len = sizeof(s_in); int ns = accept(fd, (struct sockaddr *) &s_in, &len); if (ns #include #include void printbuf(struct evbuffer *evbuf) { for (;;) { char *buf = evbuffer_readline(evbuf); printf("* buf = %p, the string = \"\e[1;33m%s\e[m\"\n", buf, buf); if (buf == NULL) break; free(buf); } } int main(void) { struct evbuffer *evbuf; evbuf = evbuffer_new(); if (evbuf == NULL) { fprintf(stderr, "%s(): evbuffer_new() failed.\n", __func__); exit(1); } /* Add "gslin" into buffer. */ u_char *buf1 = "gslin"; printf("* Add \"\e[1;33m%s\e[m\".\n", buf1); evbuffer_add(evbuf, buf1, strlen(buf1)); printbuf(evbuf); u_char *buf2 = " is reading.\nAnd he is at home.\nLast."; printf("* Add \"\e[1;33m%s\e[m\".\n", buf2); evbuffer_add(evbuf, buf2, strlen(buf2)); printbuf(evbuf); evbuffer_free(evbuf); } 最後的 event_dispatch() 表示進入 event loop,當 Queue 裡面的任何一個 File Description 發生事件的時候就會進入 callback function 執行。 |