select/poll被監視的文件描述符數目非常大時要O(n)效率很低;epoll與舊的 select 和 poll 系統調用完成操作所需 O(n) 不同, epoll能在O(1)時間內完成操作,所以性能相當高。
epoll不用每次把注冊的fd在用戶態和內核態反復拷貝。
epoll不同與之前的輪詢方式,用了類似事件觸發的方式,能夠精確得獲得實際需要操作的fd.
今天看到一個說法是 epoll_wait 里面 maxevents 這個參數,不能大於epoll_create的size參數。而之前我的程序,epoll_wait用的都是1024,而epoll_create用的都是5. 看來以后epoll_create的參數要謝大一點了。
但是實際上,epoll_create的參數不使用了。
Since Linux 2.6.8, the size argument is unused. (The kernel dynamically sizes the required data structures without needing this initial hint.)
然后epoll_ctl很重要,我一般都是單獨寫一個wrapper函數,如下:
void addfd(int epollfd, int fd, bool enable_et) { epoll_event event; event.data.fd = fd; event.events = EPOLLIN; if (enable_et) { event.events |= EPOLLET; } epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); setnonblocking(fd); }
上面epoll_ctl的第二個參數,可以有如下選擇:
EPOLL_CTL_ADD //注冊新的fd到epfd中; EPOLL_CTL_MOD //修改已經注冊的fd的監聽事件; EPOLL_CTL_DEL //從epfd中刪除一個fd;
第三個參數是需要監聽的fd,第四個參數是告訴內核需要監聽什么事,struct epoll_event 結構如下:
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
上面,我一般都會把epoll_event.data里面的fd也寫成正確的fd.
epoll_event里面的events可以是下面的宏的集合:
EPOLLIN //表示對應的文件描述符可以讀(包括對端SOCKET正常關閉); EPOLLOUT //表示對應的文件描述符可以寫; EPOLLPRI //表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來); EPOLLERR //表示對應的文件描述符發生錯誤; EPOLLHUP //表示對應的文件描述符被掛斷; EPOLLET //將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。 EPOLLONESHOT//只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里。
注意上面的EPOLLONESHOT,在讀取完一整個事件之后,要重置EPOLLONESHOT讓其他的線程能夠接收到事件,通過如下方式來重置:
void reset_oneshot(int epollfd, int fd) { epoll_event event; event.data.fd = fd; event.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event); }
注意以上的方式,是確知原來events內容的情況下;如果穩妥起見,最好把原來的events信息拿過來(更正:下面有講到,其實對於EPOLLONESHOT,不是恢復事件,而是重新注冊事件,所以也不一定要拿原來的events信息了)。
當對方關閉連接(FIN), EPOLLERR,都可以認為是一種EPOLLIN事件,在read的時候分別有0,-1兩個返回值。
另注意:
read返回0,不管阻塞還是非阻塞,一概是對方關閉連接;阻塞的話讀不到數據不會返回,返回0說明對方關閉;非阻塞的話讀不到數據會返回-1同時errno是EAGIN,返回0也說明對方關閉。
read返回-1,對於阻塞,是有錯誤返回,需檢查錯誤碼處理;對於非阻塞,有可能是需要重試,也需要檢查錯誤碼,如果是EAGAIN,那么正常重試獲取數據就可以了。
ERRNO及線程安全性
上面提到了errno,那么如果errno不是線程安全的,多個線程同時讀取的時候,豈不是會出現大問題?還好!errno是線程安全的!
從字面上看,errno是全局變量,但是實際上,errno其實是線程局部變量!這是GCC中保證的。他保證了線程之間的錯誤原因不會互相串改,當你在一個線程中串行執行一系列過程,那么得到的errno仍然是正確的。
看下,bits/errno.h的定義:
# ifndef __ASSEMBLER__ /* Function to get address of global `errno' variable. */ extern int *__errno_location (void) __THROW __attribute__ ((__const__)); # if !defined _LIBC || defined _LIBC_REENTRANT /* When using threads, errno is a per-thread value. */ # define errno (*__errno_location ()) # endif # endif /* !__ASSEMBLER__ */
注意其中,飄紅的那一句。是一個線程局部變量。
另外還有個errno.h中是這樣定義的:
/* Declare the `errno' variable, unless it's defined as a macro by bits/errno.h. This is the case in GNU, where it is a per-thread variable. This redeclaration using the macro still works, but it will be a function declaration without a prototype and may trigger a -Wstrict-prototypes warning. */ #ifndef errno extern int errno; #endif
從上面可以看出,errno首先是在bits/errno.h中定義的,沒定義的話,才會在errno.h中定義。而且errno實際上是一個整型指針(見bits/errno.h),並不是我們通常認為的是個整型數值,而是通過整型指針來獲取值的。這個整型就是線程安全的。
如果想看下編譯選項里面有沒有加上_LIBC_REENTRANT,可以用下面的代碼:
#include <stdio.h> #include <errno.h> int main() { #ifndef __ASSEMBLER__ printf( "Undefine __ASSEMBLER__\n" ); #else printf( "define __ASSEMBLER__\n" ); #endif #ifndef __LIBC printf( "Undefine __LIBC\n" ); #else printf( "define __LIBC\n" ); #endif #ifndef _LIBC_REENTRANT printf( "Undefine _LIBC_REENTRANT\n" ); #else printf( "define _LIBC_REENTRANT\n" ); #endif return 0; }
編譯運行:
$ g++ -o errno_demo errno_demo.cpp $ ./errno_demo
Undefine __ASSEMBLER__
Undefine __LIBC
Undefine _LIBC_REENTRANT
注意,__ASSEMBLER__沒有定義,所以進入了bits/errno.h的代碼塊,然后__LIBC沒有定義,errno就會用線程安全的定義,不需要再看_LIBC_REENTRANT是不是定義。也就是說默認的編譯選項,errno就已經是線程安全的!!!安全的!!!
errno的實現可以參考如下:
static pthread_key_t key; static pthread_once_t key_once = PTHREAD_ONCE_INIT; static void make_key() { (void) pthread_key_create(&key, NULL); } int *_errno() { int *ptr ; (void) pthread_once(&key_once, make_key); if ((ptr = pthread_getspecific(key)) == NULL) { ptr = malloc(sizeof(int)); (void) pthread_setspecific(key, ptr); } return ptr ; }
其中有pthread_key_t 和 pthread_once_t。在另外的文章里面詳細說吧。
epoll_wait的原型是這樣的:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
第四個參數timeout為0的時候表示不阻塞立即返回,為-1表示一直阻塞。
返回值是等待處理的事件數量,如果是0可能是因為超時或者非阻塞。
LT vs. ET
EPOLL事件有兩種模型 Level Triggered (LT) 和 Edge Triggered (ET):
LT(level triggered,水平觸發模式)是缺省的工作方式,並且同時支持 block 和 non-block socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。
ET(edge-triggered,邊緣觸發模式)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知,等到下次有新的數據進來的時候才會再次出發就緒事件。
要注意的是,如果設置了EPOLL_ONESHOT模式,那么在每次獲取一個fd上的事件之后,這個fd上的這個事件會被清除(主要是為了避免多個線程讀數據時候相互干擾),直到讀完數據需要手動地使用epoll_ctl的EPOLL_CTL_MOD再對這個fd加上這個event事件才行。
EPOLL_ONESHOT的更多內容,可以參考我的另一篇文章:http://www.cnblogs.com/charlesblc/p/5538363.html
從man手冊中,得到ET和LT的具體描述如下
EPOLL事件有兩種模型:
Edge Triggered(ET) //高速工作方式,錯誤率比較大,只支持no_block socket (非阻塞socket)
LevelTriggered(LT) //缺省工作方式,即默認的工作方式,支持blocksocket和no_blocksocket,錯誤率比較小。
注意,ET這種方式對於accept也是一樣的,如果是listen的句柄,那么ET模式下收到事件,必須循環確保都處理完,因為多個accept同時發生也只會觸發一次事件。
EPOLLOUT
另外,EPOLLOUT這種監聽方式,平時不太用的到。在網上搜到如下的解釋和用法,覺得很好:
對於LT 模式,如果注冊了EPOLLOUT,只要該socket可寫(發送緩沖區)未滿,那么就會觸發EPOLLOUT。
對於ET模式,如果注冊了EPOLLOUT,只有當socket從不可寫變為可寫的時候,會觸發EPOLLOUT。
如果需要,一種用法:自己在應用層加個發送緩沖區,需要發送數據的時候,如果應用層的發送緩沖區為空,則直接寫到socket中。否則就寫到應用層的發送緩沖區,並注冊OUT時間(LT模式)。
反正我是沒用過EPOLLOUT,直接寫就行了,哈哈哈。
負責listen的socket上同時注冊EPOLLIN | EPOLLOUT,收到connet請求時,只看到EPOLLIN事件。 在accectp后的socket上同時注冊EPOLLIN | EPOLLOUT,這時候客戶端還沒有操作,這時只發生了EPOLLOUT事件。 客戶端send后,服務端收到了EPOLLIN事件,然后改為關注EPOLLOUT事件,立即就又收到了EPOLLOUT事件。 跟上面的分析一致。另外從實驗中發現貌似listen的fd只有EPOLLIN會生效。
EAGAIN
最后,還是要再說一下EAGAIN,仔細領悟下面這句話:
/* If errno == EAGAIN, that means we have read all
data. So go back to the main loop. */
也就是說,對於ET模式循環讀取數據的情況,如果read函數返回-1並且errno等於EAGAIN,是要跳出循環的,但是不需要close socket,因為不是真的有錯誤;其他的errno才是有錯誤,才需要關閉socket(為了兼容其他系統,有時候會把EWOULDBLOCK和EAGAIN放在一起處理,其實是等價的);只有read返回>0的時候,才需要繼續在循環里面讀取;read返回0表示對方關閉了,直接跳出循環,並且關閉socket.
以上基本就是ET模式對於read函數返回幾種情況的處理方式。對於LT模式,基本也是相同的處理,只不過不需要放在循環里讀取,也就是說read函數返回>0的時候,不回到循環繼續讀取也是可以的,因為對於這種還有數據沒有讀完的情況,LT模式會再次觸發EPOLLIN事件的。