epoll 實現 select接口

轉載請註明來源:http://blog.csdn.net/letian0805/article/details/16891207

        公司某部分軟件用的是開源庫,該開源庫中用的是select。衆所周知,select能處理的最多文件描述符受限於fd_set,系統默認最大文件描述符是1024。對於網絡連接來說,1024遠遠不夠,所以需要使用epoll來實現,總監將這件事交給了我。但是,太大的代碼改動可能帶來額外的bug。所以,我第一想法就是用epoll實現select接口。特意寫這篇博文與大家分享。

        首先,我實現了epfd_set,以及操作epfd_set的宏。代碼如下:

#include <sys/epoll.h>
#define EPFD_MAX_FD (64*1024)
#define EPFD_PER_UINT (8*sizeof(uint32_t))
typedef uint32_t epfd_set[EPFD_MAX_FD/EPFD_PER_UINT + 1];

#define EPFD_ZERO(_epfd_set) memset((_epfd_set), 0, sizeof(_epfd_set))

#define EPFD_CLR(_fd, _epfd_set) \
do{ \
    uint32_t *bitmap = ((uint32_t *)(*(_epfd_set)))[(_fd)/EPFD_PER_UINT]; \
    *bitmap &= ~(1<<((_fd) % EPFD_PER_UINT)); \
}while(0)

#define EPFD_SET(_fd, _epfd_set) \
do{ \
    uint32_t *bitmap = ((uint32_t *)(*(_epfd_set)))[(_fd)/EPFD_PER_UINT]; \
    *bitmap |= (1<<((_fd) % EPFD_PER_UINT)); \
}while(0)

#define EPFD_ISSET(_fd, _epfd_set) (((uint32_t *)(*(_epfd_set)))[(_fd)/EPFD_PER_UINT] & (1<<((_fd) % EPFD_PER_UINT)))

   接下來,我們可以來實現epoll版的select了——ep_select。在實現ep_select之前,我們需要了解select的各個參數的含義。下面是select的原型。


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

    第一個參數 nfds表示放進select裏的文件描述符的個數,由於select和fd_set的機制決定,所以該參數是所有fd_set中最大的文件描述符+1 (之所以+1是因爲文件描述符從0開始)。

    第二個參數readfds用於檢測描述符是否可讀,第三個參數writefds用於檢測描述符是否可寫,第四個參數exceptfds檢測是否有帶外數據(緊急數據),第五個參數timeout用於設定select的超時時間。

   select檢測到符合要求的狀態時會立即返回符合要求的文件描述符的個數;如果超時了,則返回0,錯誤返回-1並設置全局錯誤碼。

    由於ep_select參數、返回值和select的參數一樣,所以ep_select的原型如下:

int ep_select(int nfds, epfd_set *readfds, epfd_set *writefds, epfd_set *exceptfds, struct timeval *timeout);

    接着,我們再看epoll相關的函數。epoll主要由3個函數組成epoll_create、epoll_ctl、epoll_wait。epoll_create首先創建epoll專用的文件描述符,利用epoll_ctl可以將需要監控的文件描述符添加到epoll,或者修改需要檢查的狀態,或者從epoll中移除,epoll_wait則是負責在給定時間內檢查文件描述符狀態。

    epoll_create比較好理解,參數是能同時監控的文件描述符最大數量。

    epoll_wait也比較好理解,第一個參數epfd是通過epoll_create創建的,第二個參數events用於存儲返回的文件描述符狀態,第三個參數maxevents表示第二個參數最多能存儲文件描述符狀態的數量(也就是events的大小),第四個參數timeout是超時時間(毫秒)。

    這三個函數中,最難用的則是epoll_ctl。該函數有4個參數,第一個參數epfd則是由epoll_create創建的epoll描述符;第二個參數op則是需要進行的操作,有三個選項:EPOLL_CTL_ADD  將文件描述符添加到epoll裏,EPOLL_CTL_MOD  修改對應文件描述符需要監控的狀態, EPOLL_CTL_DEL則是將文件描述符從epoll中移除。epoll能監控的文件描述符的狀態有:EPOLLIN 檢查是否可讀, EPOLLOUT 檢查是否可寫,EPOLLRDHUP  檢查遠端是否關閉連接(man手冊給出的解釋是特別用於邊緣觸發的方式),EPOLLPRI 檢查是否有帶外數據(緊急數據),EPOLLET 檢測邊緣觸發狀態(需要遠端將連接設置爲邊緣觸發模式,默認模式是水平觸發模式),EPOLLONESHOT 一次性檢測(當下次再檢測時,需要再次調用epoll_ctl添加需要檢測的狀態), 這些都是需要通過epoll_ctl來設置的,後面兩個是自動檢測的:EPOLLERR 文件描述符出錯, EPOLLHUP 文件描述符已經關閉(不一定是遠端關閉的)。由此我們可以知道:select中的第二個參數對應了EPOLLIN,第三個參數對應了EPOLLOUT,第四個參數對應了EPOLLPRI。

    接下來,我們看一下struct epoll_event這個類型。這是個結構體,有兩個成員:uint32_t events; epoll_data_t data; 第一個成員是 要監控 或者 已獲取 的文件描述符狀態,第二個成員是個聯合體,有4種類型,主要用於判斷返回的文件描述符狀態是屬於哪個文件描述符(聯合體中的fd)或者自定義的變量(聯合體中的u32、u64、ptr,且該變量一定能用於查找對應的文件描述符)。好了,讓我們開工實現ep_select。

        我們先實現一個函數用於將文件描述符添加到epoll或修改文件描述符需要監控的狀態,代碼如下:

