天天看點

select, poll, epoll詳解(一)1.select函數2.poll函數3.epoll函數

目錄

1.select函數

1.1參數說明

2.poll函數

2.1參數說明     

2.2傳回值

2.3事件标記

3.epoll函數

3.1參數說明   

3.2傳回值

3.3與select&poll的差別

3.4epoll的優點

3.5epoll的模型

1.select函數

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
           struct timeval *timeout);
           

1.1參數說明

    第1個參數nfds為fdset集合中最大描述符值加1,fdset是一個位數組,其大小限制為__FD_SETSIZE(1024),位數組的每一位代表其對應的描述符是否需要被檢查。

    第2,3,4個參數表示需要關注讀、寫、錯誤事件的檔案描述符位數組,這些參數既是輸入參數也是輸出參數,可能會被核心修改用于标示哪些描述符上發生了關注的事件。是以每次調用select前都需要重新初始化fdset。

    第5個參數為逾時時間,該結構會被核心修改,其值為逾時剩餘的時間。

    select對應于核心中的sys_select調用,sys_select首先将第2,3,4個參數指向的fd_set拷貝到核心,然後對每個被SET的描述符調用進行poll輪詢,并記錄在臨時結果中(fdset),如果有事件發生,select會将臨時結果寫到使用者空間并傳回;當輪詢一遍後沒有任何事件發生時,如果指定了逾時時間,則select會睡眠到逾時,睡眠結束後再進行一次輪詢,并将臨時結果寫到使用者空間,然後傳回。

    select傳回後,需要逐一檢查關注的描述符是否被SET(事件是否發生)。

2.poll函數

struct pollfd {
  int fd;        //檔案描述符 
  short events;  //要求檢測的事件掩碼
  short revents; //傳回的事件掩碼 
}
typedef unsigned long   nfds_t;

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
           

2.1參數說明

    poll與select不同,通過一個pollfd數組向核心傳遞需要關注的事件,故沒有描述符個數的限制。pollfd中的events字段和revents分别用于标示關注的事件和發生的事件,故pollfd數組隻需要被初始化一次。

    第1個參數用于存放需要檢測其狀态的socket描述符;每當調用這個函數之後,系統不會清空這個數組,操作起來比較友善;特别是對于socket連接配接比較多的情況下,在一定程度上可以提高處理的效率;這一點與select()函數不同,調用select()函數之後,select()函數會清空它所檢測的socket描述符集合,導緻每次調用select()之前都必須把socket描述符重新加入到待檢測的集合中;是以,select()函數适合于隻檢測一個socket描述符的情況,而poll()函數适合于大量socket描述符的情況。

    poll的實作機制與select類似,其對應核心中的sys_poll,隻不過poll向核心傳遞pollfd數組,然後對pollfd中的每個描述符進行poll,相比處理fdset來說,poll效率更高。

    poll傳回後,需要對pollfd中的每個元素檢查其revents值,來得知事件是否發生。

    第2個參數用于标記數組fds中的結構體元素的總數量。

    第3個參數是poll函數調用阻塞的時間,機關為毫秒。

2.2傳回值

    >0:     數組fds中準備好讀、寫或出錯狀态的那些socket描述符的總數量。

    ==0:   數組fds中沒有任何socket描述符準備好讀、寫,或出錯;此時poll逾時,逾時時間是timeout毫秒;換句話說,如果所檢測的 socket描述符上沒有任何事件發生的話,那麼poll()函數會阻塞timeout所指定的毫秒時間長度之後傳回,如果timeout==0,那麼 poll() 函數立即傳回而不阻塞,如果timeout==INFTIM,那麼poll() 函數會一直阻塞下去,直到所檢測的socket描述符上的感興趣的事件發 生是才傳回,如果感興趣的事件永遠不發生,那麼poll()就會永遠阻塞下去。

    -1:     poll函數調用失敗,同時會自動設定全局變量errno。

    如果待檢測的socket描述符為負值,則對這個描述符的檢測就會被忽略,也就是不會對成員變量events進行檢測,在events上注冊的事件也會被忽略,poll()函數傳回的時候,會把成員變量revents設定為0,表示沒有事件發生。

    另外,poll() 函數不會受到socket描述符上的O_NDELAY标記和O_NONBLOCK标記的影響和制約,也就是說,不管socket是阻塞的還是非阻塞 的,poll()函數都不會收到影響;而select()函數則不同,select()函數會受到O_NDELAY标記和O_NONBLOCK标記的影 響,如果socket是阻塞的socket,則調用select()跟不調用select()時的效果是一樣的,socket仍然是阻塞式TCP通信,相反,如果socket是非阻塞的socket,那麼調用select()時就可以實作非阻塞式TCP通信。

    是以poll() 函數的功能和傳回值的含義與 select() 函數的功能和傳回值的含義是完全一樣的,兩者之間的差别就是内部實作方式不一樣。

