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