1. 程式人生 > >Linux I/O複用中select poll epoll模型的介紹及其優缺點的比較

Linux I/O複用中select poll epoll模型的介紹及其優缺點的比較

關於I/O多路複用:

I/O多路複用(又被稱為“事件驅動”),首先要理解的是,作業系統為你提供了一個功能,當你的某個socket可讀或者可寫的時候,它可以給你一個通知。這樣當配合非阻塞的socket使用時,只有當系統通知我哪個描述符可讀了,我才去執行read操作,可以保證每次read都能讀到有效資料而不做純返回-1和EAGAIN的無用功。寫操作類似。作業系統的這個功能通過select/poll/epoll之類的系統呼叫來實現,這些函式都可以同時監視多個描述符的讀寫就緒狀況,這樣,**多個描述符的I/O操作都能在一個執行緒內併發交替地順序完成,這就叫I/O多路複用,這裡的“複用”指的是複用同一個執行緒。

一、I/O複用之select

1、介紹:
select系統呼叫的目的是:在一段指定時間內,監聽使用者感興趣的檔案描述符上的可讀、可寫和異常事件。poll和select應該被歸類為這樣的系統呼叫,它們可以阻塞地同時探測一組支援非阻塞的IO裝置,直至某一個裝置觸發了事件或者超過了指定的等待時間——也就是說它們的職責不是做IO,而是幫助呼叫者尋找當前就緒的裝置。
下面是select的原理圖:
這裡寫圖片描述

2、select系統呼叫API如下:

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int
select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

fd_set結構體是檔案描述符集,該結構體實際上是一個整型陣列,陣列中的每個元素的每一位標記一個檔案描述符。fd_set能容納的檔案描述符數量由FD_SETSIZE指定,一般情況下,FD_SETSIZE等於1024,這就限制了select能同時處理的檔案描述符的總量。

3、下面介紹一下各個引數的含義:
1)nfds引數指定被監聽的檔案描述符的總數。通常被設定為select監聽的所有檔案描述符中最大值加1;
2)readfds、writefds、exceptfds分別指向可讀、可寫和異常等事件對應的檔案描述符集合。這三個引數都是傳入傳出型引數,指的是在呼叫select之前,使用者把關心的可讀、可寫、或異常的檔案描述符通過FD_SET(下面介紹)函式分別新增進readfds、writefds、exceptfds檔案描述符集,select將對這些檔案描述符集中的檔案描述符進行監聽,如果有就緒檔案描述符,select會重置readfds、writefds、exceptfds檔案描述符集來通知應用程式哪些檔案描述符就緒。這個特性將導致select函式返回後,再次呼叫select之前,必須重置我們關心的檔案描述符

,也就是三個檔案描述符集已經不是我們之前傳入 的了。
3)timeout引數用來指定select函式的超時時間(下面講select返回值時還會談及)。

struct timeval
{
    long tv_sec;        //秒數
    long tv_usec;       //微秒數
};

4、下面幾個函式(巨集實現)用來操縱檔案描述符集:

void FD_SET(int fd, fd_set *set);   //在set中設定檔案描述符fd
void FD_CLR(int fd, fd_set *set);   //清除set中的fd位
int  FD_ISSET(int fd, fd_set *set); //判斷set中是否設定了檔案描述符fd
void FD_ZERO(fd_set *set);          //清空set中的所有位(在使用檔案描述符集前,應該先清空一下)
    //(注意FD_CLR和FD_ZERO的區別,一個是清除某一位,一個是清除所有位)

5、select的返回情況:
1)如果指定timeout為NULL,select會永遠等待下去,直到有一個檔案描述符就緒,select返回;
2)如果timeout的指定時間為0,select根本不等待,立即返回;
3)如果指定一段固定時間,則在這一段時間內,如果有指定的檔案描述符就緒,select函式返回,如果超過指定時間,select同樣返回。
4)返回值情況:
a)超時時間內,如果檔案描述符就緒,select返回就緒的檔案描述符總數(包括可讀、可寫和異常),如果沒有檔案描述符就緒,select返回0;
b)select呼叫失敗時,返回 -1並設定errno,如果收到訊號,select返回 -1並設定errno為EINTR。

6、檔案描述符的就緒條件:
在網路程式設計中,
1)下列情況下socket可讀:
a) socket核心接收緩衝區的位元組數大於或等於其低水位標記SO_RCVLOWAT;
b) socket通訊的對方關閉連線,此時該socket可讀,但是一旦讀該socket,會立即返回0(可以用這個方法判斷client端是否斷開連線);
c) 監聽socket上有新的連線請求;
d) socket上有未處理的錯誤。
2)下列情況下socket可寫:
a) socket核心傳送緩衝區的可用位元組數大於或等於其低水位標記SO_SNDLOWAT;
b) socket的讀端關閉,此時該socket可寫,一旦對該socket進行操作,該程序會收到SIGPIPE訊號;
c) socket使用connect連線成功之後;
d) socket上有未處理的錯誤。

二、I/O複用之poll

1、poll系統呼叫的原理與原型和select基本類似,也是在指定時間內輪詢一定數量的檔案描述符,以測試其中是否有就緒者。

2、poll系統呼叫API如下:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

3、下面介紹一下各個引數的含義:
1)第一個引數是指向一個結構陣列的第一個元素的指標,每個元素都是一個pollfd結構,用於指定測試某個給定描述符的條件。

struct pollfd
{
    int fd;             //指定要監聽的檔案描述符
    short events;       //指定監聽fd上的什麼事件
    short revents;      //fd上事件就緒後,用於儲存實際發生的時間
};