2.3事件标記

    經常檢測的事件标記: POLLIN/POLLRDNORM(可讀)、POLLOUT/POLLWRNORM(可寫)、POLLERR(出錯)。

    如果是對一個描述符上的多個事件感興趣的話,可以把這些常量标記之間進行按位或運算。例如:對socket描述符fd上的讀、寫、異常事件感興趣,就可以這樣做:

        struct pollfd fds;

        fds[nIndex].events=POLLIN | POLLOUT | POLLERR;

    當 poll()函數傳回時,要判斷所檢測的socket描述符上發生的事件,可以這樣做:

        struct pollfd fds;

        檢測可讀TCP連接配接請求:

        if((fds[nIndex].revents & POLLIN) == POLLIN) { 

            //接收資料/調用accept()接收連接配接請求

        } 

        檢測可寫:

        if((fds[nIndex].revents & POLLOUT) == POLLOUT) {

            //發送資料

        }

        檢測異常:

        if((fds[nIndex].revents & POLLERR) == POLLERR) {

            //異常處理

        }

3.epoll函數

int epoll_create(int size); 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 
           

3.1參數說明   

    epoll主要包含epoll_create, epoll_ctl以及epoll_wait三個函數。

    epoll_create函數建立epoll檔案描述符,參數size并不是限制了epoll所能監聽的描述符最大個數,隻是對核心初始配置設定内部資料結構的一個建議。

    當建立好epoll句柄後,會占用一個fd值,在linux下可以通過"cat /proc/程序id/fd/" 檢視。是以在使用完epoll後,必須調用close()關閉,否則可能導緻fd被耗盡。

    使用例子:int epfd = epoll_create(int size);   

   epoll_ctl事件注冊函數,控制對指定描述符fd執行op操作。   

    第1個參數是epoll_create()的傳回值;

    第2個參數表示動作,使用如下三個宏來表示:

        EPOLL_CTL_ADD //注冊新的fd到epfd中;

        EPOLL_CTL_MOD //修改已經注冊的fd的監聽事件;

        EPOLL_CTL_DEL //從epfd中删除一個fd;

    第3個參數是需要監聽的fd。

    第4個參數是告訴核心需要監聽什麼事件。events可以是下面幾個宏的集合:

        EPOLLIN     //表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);

        EPOLLOUT //表示對應的檔案描述符可以寫;

        EPOLLPRI  //表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);

        EPOLLERR //表示對應的檔案描述符發生錯誤;

        EPOLLHUP //表示對應的檔案描述符被挂斷;

        EPOLLET   //将epoll設為邊緣觸發(Edge Triggered)模式,這是相對于水準觸發(Level Triggered)來說的。

        EPOLLONESHOT //隻監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,

                                      //需要再次把這個socket加入到epoll隊列裡。

    當對方關閉連接配接(FIN), EPOLLERR,都可以認為是一種EPOLLIN事件,在read的時候分别有0,-1兩個傳回值。

    使用例子:

        struct epoll_event ev;

        ev.data.fd=listenfd;                 //設定與要處理的事件相關的檔案描述符

        ev.events=EPOLLIN|EPOLLET; //設定要處理的事件類型

        epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //注冊epoll事件

    epoll_wait等待epfd上的io事件,最多傳回maxevents個事件。

    第1個參數是epoll_create()的傳回值,即生成的epoll專用的檔案描述符;

    第2個參數是用于回傳代處理事件的數組;

    第3個參數是每次能處理的事件數,用于告訴核心這個events有多大,maxevents的值不能大于建立epoll_create()時的size;

    第4個參數是等待I/O事件發生的逾時時間。機關是毫秒,0會立即傳回,-1将不确定,也有說法說是永久阻塞。

    epoll_wait運作的原理是:等侍注冊在epfd上的socket fd的事件的發生,如果發生則将發生的socket fd和事件類型放入到events數組中。并且将注冊在epfd上的socket fd的事件類型給清空,是以如果下一個循環你還要關注這個socket fd的話,則需要用epoll_ctl(epfd, EPOLL_CTL_MOD, listenfd, &ev)來重新設定socket fd的事件類型。這時不用EPOLL_CTL_ADD,因為socket fd并未清空,隻是事件類型清空。這一步非常重要。

