轉載自:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201322375435117/
select,poll,epoll簡介:
select |
select本質上是通過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:
1、 單個進程可監視的fd數量被限制,數組有大小限制; 2 、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大; 3 、對socket進行掃描時是線性掃描; 4、select也是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次selectl時會再次報告該fd; |
poll |
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。 1、它沒有最大連接數的限制,原因是它是基於鏈表來存儲的; 2、和select一樣大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義。 3、poll也是線性掃描; 4、poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。 |
epoll |
epoll支持水平觸發和邊緣觸發; 1、最大的特點在於邊緣觸發,它只告訴進程哪些fd剛剛變爲就需態,並且只會通知一次。 2、在前面說到的複製問題上,epoll使用mmap減少複製開銷(使用mmap,內核和用戶態共享數據)。 3、還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。 4、雖然也有限制,可以認爲無限大。 |
select,poll,epoll的比較:
1、支持一個進程所能打開的最大連接數
select | 單個進程所能打開的最大連接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是32*32,同理64位機器上 FD_SETSIZE爲32*64),當然我們可以對它進行修改,然後重新編譯內核,但是性能可能會受到影響,這需要進一步的測試。 |
poll | poll本質上和select沒有區別,但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的。 |
epoll | 雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接,2G內存的機器可以打開20萬左右的連接。 |
epoll管理鏈接數上限:/proc/sys/fs/epoll/max_user_watchs
根據epoll的實現,在64位環境下,epoll在內核中需要爲每個fd消耗160Bytes。這部分內存可以通過slabtop查看。
其次,根據linux內核2.6.32.43中對tcp協議棧的實現,分析內核中socket相關數據結構的內存開銷。內核爲每個應用層中打開的socket維護structsocket_alloc數據結構,它包含struct socket和struct inode結構,分別對應socket在tcp中的表示和vfs中的inode數據結構。
在網絡層中,還需要struct sock數據結構來表示socket。
2、FD劇增後帶來的IO效率問題
select |
因爲每次調用時都會對連接進行線性遍歷,所以隨着FD的增加會造成遍歷速度慢的“線性下降性能問題”。 |
poll | 同上 |
epoll |
因爲epoll內核中實現是根據每個fd上的callback函數來實現的,只有活躍的socket纔會主動調用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。 |
3、消息傳遞方式
select | 內核需要將消息傳遞到用戶空間,都需要內核拷貝動作。 |
poll | 同上 |
epoll | epoll通過內核和用戶空間通過mmap共享一塊內存來實現的。 |
綜上比較:
在選擇select,poll,epoll時,要根據具體的使用場合以及select,poll,epoll這三種方式的自身特點。
從表象看epoll的性能最好,但是在連接數少,並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多回調函數來完成。
epoll在事件管理上,使用的是紅黑樹,可以快速的增刪事件和快速查找事件。
以上參考博客:
http://xingyunbaijunwei.blog.163.com/blog/static/76538067201241685556302/
select的接口,及解釋:
select()
select()用來監控多個文件描述符,當fd變得可讀/可寫時,select()將標記可讀/可寫fd。select()現在被認爲是低效的fd監控接口,在實際項目中通常用epoll()來代替select()。
#include <unistd.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/time.h>
int select(int maxfd, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
void FD_CLR(int fd, fd_set* set);
int FD_ISSET(int fd, fd_set* set);
void FD_SET(int fd, fd_set* set);
void FD_ZERO(fd_set* set);
select()的參數解析:
1、一個參數maxfd是加入select()的最大文件描述符值+1,最大值爲1024。可以修改FD_SETSIZE的值以使select()支持更多文件描述符監控,但必須重新編譯內核,否則結果未知。
2、中間3個參數是3個fd集合,分別是你想監聽的可讀fd、可寫fd、異常fd。
3、最後一個參數timeout是指定select()的超時時間。
timeout的取值可以是:
NULL - 永久等待,直到有讀/寫/異常事件發生。
0 - 立即返回,此時select()爲非阻塞狀態。
其他值 - 指定select()等待時間。注意,timeout指定最長等待時間,但一旦有1個或多個fd可讀/寫/異常時select()就會返回。
select()的返回值:
0 - 超時,且沒有任何讀/寫fd。
> 0 - 有讀/寫fd,用FD_ISSET()進一步判斷。
-1 - select()出錯。常見的錯誤包括:
EINTR - 捕獲到信號。通常可忽略。
EBADF - 有無效的文件描述符。
Socket可讀/寫的常見情況分析:
select()返回sockfd可讀:
1、Receive緩衝區的數據大於或等於low-water mark的值。low-water mark的值可通過SO_RCVLOWAT選項控制,默認是1。 (即讀緩衝區中有數據)
2、TCP連接接收到FIN,即Read half of the connections is closed。此時對sockfd的讀操作將返回0,即EOF。
3、如果sockfd是一個監聽套接字,則表明有新連接,可調用accept()函數建立新連接。
4、Socket出錯,此時對sockfd的讀操作將返回-1。
select()返回sockfd可寫:
1、Send緩衝區的數據大於或等於low-water mark的值。low-water mark的值可通過SO_SNDLOWAT選項控制,默認是2048。 (即緩衝區有數據)
2、Write half of the connection is closed,對sockfd的寫操作將產生SIGPIPE信號。
3、對非阻塞的sockfd調用connect(),connect()完成或失敗。
4、Socket出錯,此時對sockfd的寫操作將返回-1。
分析。若讀緩衝有數據,則socket可讀;若寫緩衝有空間,則socket可寫。如果socket出錯,則它本身處於可讀寫狀態,且調用read()/write()返回-1。若是Listen Socket,則有新連接來時它可讀;若是Non-block Connect Socket,則連接成功時它可寫。這些情況都不難理解,只有以上列出情況3,需要進一步說明。
參考:http://www.berlinix.com/dev/network.php
epoll的接口非常簡單,一共就三個函數:
1.創建epoll句柄
int epfd = epoll_create(int size);
創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。
2.將被監聽的描述符添加到epoll句柄或從epool句柄中刪除或者對監聽事件進行修改。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件註冊函數,它不同與select()是在監聽事件時告訴內核要監聽什麼類型的事件,而是在這裏先註冊要監聽的事件類型。
EPOLL_CTL_ADD: 註冊新的fd到epfd中;
EPOLL_CTL_MOD: 修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL: 從epfd中刪除一個fd;
第三個參數是需要監聽的fd,
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下幾個宏的集合:
EPOLLIN : 觸發該事件,表示對應的文件描述符上有可讀數據。(包括對端SOCKET正常關閉);
EPOLLOUT: 觸發該事件,表示對應的文件描述符上可以寫數據;
EPOLLPRI: 表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR: 表示對應的文件描述符發生錯誤;
EPOLLHUP: 表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT: 只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏。
3. 等待事件觸發,當超過timeout還沒有事件觸發時,就超時。
等待事件的產生,類似於select()調用。
當產生了一個EPOLLIN事件後:
讀數據的時候需要考慮的是當recv()返回的大小如果等於要求的大小,即sizeof(buf),那麼很有可能是緩衝區還有數據未讀完,也意味着該次事件還沒有處理完,所以還需要再次讀取(如果在ET水平模式下,可以等到下次事件觸發時,再讀數據):
while(rs) //ET模型
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
// 由於是非阻塞的模式,所以當errno爲EAGAIN或EINT時,表示當前緩衝區已無數據可讀(被其它線程可能讀了)
// 在這裏就當作是該次事件已處理處。因爲是ET模式,如果有數據,內核還會繼續觸發讀事件。
//即當buflen<0且errno=EAGAIN||errno=EINT時,表示沒有數據了。(讀/寫都是這樣)
if(errno == EAGAIN || errno == EINT)
break;
else
return; //真的失敗了。
}
else if(buflen == 0)
{
// 這裏表示對端的socket已正常關閉,收到了FIN包。soket在read到0個數據時,表示收到對端請求的FIN包。
}
if(buflen == sizeof(buf)
rs = 1; // 需要再次讀取(有可能是因爲數據緩衝區buf太小,所以數據沒有讀完,可以等到下次再讀,沒有buf了)
else
rs = 0; //不需要再次讀取(當buflen<sizeof(buf)時,非阻塞文件描述符的特性),
}
當產生了一個EPOLLOUT事件後:
還有,假如發送端流量大於接收端的流量(意思是epoll所在的程序讀比轉發的socket要快),由於是非阻塞的socket,那麼send()函數雖然返回,但實際緩衝區的數據並未真正發給接收端,這樣不斷的讀和發,當緩衝區滿後會產生EAGAIN錯誤(參考man
send),同時,不理會這次請求發送的數據。所以,需要封裝socket_send()的函數用來處理這種情況,該函數會盡量將數據寫完再返回,返回-1表示出錯。在socket_send()內部,當寫緩衝已滿(send()返回-1,且errno爲EAGAIN),那麼會等待後再重試。這種方式並不很完美,在理論上可能會長時間的阻塞在socket_send()內部,但暫沒有更好的辦法。這種方法類似於readn和writen的封裝(在《UNIX環境高級編程》中也有介紹)
ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total = buflen;
const char *p = buffer;
while(1)
{
tmp = send(sockfd, p, total, 0);
if(tmp < 0)
{
// 當send收到信號時,可以繼續寫,但這裏返回-1.
if(errno == EINTR)
return -1;
// 當socket是非阻塞時,如返回此錯誤,表示寫緩衝隊列已滿,
// 在這裏做延時後再重試.
if(errno == EAGAIN)
{
usleep(1000);
continue;
}
return -1;
}
if((size_t)tmp == total)
return buflen;
total -= tmp;
p += tmp;
}
return tmp;
}