一、 介绍
Epoll 是一种高效的管理socket的模型,相对于select和poll来说具有更高的效率和易用性。传统的select以及poll的效率会因为 socket数量的线形递增而导致呈二次乃至三次方的下降,而epoll的性能不会随socket数量增加而下降。标准的linux-2.4.20内核不支持epoll,需要打patch。本文主要从linux-2.4.32和linux-2.6.10两个内核版本介绍epoll。
select运行机制
select()的机制中提供一种fd_set
的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。
select机制的问题
fd_set
集合从用户态拷贝到内核态,如果fd_set
集合很大时,那这个开销也很大fd_set
,如果fd_set
集合很大时,那这个开销也很大fd_set
集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。
二、 Epoll的使用
epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:
所用到的数据结构
typedef union epoll_data {
void ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; / Epoll events /
epoll_data_t data; / User data variable /
};
结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,其中epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据,例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件可能的取值为:EPOLLIN :表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:表示对应的文件描述符设定为edge模式;
所用到的函数:
1、epoll_create函数
函数声明:int epoll_create(int size)
该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。在linux-2.4.32内核中根据size大小初始化哈希表的大小,在linux2.6.10内核中该参数无用,使用红黑树管理所有的文件描述符,而不是hash。
2、epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值
如果调用成功返回0,不成功返回-1
EPOLL_CTL_ADD 注册、
EPOLL_CTL_MOD 修改、
EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
3、epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(ms);-1永不超时,直到有事件产生才触发,0立即返回。
返回发生事件数。-1有错误。
有关EPOLLOUT(写)监听的使用,理解起来有点麻烦。因为监听一般都是被动操作,客户端有数据上来需要读写(被动的读操作),EPOLIN监听事件很好理解,但是服务器给客户发送数据是个主动的操作,写操作如何监听呢?
如果将客户端的socket接口都设置成 EPOLLIN | EPOLLOUT(读,写)两个操作都设置,那么这个写操作会一直监听,有点影响效率。EPOLLOUT(写)监听的使用场,一般说明主要有以下三种使用场景:
如果发送遇到EAGAIN/EWOULDBLOCK 就去加入EPOLLOUT,等待直到socket输出缓冲区可以写,那么epoll_wait就能触发EPOLLOUT,再去写数据。见示例2
简单的应用
跟网上的其它代码修改了一下,可以直接编译运行
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000
/*
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; // Epoll events
epoll_data_t data; // User data variable
};
*/
void setnonblocking(int sock)
{
int opts;
opts=fcntl(sock,F_GETFL);
if(opts<0)
{
perror("fcntl(sock,GETFL)");
exit(1);
}
opts = opts|O_NONBLOCK;
if(fcntl(sock,F_SETFL,opts)<0)
{
perror("fcntl(sock,SETFL,opts)");
exit(1);
}
}
int main(int argc, char* argv[])
{
int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
if ( 2 == argc )
{
if( (portnumber = atoi(argv[1])) < 0 )
{
fprintf(stderr,"Usage:%s portnumber\n",argv[0]);
return 1;
}
}
else
{
fprintf(stderr,"Usage:%s portnumber\n",argv[0]);
return 1;
}
//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
struct epoll_event ev, events[20];
//生成用于处理accept的epoll专用的文件描述符
epfd=epoll_create(256);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//把socket设置为非阻塞方式
//setnonblocking(listenfd);
//设置与要处理的事件相关的文件描述符
ev.data.fd=listenfd;
//设置要处理的事件类型
ev.events = EPOLLIN|EPOLLET;
//ev.events=EPOLLIN;
//注册epoll事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
char *local_addr="127.0.0.1";
inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);
serveraddr.sin_port=htons(portnumber);
bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
listen(listenfd, LISTENQ);
maxi = 0;
while ( 1 ) {
//等待epoll事件的发生
nfds=epoll_wait(epfd, events, 20, 500);
//处理所发生的所有事件
for(i=0;i
示例2
void write_handler2(ev_data *ptr)
{
//判断指针, 可自由发挥
if(!ptr || !ptr->buffer){
close(ptr->fd);
puts(" write_handler2 ptr is empty ");
return;
}
//如果没数据,就直接返回了
if(ptr->nread <= 0){
printf("buffer is empty , ptr->nread:%d \n",ptr->nread);
return;
}
static struct epoll_event ev ={0,{0}};
//要发送的字节数
int left = ptr->nread - ptr->start_write_index;
//指针位置
char * pbuffer = ptr->buffer + ptr->start_write_index;
int nwriten = -1;
int write_bytes = 0;
printf("begin write , nread:%d, start_write_index:%d,left:%d\n",
ptr->nread,ptr->start_write_index,left);
while(left > 0){
nwriten = write(ptr->fd,pbuffer,left);
//如果有错误
if(nwriten <= 0){
/*
socket输出缓冲区满了
那就加入EPOLLOUT事件,等epoll_wait返回通知你
*/
if(errno < 0 && ( EWOULDBLOCK == errno || EAGAIN == errno)){
//记录一下, 下一次要从哪里开始
ptr->start_write_index += write_bytes;
//这个纯粹为了防止重复的epoll_ctl,额外增加负担的,你不做判断也没事
if(EPOLLOUT & ptr->events)
return;
//增加EPOLLOUT事件
ptr->events |= EPOLLOUT;
ev.events = ptr->events;
ev.data.ptr = ptr;
//修改事件
epoll_ctl(ptr->epoll_fd,EPOLL_CTL_MOD,ptr->fd,&ev);
printf("socket buff is full , nread:%d,start_write_index:%d,left:%d\n",
ptr->nread,ptr->start_write_index,left);
return;
}
else{
//如果出错了, 这里你可以自由发挥
close(ptr->fd);
free_event(ptr);
perror("write error");
return;
}
}
pbuffer += nwriten;
left -= nwriten;
write_bytes += nwriten;
}
//到这里,说明该写的都写了, 重置一下读写位置
ptr->start_write_index = ptr->nread = 0;
//这个判断纯粹为了防止每次都去epoll_ctl,不加判断也行
if(EPOLLOUT & ptr->events) {
//把 EPOLLOUT 删了 ,这样就跟原来一样还是EPOLLIN|EPOLLET
ptr->events &= ~EPOLLOUT;
ev.events =ptr->events;
ev.data.ptr = ptr;
//修改一下
epoll_ctl(ptr->epoll_fd, EPOLL_CTL_MOD, ptr->fd, &ev);
}
}