3.2傳回值

    epoll_create傳回一個epoll專用的檔案描述符。它其實是在核心申請一空間,用來存放你想關注的socket fd上是否發生以及發生了什麼事件。

    epoll_ctl執行成功時傳回0; 失敗傳回-1.

    epoll_wait傳回需要處理的事件數目,若傳回0則表示已逾時。

3.3與select&poll的差別

    在 select/poll中,程序隻有在調用一定的方法後,核心才對所有監視的檔案描述符進行掃描,而epoll事先通過epoll_ctl()來注冊一 個檔案描述符,一旦基于某個檔案描述符就緒時,核心會采用類似callback的回調機制,迅速激活這個檔案描述符,當程序調用epoll_wait() 時便得到通知。

    epoll與select、poll不同,首先,其不用每次調用都向核心拷貝事件描述資訊,在第一次調用後,事件資訊就會與對應的epoll描述符關聯起來。其次,epoll不是通過輪詢,而是通過在等待的描述符上注冊回調函數,當事件發生時,回調函數負責把發生的事件存儲在就緒事件連結清單中,最後寫到使用者空間。

   epoll傳回後,該參數指向的緩沖區中即為發生的事件,對緩沖區中每個元素進行處理即可,而不需要像poll、select那樣進行輪詢檢查。

3.4epoll的優點

    1. 監視的描述符數量不受限制,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1GB記憶體的機器上大約是10萬左 右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。select的最大缺點就是程序打開的fd是有數量限制的。這對于連接配接數量比較大的伺服器來說根本不能滿足。雖然也可以選擇多程序的解決方案( Apache就是這樣實作的),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上線程間同步的高效,是以也不是一種完美的方案。

    2. IO的效率不會随着監視fd的數量的增長而下降。epoll不同于select和poll輪詢的方式,而是通過每個fd定義的回調函數來實作的。隻有就緒的fd才會執行回調函數。

    3. 支援電平觸發和邊沿觸發(隻告訴程序哪些檔案描述符剛剛變為就緒狀态,它隻說一遍,如果我們沒有采取行動,那麼它将不會再次告知,這種方式稱為邊緣觸發)兩種方式,理論上邊緣觸發的性能要更高一些,但是代碼實作相當複雜。

    4. mmap加速核心與使用者空間的資訊傳遞。epoll是通過核心與使用者空間mmap同一塊記憶體,避免了無謂的記憶體拷貝。

3.5epoll的模型

    EPOLL事件有兩種模型 Level Triggered (LT) 和 Edge Triggered (ET):

    LT(level triggered,水準觸發模式)是預設的工作方式,并且同時支援 block 和 non-block socket。在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,是以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。

    ET(edge-triggered,邊緣觸發模式)是高速工作方式,隻支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,并且不會再為那個檔案描述符發送更多的就緒通知,等到下次有新的資料進來的時候才會再次出發就緒事件。