待監聽的事件由events成員指定,函式在相應的revents成員中返回該描述符的狀態(每個檔案描述符都有兩個事件,一個是傳入型的events,一個是傳出型的revents,從而避免使用傳入傳出型引數,注意與select的區別),從而告知應用程式fd上實際發生了哪些事件。events和revents都可以是多個事件的按位或。
2)第二個引數是要監聽的檔案描述符的個數,也就是陣列fds的元素個數;
3)第三個引數意義與select相同。

4、poll的事件型別:
這裡寫圖片描述
在使用POLLRDHUP時,要在程式碼開始處定義_GNU_SOURCE

5、poll的返回情況:
與select相同。

三、I/O複用之epoll

1、介紹:
epoll 與select和poll在使用和實現上有很大區別。首先,epoll使用一組函式來完成,而不是單獨的一個函式;其次,epoll把使用者關心的檔案描述符上的事件放在核心裡的一個事件表中,無須向select和poll那樣每次呼叫都要重複傳入檔案描述符集合事件集。

2、建立一個檔案描述符,指定核心中的事件表:

#include<sys/epoll.h>
int epoll_create(int size);
    //呼叫成功返回一個檔案描述符,失敗返回-1並設定errno。

size引數並不起作用,只是給核心一個提示,告訴它事件表需要多大。該函式返回的檔案描述符指定要訪問的核心事件表,是其他所有epoll系統呼叫的控制代碼。

3、操作核心事件表:

#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    //呼叫成功返回0,呼叫失敗返回-1並設定errno。

epfd是epoll_create返回的檔案控制代碼,標識事件表,op指定操作型別。操作型別有以下3種:

a)EPOLL_CTL_ADD, 往事件表中註冊fd上的事件;
b)EPOLL_CTL_MOD, 修改fd上註冊的事件;
c)EPOLL_CTL_DEL, 刪除fd上註冊的事件。

event引數指定事件,epoll_event的定義如下:

struct epoll_event
{
    __int32_t events;       //epoll事件
    epoll_data_t data;      //使用者資料
};

typedef union epoll_data
{
    void *ptr;
    int  fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data;

在使用epoll_ctl時,是把fd新增、修改到核心事件表中,或從核心事件表中刪除fd的事件。如果是新增事件到事件表中,可以往data中的fd上新增事件events,或者不用data中的fd,而把fd放到使用者資料ptr所指的記憶體中(因為epoll_data是一個聯合體,只能使用其中一個數據),再設定events。

3、epoll_wait函式
epoll系統呼叫的最關鍵的一個函式epoll_wait,它在一段時間內等待一個組檔案描述符上的事件。

#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    //函式呼叫成功返回就緒檔案描述符個數,失敗返回-1並設定errno。

timeout引數和select與poll相同,指定一個超時時間;maxevents指定最多監聽多少個事件;events是一個傳出型引數,epoll_wait函式如果檢測到事件就緒,就將所有就緒的事件從核心事件表(epfd所指的檔案)中複製到events指定的陣列中。這個陣列用來輸出epoll_wait檢測到的就緒事件,而不像select與poll那樣,這也是epoll與前者最大的區別,下文在比較三者之間的區別時還會說到。

四、三組I/O複用函式的比較

相同點:
1)三者都需要在fd上註冊使用者關心的事件;
2)三者都要一個timeout引數指定超時時間;
不同點:
1)select:
a)select指定三個檔案描述符集,分別是可讀、可寫和異常事件,所以不能更加細緻地區分所有可能發生的事件;
b)select如果檢測到就緒事件,會在原來的檔案描述符上改動,以告知應用程式,檔案描述符上發生了什麼時間,所以再次呼叫select時,必須先重置檔案描述符
c)select採用對所有註冊的檔案描述符集輪詢的方式,會返回整個使用者註冊的事件集合,所以應用程式索引就緒檔案的時間複雜度為O(n);
d)select允許監聽的最大檔案描述符個數通常有限制,一般是1024,如果大於1024,select的效能會急劇下降;
e)只能工作在LT模式。

2)poll:
a)poll把檔案描述符和事件繫結,事件不但可以單獨指定,而且可以是多個事件的按位或,這樣更加細化了事件的註冊,而且poll單獨採用一個元素用來儲存就緒返回時的結果,這樣在下次呼叫poll時,就不用重置之前註冊的事件;
b)poll採用對所有註冊的檔案描述符集輪詢的方式,會返回整個使用者註冊的事件集合,所以應用程式索引就緒檔案的時間複雜度為O(n)。
c)poll用nfds引數指定最多監聽多少個檔案描述符和事件,這個數能達到系統允許開啟的最大檔案描述符數目,即65535。
d)只能工作在LT模式。

3)epoll:
a)epoll把使用者註冊的檔案描述符和事件放到核心當中的事件表中,提供了一個獨立的系統呼叫epoll_ctl來管理使用者的事件,而且epoll採用回撥的方式,一旦有註冊的檔案描述符就緒,講觸發回撥函式,該回調函式將就緒的檔案描述符和事件拷貝到使用者空間events所管理的記憶體,這樣應用程式索引就緒檔案的時間複雜度達到O(1)。
b)epoll_wait使用maxevents來制定最多監聽多少個檔案描述符和事件,這個數能達到系統允許開啟的最大檔案描述符數目,即65535;
c)不僅能工作在LT模式,而且還支援ET高效模式(即EPOLLONESHOT事件,讀者可以自己查一下這個事件型別,對於epoll的執行緒安全有很好的幫助)。

select/poll/epoll總結:
這裡寫圖片描述