int epoll_add_fd(int epfd, int fd, uint32_t evts)
{
    struct epoll_event evt;
    evt.events = (evts | EPOLLONESHOT);
    evt.data.fd = fd;
    retrun epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt); 
}

int epoll_remove_fd(int epfd, int fd)
{
    struct epoll_event evt;
    evt.events = 0;
    evt.data.fd = fd;
    return epoll_ctl(epfd, EPOLL_CTL_RM, fd, &evt);
}
接下來實現ep_select, 代碼如下:

static epfd_set readfds_bk;
static epfd_set writefds_bk;
static epfd_set exceptfds_bk;
static struct epoll_event events_buffer[EPFD_MAX_FD + 1];

int ep_select(int nfds, epfd_set *readfds, epfd_set *writefds, epfd_set *exceptfds, struct timeval *timeout)
{
    int msec = timeout ? (timeout->tv_sec * 1000 + timeout->tv_usec / 1000) : -1;
    if (nfds <= 0 || (!readfds && !writefds && !expectfds)){
EMPTY:
        if (msec > 0){
            usleep( msec * 1000 );
        }
        return 0;
    }
    if (readfds){
        readfds_bk = *readfds;
        EPFD_ZERO(readfds);
    }else{
        EPFD_ZERO(&readfds_bk);
    }
    if (writefds){
        writefds_bk = *writefds;
        EPFD_ZERO(writefds);
    }else{
        EPFD_ZERO(&writefds_bk);
    }
    if (exceptfds){
        exceptfds_bk = *exceptfds;
        EPFD_ZERO(exceptfds);
    }else{
        EPFD_ZERO(&exceptfds);
    }
    static int epfd = 0;
    if (epfd == 0){
        epfd = epoll_create(EPFD_MAX_FD + 1);
    }
    int fd_count = 0;
    int fd;
    uint32_t evts = 0;
    for (fd = 0; fd < nfds; fd++){
        evts = 0;
        if (EPFD_ISSET(epfd, fd, readfds_bk)){
            evts |= EPOLLIN;
        }
        if (EPFD_ISSET(fd, writefds_bk)){
            evts |= EPOLLOUT;
        }
        if (EPFD_ISSET(fd, exceptfds_bk)){
            evts |= EPOLLPRI;
        }
        if (evts){
            fd_count++;
            epoll_add_fd(fd, evts);
        }
    }

    if (fd_count == 0){
        goto EMPTY;
    }

    int result = epoll_wait(epfd, &events_buffer, msec);
    int i;
    for (i = 0; i < result; i++){
        fd = events_buffer[i].data.fd;
        epoll_remove_fd(epfd, fd);
        if (EPFD_ISSET(fd, &readfds_bk)){
            EPFD_SET(fd, readfds);
        }
        if (EPFD_ISSET(fd, &writefds_bk)){
            EPFD_SET(fd, writefds);
        }
        if (EPFD_ISSET(fd, &exceptfds_bk)){
            EPFD_SET(fd, exceptfds);
        }
    }
    return result;
}


 

 

請注意:
1、以上代碼沒有考慮多線程問題,如果需要多線程,則events_buffer、readfds_bk等需要動態分配。
2、以上代碼由於考慮的是接口替換,所以效率會比epoll通用方法有所降低。
3、由於在CSDN裏編輯代碼不大方便,有錯誤之處還望各位指正。

發佈了22 篇原創文章 · 獲贊 5 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章