IO復用
IO復用簡單介紹
IO復用使得程序能同一時候監聽多個文件描寫敘述符。這對提高程序的性能至關重要。通常。網絡程序在下列情況下須要使用IO復用技術:- client程序要同一時候處理多個socket。
- client程序要同一時候處理用戶輸入和網絡連接。
- TCPserver同一時候處理監聽socket和連接socket。
- server要同一時候處理TCP請求和UDP請求。
Linux下實現IO復用的系統調用主要有select、poll和epoll。
select系統調用
select系統調用的用途是:在一段指定時間內,監聽用戶感興趣的文件描寫敘述符上的可讀、可寫和異常事件。select API
select系統調用的原型例如以下:#include <sys/select.h> #include <sys/time.h> int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout);
1)maxfd參數指定被監聽的文件描寫敘述符的總數。它通常被設置為select監聽的全部描寫敘述符中的最大值加1,由於文件描寫敘述符是從0開始計數的。
2)readfds、writefds和exceptfds參數分別指向可讀、可寫和異常等事件相應的文件描寫敘述符中集合。應用程序調用select函數時。通過這3個參數傳入自己感興趣的文件描寫敘述符。
select調用返回時。內核將改動它們來通知應用程序哪些文件描寫敘述符已經就緒。
這3個參數是fd_set結構體指針類型。
因為位操作過於繁瑣。我們應該使用以下的一系列宏來訪問fd_set結構體中的位:
void FD_CLR(int fd, fd_set *fdset) /* 清除fdse全部位.*/ int FD_ISSET(int fd, fd_set *fdset) /* 測試fdset的位fd是否被設置 */ void FD_SET(int fd, fd_set *fdset) /* 設置fdset的位fd */ void FD_ZERO(fd_set *fdset) /* 清除fdset的位fd */
3)timeout參數用來設置select函數的超時時間。
它是一個timeval結構類型的指針,採用指針參數是由於內核將改動它以告訴程序select等待了多久。只是我們不能全然信任select調用返回後的timeout值,比方調用失敗時timeout值是不確定的。timeout結構體的定義例如以下:
struct timeval { long tv_sec; // seconds long tv_usec; // and microseconds };
由上定義可知,select給我們提供了一個微秒級的定時方式。假設給timeout變量的tv_sec成員和tv_usec成員都傳遞0,則select將馬上返回。
假設給timeout傳遞NULL,則select將一直堵塞,直到某個文件描寫敘述符就緒。 select成功時返回就緒(可讀、可寫和異常)文件描寫敘述符的總數。
假設在超時時間內沒有不論什麽文件描寫敘述符就緒,select將返回0.select失敗時返回-1並設置errno。
假設在select等待期間。程序收到信號,則select馬上返回-1,並設置errno為EINTR。
文件描寫敘述符就緒條件
哪些情況下文件描寫敘述符能夠被覺得是可讀、可寫或異常,對於select的使用很關鍵。在網絡編程中。下列情況下socket可讀:- socket內核接收緩存區中的字節數大於或等於其低水位標記SO_RCVLOWAT。此時我們能夠無堵塞地讀該socket,而且讀操作返回的字節數大於0.
- socket通信的對方關閉連接。
此時對改socket的讀操作將返回0.
- 監聽socket上有新的連接請求。
- socket上有未處理的錯誤。此時我們能夠使用getsockopt來讀取和清除該錯誤。
- socket內核發送緩存區中的可用字節數大於或等於其低水位標記SO_SNDLOWAT。
此時我們能夠無堵塞地寫該socket,而且寫操作返回的字節數大於0.
- socket的寫操作被關閉。對寫操作被關閉的socket運行寫操作將觸發一個SIGPIPE信號。
- socket使用非堵塞connect連接成功或者失敗(超時)之後。
- socket上有未處理的錯誤。此時我們能夠使用getsockopt來讀取和清除該錯誤。
poll系統調用
poll系統調用和select類似,也是在指定時間被輪詢一定數量的文件描寫敘述符。以測試當中是否有就緒者。poll的原型例如以下:#include <poll.h> int poll(struct pollfd *fd, nfds_t nfds, int timeout);
1)fds參數是一個pollfd結構類型的數組,它指定全部我們感興趣的文件描寫敘述符上發生的可讀、可寫和異常等事件。
pollfd結構體的定義例如以下:
struct pollfd { int fd; /* 文件描寫敘述符 */ short events; /* 等待的事件 */ short revents; /* 實際發生了的事件 */ };
當中,fd成員指定文件描寫敘述符。events成員告訴poll監聽fd上的哪些事件。它是一系列事件的按位或。revents成員則由內核改動。以通知應用程序fd上實際發生了哪些事件。
POLLIN 有數據可讀。
POLLRDNORM 有普通數據可讀。
POLLRDBAND 有優先數據可讀。
POLLPRI 有緊迫數據可讀。
POLLOUT 寫數據不會導致堵塞。
POLLWRNORM 寫普通數據不會導致堵塞。
POLLWRBAND 寫優先數據不會導致堵塞。
POLLMSGSIGPOLL 消息可用。
此外,revents域中還可能返回下列事件:
POLLER 指定的文件描寫敘述符錯誤發生。
POLLHUP 指定的文件描寫敘述符掛起事件。
POLLNVAL 指定的文件描寫敘述符非法。
這些事件在events域中無意義,由於它們在合適的時候總是會從revents中返回。
2)nfds參數指定被監聽事件集合fds的大小。其類型nfds_t的定義例如以下:
typedef unsigned long int nfds_t;
3)timeout參數指定poll的超時值,單位是毫秒。當timeout為-1時,poll調用將永久堵塞,直到某個事件發生。當timeout為0時。poll調用將馬上返回。 poll系統調用的返回值的含義與select同樣。
epoll系列系統調用
內核事件表
epoll是Linux特有的IO復用函數。它在實現和使用上與select、poll有非常大差異。首先,epoll使用一組函數來完畢任務。而不是單個函數。
其次、epoll把用戶關心的文件描寫敘述符上的事件放在內核裏的一個事件表中,從而無須像select和poll那樣每次調用都要反復傳入文件描寫敘述符集或事件集。
但epoll須要使用一個額外的文件描寫敘述符,來唯一標識內核中的這個事件表。這個文件描寫敘述符使用例如以下epoll_reate函數來創建:
#include<sys/epoll.h> int epoll_create(int size);
size參數如今並不 起作用,僅僅是給內核一個提示。告訴它事件表須要多大。
該函數返回的文件描寫敘述符將用作其它全部epoll系統調用的第一個參數,以指定要訪問的內核事件表。 以下的函數用來操作epoll的內核事件表:
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
fd參數是要操作的文件描寫敘述符,op參數則指定操作類型。
操作類型有例如以下3種:
- EPOLL_CTL_ADD,往事件表中註冊fd上的事件。
- EPOLL_CTL_MOD。改動fd上的註冊事件。
- EPOLL_CTL_DEL,刪除fd上的註冊事件。
epoll_event的定義例如以下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
當中events成員描寫敘述事件類型。
epoll支持的事件類型和poll基本同樣。表示epoll事件類型的宏是在poll相應的宏前加上“E”,比方epoll的數據可讀事件是EPOLLIN。但epoll有兩個額外的事件類型——EPOLLET和EPOLLONESHOT。它們對於epoll的高效運作很關鍵,我們將在後面討論它們。data成員用於存儲用戶數據。 epoll_ctl成功時返回0。失敗則返回-1並設置errno。
epoll_wait函數
epoll系列系統調用的主要接口是epoll_wait函數。它在一段超時時間內等待一組文件描寫敘述符上的事件,其原型例如以下:#include <sys/epoll.h> int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);
該函數成功時返回就緒的文件描寫敘述符的個數,失敗時返回-1並設置errno。 關於該函數的參數,我們從後往前討論。
timeout參數的含義與poll接口的timeout參數同樣。
maxevents參數指定最多監聽多少個事件,它必須大於0. epoll_wait函數假設檢測到事件,就將全部就緒的事件從內核事件表(由epfd參數指定)中拷貝到它的第二個參數events指向的數組中。
這個數組僅僅用於輸出epoll_wait檢測到的就緒事件。而不像select和poll的數組參數那樣既用於傳入用戶註冊的事件,又用於輸出內核檢測到的就緒事件。這就極大地提高了應用程序索引就緒文件描寫敘述符的效率。
LT和ET模式
epoll對文件描寫敘述符的操作有兩種模式;LT(Level Trigger。電平觸發)模式和ET(Edge Trigger,邊沿觸發)模式。LT模式是默認的工作模式,這樣的模式下epoll相當於一個效率較高的poll。當往epoll內核事件表中註冊一個文件描寫敘述符上的EPOLLET事件時。epoll將以ET模式來操作該文件描寫敘述符。ET模式是epoll的高效工作模式。 對於採用LT工作模式的文件描寫敘述符。當epoll_wait檢測到其上有事件發生並將此事件通知應用程序後。應用程序能夠不馬上處理該事件。這樣。當應用程序下一次調用epoll_wait時,epoll_wait還會再次向應用程序通告此事件,直到該事件被處理。
而對於採用ET工作模式的文件描寫敘述符,當epoll_wait檢測到其上有事件發生並將此事件通知應用程序後。應用程序必須馬上處理該事件,由於後序的epoll_wait調用將不再想應用程序通知這一事件。可見,ET模式在非常大程度上減少了同一個epoll事件被反復觸發的次數,因此效率要比LT模式高。
EPOLLONESHOT事件
即使我們使用ET模式。一個socket上的某個事件還是可能被觸發多次。這在並發程序中就會引發一個問題。比方一個線程(或進程。下同)在讀取完某個socket上的數據後開始處理這些數據,而在數據的處理過程中該socket上又有新數據可讀(EPOLLIN再次被觸發),此時另外一個線程被喚醒來讀取這些新的數據。於是就出現了兩個線程同一時候操作一個socket的局面。
這當然不是我們期望的。
我們期望的是一個socket連接在隨意時刻都僅僅能被一個線程處理。這一點能夠使用epoll的EPOLLONESHOT事件來實現。 對於註冊了EPOLLONESHOT事件的文件描寫敘述符。操作系統最多觸發其上註冊的一個可讀、可寫或者異常事件。這樣,當一個線程在處理某個socket時,其它線程是不可能有機會操作該socket的。但反過來思考,註冊了EPOLLONESHOT事件的socket一旦被某個線程處理完成,該線程就應該馬上重置這個socket的EPOLLONESHOT事件,以確保這個socket下一次可讀時,其EPOLLIN事件能被觸發。進而讓其它工作線程有機會繼續處理這個socket。
epoll的長處
1)支持一個進程打開大數目的socket描寫敘述符(FD)select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置。 默認值是2048。
對於那些須要支持上萬連接數目的IMserver來說顯然太少了。這時候你一是能夠選擇改動這個宏然後又一次編譯內核,只是資料也同一時候指出這樣 會帶來網絡效率的下降;二是能夠選擇多進程的解決方式(傳統的Apache方案)。只是盡管linux上面創建進程的代價比較小,但仍舊是不可忽視的,加 上進程間數據同步遠比不上線程間同步高效。所以這也不是一種完美的方案。
只是epoll 沒有這個限制。它所支持的FD上限是最大能夠打開文件的數目,這個數字一般遠大於select
所支持的2048。舉個樣例,在1GB內存的機器上大約是10萬左右。詳細數目能夠cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系非常大。
2)IO效率不隨FD數目添加而線性下降
傳統select/poll的還有一個致命弱點就是當你擁有一個非常大的socket集合,由於網絡得延時,使得任一時間僅僅有部分的socket是"活躍" 的,而select/poll每次調用都會線性掃描所有的集合。導致效率呈現線性下降。可是epoll不存在這個問題,它僅僅會對"活躍"的socket進 行操作---這是由於在內核實現中epoll是依據每一個fd上面的callback函數實現的。於是,僅僅有"活躍"的socket才會主動去調用 callback函數,其它idle狀態的socket則不會,在這點上,epoll實現了一個"偽"AIO,由於這時候推動力在os內核。
在一些
benchmark中,假設全部的socket基本上都是活躍的---比方一個快速LAN環境,epoll也不比select/poll低多少效率,但若 過多使用的調用epoll_ctl,效率略微有些下降。然而一旦使用idle connections模擬WAN環境。那麽epoll的效率就遠在select/poll之上了。
3) 使用mmap加速內核與用戶空間的消息傳遞
這點實際上涉及到epoll的詳細實現。不管是select,poll還是epoll都須要內核把FD消息通知給用戶空間,怎樣避免不必要的內存拷貝就顯 得非常重要,在這點上,epoll是通過內核於用戶空間mmap同一塊內存實現的。而假設你像我一樣從2.5內核就開始關註epoll的話。一定不會忘記手 工mmap這一步的。
4) 內核微調
這一點事實上不算epoll的長處。而是整個linux平臺的長處。
或許你能夠懷疑linux平臺,可是你無法回避linux平臺賦予你微調內核的能力。比 如。內核TCP/IP協議棧使用內存池管理sk_buff結構,能夠在執行期間動態地調整這個內存pool(skb_head_pool)的大小---通 過echo XXXX>/proc/sys/net/core/hot_list_length來完畢。
再比方listen函數的第2個參數(TCP完畢3次握 手的數據包隊列長度),也能夠依據你平臺內存大小來動態調整。甚至能夠在一個數據包面數目巨大但同一時候每一個數據包本身大小卻非常小的特殊系統上嘗試最新的 NAPI網卡驅動架構。
IO復用