一般來講我喜歡Linux更甚於BSD系統,可是我真的想在Linux上擁有BSD的kqueue功能。html
假設你有一個簡單的web服務器,而且那裏已經打開了兩個socket鏈接。當服務器從兩個鏈接那裏都收到Http請求的時候,它應該返回一個Http響應給客戶端。可是你無法知道那個客戶端先發送的消息和何時發送的。BSD套接字接口的阻塞行爲意味着,若是你在一個鏈接上調用recv()函數,你就沒辦法去響應另一個鏈接上的請求。這時你就須要I/O複用技術。 I/O複用技術的一個直接方式是讓每一個鏈接都擁有一個進程/線程,這樣鏈接上的阻塞行爲就不會相互影響。這樣,你就把全部繁瑣的調度/複用問題交給了操做系統內核。這樣的多線程架構伴隨着的是高昂資源消耗。維護大量的線程對內核來講沒有什麼必要。每一個鏈接上的獨立棧不只要增長內存痕跡,同時也下降了CPU本地緩存能力。 那麼咱們如何不使用線程-鏈接模式來實現I/O複用技術呢?你能夠經過一個簡單的忙等輪詢來實現,即在每一個鏈接上進行非阻塞的套接字操做,但這種行爲過於的浪費。咱們所要知道的只不過是哪一個套接字已經就緒。所以系統內核爲應用與內核之間提供了一個單獨的通道,這個通道在你的套接字變爲就緒時會發出通知。這就是基於準備就緒模式下的select()/poll()工做模式。linux
select()和poll()的工做方式很是相似。讓咱們先快速看一下select()函數web
select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout)
調用select()函數,你的應用程序須要提供三個興趣集:r,w和e。每個集合都是一個文件描述符的位圖。例如,若是你關注從文件描述符6裏面讀取數據,那麼r集合裏面的第6個字節位就設成1。這個調用會被阻塞直到興趣集中有更多的文件描述符就緒,所以你能夠操縱這些文件描述符而不會被阻塞。在返回後,系統內核會覆寫整個位圖來指明哪些文件描述符已經就緒。 從擴展性角度,咱們能夠找到4個問題:編程
poll()的設計意圖就是解決這些問題。api
poll(struct pollfd *fds, int nfds, int timeout) struct pollfd { int fd; short events; short revents; }
poll()的實現不依賴於位圖,而是用文件描述符數組(這樣第一個問題就解決了)。經過對興趣事件與結果事件採起分離字段,第二個問題也得以解決,由於用戶程序能夠維護並重用這個數組。若是poll函數可以拆分該數組而不是字段,那麼第三個問題也就引刃而解。第四個問題是繼承而來的並且是不可避免,由於poll()和select()都是無狀態的,內核不會在內部維護興趣集狀態。數組
若是你的網絡服務器須要維護一個相對較小的鏈接數(如100個),而且鏈接率也比較低(如每秒100個), 那麼poll()和select()就足夠了。也許你根本不須要爲事件驅動編程而苦惱,只要多進程/多線程架構就能夠了。若是性能不是你關注的重點,那麼靈活性與容易開發纔是關鍵。Apache web服務器就是一個典型的例子。緩存
可是,若是你的服務器程序是網絡資源敏感的(如1000個併發鏈接數或者一個較高的鏈接率),那麼你就要真的在乎性能問題了。這種狀況一般被稱爲c10k問題。你的網絡服務器將很難執行任何有用的東西,除了在這樣的高負荷下浪費寶貴的CPU週期。服務器
假設這裏有10000併發鏈接。通常來講,只有少許的文件描述符被使用,如10個已經讀就緒。那麼每次poll()/select()被調用,就有9990個文件描述符被毫無心義的拷貝和掃描。網絡
正如更早時候提到過的,這個問題是因爲select()/poll()接口的無狀態產生的。Banga et al的論文(發佈於USENIX ATC 1999)提供了一個新的建議:狀態相關興趣集。經過在內核內部維護興趣集的狀態,來取代每次調用都要提供整個興趣集這樣的方式。在decalre_interest()調用之上,內核持續的更新興趣集。用戶程序經過調用get_next_event()函數來分發事件。多線程
靈感一般來自於研究成果,Linux和Free BSD都有它們本身的實現, 分別是epoll和kqueue。但這又意味着缺乏了可移植性,一個基於epoll的程序是沒法跑在Free BSD系統上的。有一種說法是kqueue技術上比epoll更優,因此看起來epoll也沒有存在的理由了。
epoll接口由3個調用組成:
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_ctl()和epoll_ctl()本質上是分別對應到declare_interest()和get_next_event() 函數的。epoll_create()建立一個相似於文件描述符的上下文,這個上下文其實暗指進程的上下文。 從內部機制來講,epoll在Linux內核中的實現並不是很是不一樣於select()/poll()的實現。惟一不一樣的地方就是是否狀態相關。由於本質上來講它們的設計目標是同樣的(基於套接字/管道的事件複用技術)。查看Linux分支樹種的源代碼文件fs/select.c(對應select和poll)和fs/eventpoll.c(對應epoll)能夠獲得更多的信息。 你也能夠從這裏找到Linus Torvalds對於epoll的早期一些想法。
如epoll那樣,kqueue一樣支持每一個進程中有多個上下文(興趣集)。kqueue()函數行爲有點相似於epoll_create()。可是,kevent()卻集成了epoll_ctl()(用於調整興趣集)和epoll_wait()(獲取事件) 的角色。
int kqueue(void); int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);
事實上,kqueue從易於編程角度來看相比epoll要更復雜一些。這是由於kqueue設計更抽象一些,目的更寬泛。讓咱們來看一下kevent結構體:
struct kevent { uintptr_t ident; /* 事件標識 */ int16_t filter; /* 事件過濾器 */ uint16_t flags; /* 通用標記 */ uint32_t fflags; /* 特定過濾器標記 */ intptr_t data; /* 特定過濾器數據 */ void *udata; /* 不透明的用戶數據標識 */ };
這些字段的細節已經超出了本文的範圍,但你可能已經注意到了這裏沒有顯式的文件描述符字段。這是由於kqueue設計的目的並不是是爲了替代基於套接字事件複用技術的select()/poll(),而是提供通常化的機制來處理多種操做系統事件。
過濾器字段指明瞭內核事件類型。若是它是EVFILT_READ或EVFILT_WRITE,kqueue就與epoll是同樣的。這種狀況下,ident字段表現爲一個文件描述符。ident字段也可能表現爲其餘類型事件的標識,如進程號和信號數目,這取決於過濾器類型。更多的細節能夠從man手冊和這篇文檔裏找到。
從性能角度講,epoll存在一個設計上的缺陷;它不能在單次系統調用中屢次更新興趣集。當你的興趣集中有100個文件描述符須要更新狀態時,你不得不調用100次epoll_ctl()函數。性能降級在過渡的系統調用時表現的很是明顯,這篇文章有作解釋。我猜這是Banga et al原來工做的遺留,正如declare_interest()只支持一次調用一次更新那樣。相對的,你能夠在一次的kevent調用中指定進行屢次興趣集更新。
另外一個問題,在我看了更重要一些,一樣也是epoll的一個限制。它的設計目的是爲了提升select()/poll()的性能,epoll只能基於文件描述符工做。這有什麼問題嗎? 一個常見的說法是「在unix中,全部東西都是文件」。大部分狀況都是對的,但並不老是這樣。例如時鐘就不是,信號也不是,信號量也不是,包括進程也不是。(在Linux中)網絡設備也不是文件。在類Unix系統中有好多事物都不是文件。你沒法對這些事物採用select()/poll()/epoll()的事件複用技術。典型的網絡服務器管理不少類型的資源,除了套接字外。你可能想經過一個單一的接口來管理它們,可是你作不到。爲了不這個問題,Linux提供了不少補充性質的系統調用,如signalfd(),eventfd()和timerfd_create()來轉換非文件類型到文件描述符,這樣你就可使用epoll了。可是看起來不那麼的優雅...你真的想讓用一個單獨的系統調用來處理每一種資源類型嗎? 在kqueue中,多才多藝的kevent結構體支持多種非文件事件。例如,你的程序能夠得到一個子進程退出事件通知(經過設置filter = EVFILT_PROC, ident = pid, 和fflags = NOTE_EXIT)。即使有些資源或事件不被當前版本的內核支持,它們也會在未來的內核中被支持,同時還不用修改任何API接口。
最後一個問題是epoll並不支持全部的文件描述符;select()/poll()/epoll()不能工做在常規的磁盤文件上。這是由於epoll有一個強烈基於準備就緒模型的假設前提。你監視的是準備就緒的套接字,所以套接字上的順序IO調用不會發生阻塞。可是磁盤文件並不符合這種模型,由於它們老是處於就緒狀態。 磁盤I/O只有在數據沒有被緩存到內存時會發生阻塞,而不是由於客戶端沒發送消息。磁盤文件的模型是完成通知模型。在這樣的模型裏,你只是產生I/O操縱,而後等待完成通知。kqueue支持這種方式,經過設置EVFILT_AIO 過濾器類型來關聯到 POSIX AIO功能上,諸如aio_read()。在Linux中,你只能祈禱由於緩存命中率高而磁盤發生不阻塞(這種狀況在一般的網絡服務器上是個彩蛋),或者經過分離線程來使得磁盤I/O阻塞不會影響網絡套接字的處理(如FLASH架構)。
在咱們以前的文章中,咱們建議了一種新的編程接口:MegaPipe。它是徹底基於完成通知模型的,可用於磁盤文件和非磁盤文件。 最後原文在這裏