epoll函數與參數總結學習 & errno的線程安全


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事件的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM