我們首先需要知道select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。
select的基本用法:http://blog.csdn.net/nk_test/article/details/49256129
poll的基本用法:http://blog.csdn.net/nk_test/article/details/49283325
epoll的基本用法:http://blog.csdn.net/nk_test/article/details/49331717
接下來我們探讨如何正确使用non-blocking I/O Multiplexing + poll/epoll。
先來說幾個常見的問題:
1.SIGPIPE信号的産生和處理
如果用戶端使用close關閉套接字,而伺服器端調用了一次write,伺服器會接收一個RST segment(TCP傳輸層); 如果伺服器再次調用write,這個時候就會産生SIGPIPE信号,如果不忽略,就會預設退出程式,顯然是不滿足伺服器的高可用性。可以在程式中直接忽略掉,如 signal(SIGPIPE, SIG_IGN)。
2.TIME_WAIT 狀态對 伺服器的影響
應盡可能避免在服務端出現TIME_WAIT狀态。如果伺服器主動斷開連接配接(先于client調用close),服務端就會進入TIME_WAIT狀态,核心會hold住一些資源,大大降低伺服器的并發能力。解決方法:協定設計上,應該讓用戶端主動斷開連接配接,這樣就可以把TIME_WAIT狀态分散到大量的用戶端。如果用戶端不活躍了,一些惡意的用戶端不斷開連接配接,這樣就會占用伺服器端的連接配接資源。是以服務端也要有機制來踢掉不活躍的連接配接。
3.新的accept4系統調用
多了flags參數,可以設定以下兩個标志:
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to achieve the same result.
SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.
程序被替換時,檔案描述符是關閉的狀态,用來設定傳回的已連接配接套接字,也可以使用fcntl設定,但是效率稍微低一些。
4.accept(2)傳回EMFILE的處理(檔案描述符已經用完)
(1)調高程序檔案描述符數目
(2)死等
(3)退出程式
(4)關閉監聽套接字。那什麼時候重新打開呢?
(5)如果是epoll模型,可以改用edge trigger。問題是如果漏掉了一次accept(2),程式再也不會收到新連接配接(沒有狀态變化)
(6)準備一個空閑的檔案描述符。遇到這種情況,先關閉這個空閑檔案,獲得一個檔案描述符名額;再accept(2)拿到socket連接配接的檔案描述符;随後立刻close(2),這樣就優雅地斷開了與用戶端的連接配接;最後重新打開空閑檔案,把“坑”填上,以備再次出現這種情況時使用。
int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
connfd = accept4(listenfd, (struct sockaddr *)&peeraddr,
&peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
/* if (connfd == -1)
ERR_EXIT("accept4");
*/
if (connfd == -1)
{
if (errno == EMFILE)
{
close(idlefd);
idlefd = accept(listenfd, NULL, NULL);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
continue;
}
else
ERR_EXIT("accept4");
}
(一)poll的處理流程和需要注意的問題
需要注意的問題及其處理方法:
(1) 粘包問題:read 可能一次并沒有把connfd 所對應的接收緩沖區(核心)的資料都讀完,那麼connfd下次仍然是活躍的。我們應該将讀到的資料儲存在connfd的應用層緩沖區(char buf[1024]),并處理好消息的邊界。
(2)write應答的資料量較大的情況下,可能一次并不能把所有的資料發送到核心的緩沖區,是以應該有一個應用層的緩沖區,将未發送完的資料添加到應用層發送緩沖區。
(3)關注connfd 的POLLOUT 事件的時機。POLLOUT事件到來,則取出應用層發送緩沖區資料發送write,如果應用層發送緩沖區資料發送完畢,則取消關注POLLOUT事件。POLLOUT 事件觸發條件:connfd的發送緩沖區(核心)不滿(可以容納資料)。
注:connfd 的接收緩沖區(核心)資料被接收後會被清空,當發出資料段後接收到對方的ACK段後,發送緩沖區(核心)資料段會被清空。write隻是将應用層發送緩沖區資料拷貝到connfd 對應的核心發送緩沖區就傳回;read 隻是從connfd對應的核心接收緩沖區資料拷貝到應用層接收緩沖區就傳回。
(二)epoll的處理流程和需要注意的問題 電平觸發模式:
基本處理流程和poll十分相似。注意epoll_wait傳回的都是活躍的,不用周遊,直接處理即可。write傳回成功隻是說明将資料拷貝至核心緩沖區。 EPOLLIN 事件
核心中的某個socket接收緩沖區 為空 低電平 核心中的某個socket接收緩沖區 不為空 高電平
EPOLLOUT 事件
核心中的某個socket發送緩沖區 不滿 高電平 核心中的某個socket發送緩沖區 滿 低電平
注:隻要第一次write沒寫完整,則下次調用write直接把資料添加到應用層緩沖區OutBuffer,等待EPOLLOUT事件。 邊沿觸發模式:
缺點:
可能産生漏連接配接accept的bug,很難處理;
檔案描述符達到上限後,一直處于高電平,不會再觸發了。處理起來也比較麻煩。
推薦epoll使用LT模式的原因: 其中一個是與poll相容; LT(水準)模式不會發生漏掉事件的BUG,但POLLOUT事件不能一開始就關注,否則會出現busy loop(即暫時還沒有資料需要寫入,但一旦連接配接建立,核心發送緩沖區為空會一直觸發POLLOUT事件),而應該在write無法完全寫入核心緩沖區的時候才關注,将未寫入核心緩沖區的資料添加到應用層output buffer,直到應用層output buffer寫完,停止關注POLLOUT事件。 讀寫的時候不必等候EAGAIN,可以節省系統調用次數,降低延遲。(注:如果用ET模式,讀的時候讀到EAGAIN,寫的時候直到output buffer寫完或者寫到EAGAIN)
注:在使用 ET 模式時,可以寫得更嚴謹,即将 listenfd 設定為非阻塞,如果accpet 調用有傳回,除了建立目前這個連接配接外,不能馬上就回到 epoll_wait ,還需要繼續循環accpet,直到傳回-1 且errno == EAGAIN 才退出。代碼示例如下:
if(ev.events & EPOLLIN)
{
do
{
struct sockaddr_in stSockAddr;
socklen_t iSockAddrSize = sizeof(sockaddr_in);
int iRetCode = accept(listenfd, (struct sockaddr *) &stSockAddr, iSockAddrSize);
if (iRetCode > 0)
{
// ...建立連接配接
// 添加事件關注
}
else
{
//直到發生EAGAIN才不繼續accept
if(errno == EAGAIN)
{
break;
}
}
}
while(true);
// ... 其他 EPOLLIN 事件
}
(三)三者各方面的比較
select:fd_set有檔案描述符個數的限制;另外每次都要複制到核心空間,掃描O(N)的複雜度。要周遊所有的檔案描述符去判斷是否發生了事件。
poll:也是拷貝和輪詢。拷貝至核心中的連結清單,沒有最大連接配接數的限制。
epoll:使用共享記憶體(mmap)減少複制開銷,存放感興趣的套接字,都是在核心态操作的,采用事件通知的回調機制。
注意:并不是在任何條件下epoll的效率都是最高的,要根據實際的應用情況來判斷使用哪種I/O。
如果已連接配接套接字數目不太大并且這些套接字一直處于活躍的狀态,那麼不停地調用callback函數可能會造成低效率,也就是說低于一次性周遊,此時epoll的效率就可能低于select和poll。