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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章