转载请注明来源: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);
第二个参数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_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里编辑代码不大方便,有错误之处还望各位指正。