epoll是为了处理大批量句柄而做了改进的poll。它是在Linux2.5.44内核中被引进的,它具备之前所说的select、poll的所有优点,它是Linux2.6下公认的最好的多路IO就绪通知方法。
epoll接口函数:
#include <sys/epoll.h>
int epoll_create(int size);
说明:该函数用于创建一个epoll模型。
参数:size表示最大监听的文件描述符个数,但在Linux2.6.8内核之后已被废弃。
返回值:成功时,返回非负的epoll模型的句柄,失败时,返回-1,并设置errno。
备注:epoll模型用完之后,必须使用close()关闭,否则,将会出现资源泄露。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
说明:此函数用于向epoll模型中注册、删除或修改要监听的fd及事件,因op参数的取值不同,函数功能不同,不同于select是在要监听时才告诉内核要监听的文件描述符及事件,而在epoll中是先注册要监听的事件。
参数:
- epfd:epoll模型的句柄。
- op:三种取值:EPOLL_CTL_ADD,向epoll模型中注册新的要监听的fd以及事件;EPOLL_CTL_MOD,修改epoll模型中已经注册的fd的要监听的事件;EPOLL_CTL_DEL,删除epoll模型中已经注册的fd及事件。
- fd:要监视的文件描述符。
- event:fd上要监视的事件,可以是下面几个宏的集合,更多关于
struct epoll_event
结构体的说明见下面备注。
宏 | 功能 |
---|---|
EPOLLIN | 文件描述符可读(包括对端SOCKET正常关闭) |
EPOLLOUT | 文件描述符可写 |
EPOLLPRI | 文件描述符上有紧急的数据(带外数据)可读 |
EPOLLERR | 文件描述符发生错误 |
EPOLLHUP | 文件描述符被挂断 |
EPOLLET | 将epoll设为边缘触发模式 |
EPOLLONESHOT | 只监听一次事件,当事件发生后还需要再次监听这个socket的话,需要再次往epoll模型中注册该socket |
返回值:成功时,返回0,失败时,返回-1,并设置errno。
备注:
struct epoll_event结构不仅用于epoll_ctl()控制fd与事件的注册、删除、修改,还用于epoll_wait()取回已发生事件,下面来说说struct epoll_event结构体。
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event结构共包含两个字段:events,在epoll_ctl()中表示要监听的事件集,在epoll_wait()中表示已经发生的事件集;data,data的数据类型为联合体epoll_data,共有四个字段,无论使用那个字段,在epoll_ctl()中传入的值会在epoll_wait()中原样传出,具体要使用联合体的那个字段,需要根据具体需求来定,例如:要想在事件发生时知道是那个文件描述符上的事件发生,便可使用fd字段,在epoll_ctl()中将联合体的fd字段置为要监听的文件描述符,这样在epoll_wait()中联合体的fd字段便为所发生事件的文件描述符;另外,如果想要在事件发生的时候,需要使用特定的数据,就可在epoll_ctl()中使用联合体的ptr字段,如果将ptr字段置为特定数据的地址,那么在事件发生时,将ptr的值强转成指定类型的指针再解引用便可拿到指定数据用于事件的处理(void *是泛型)。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
说明:该函数用于收集在epoll模型监控的事件中已发生的事件。
参数:
- epfd:epoll模型的句柄。
- events:指向由用户开辟好的struct epoll_event结构体数组,该数组用于接收已发生的事件。此参数不可以为空,内核只负责将数据拷贝进该空间,并不会为我们分配空间。
- maxevents:events指向数组的大小,该值不能大于epoll_create()中size的大小。
- timeout:单位ms,作用同poll的timeout参数。
返回值:成功,返回已发生事件的文件描述符个数,失败,返回-1,并设置errno,返回0,表示timeout超时。
epoll的工作原理:
1.当调用epoll_create()时,内核会创建eventpoll
结构体,用于管理epoll_ctl()注册进来的文件描述符及之上要监听的事件,该结构体中有两个重要的字段rbr、rdlist与epoll的使用密切相关,rbr为红黑树的根结点,通过这棵红黑树可以找到所有注册进epoll模型的文件描述符及上面的事件,rdlist为双向链表的头结点,该双向链表中存储着将要通过epoll_wait()返回给用户的已发生的事件,每一个epoll对象只有一个eventpoll结构体。
2.在epoll中,每注册一个文件描述符及上面的监听事件,内核都会创建一个epitem结构体,并将此事件加入红黑树中,利用红黑树管理事件是非常高效的(红黑树的增、删、查、改时间复杂度为lg^N,N是红黑树的高度),该结构体有几个比较重要的成员:rbn:红黑树的节点便是基于该结构体的rbn成员;rdllink:双线链表的节点便是基于rdllink成员;ffd:要监视的时间句柄;ep:指向其所属的eventpoll结构体;event:要监视的事件集合。
3.在将事件加入红黑树的同时,内核会将该事件与网卡驱动程序建立回调关系,在事件发生的时候,会调用该回调方法在红黑树中查找,如该事件在红黑树中,则将该事件相关的信息加入双向链表中,若不存在,则由内核另外处理。
4.当用户调用epoll_wait()检查是否有事件发生的时候,内核只需检查eventpoll中的双向链表中是否有元素,如果有元素,则把链表中的数据拷贝进epoll_wait()的缓冲区中,同时将发生事件的文件描述符个数返回,此操作事件复杂度为O(1)。
优点:
- 可监视的文件描述符无上限,通过epoll_ctl()注册的文件描述符,内核是通过红黑树管理的,所以文件描述符的数量无上限。
- 基于事件就绪通知方式,一旦事件就绪,内核会采用回调机制处理该文件描述符,不会因为文件描述符的数量增加而影响监听事件就绪的性能。
- 维护就绪队列,当事件就绪时,会将事件放入内核的双向链表中,这样在调用epoll_wait()时获取就绪事件时,只需取双向链表中的数据便可,时间复杂度为O(1)。
epoll的LT/ET模式:
epoll可以工作在两种模式下:LT(水平触发,默认)、ET(边缘触发)。
水平触发(LT)模式:
epoll默认是工作在水平触发模式的,水平触发模式的特点如下:当事件到来时,用户可以不立即处理或者只处理一部分,例如:当接收缓冲区中有数据到来时,本次不处理该数据,或者本次只读取一半的数据,下次调用epoll_wait()时,仍会返回该事件,直到缓冲区中的数据处理完,epoll_wait()才不会返回该事件。水平触发模式支持阻塞式读写和非阻塞式读写。
边缘触发(ET)模式:
如果,我们在epoll_ctl()中注册文件描述符时使用了EPOLLET宏,那么epoll将工作于边缘触发(ET)模式。工作在ET模式下时,当有数据到来时,必须立即处理完该数据,否则,如果不处理或者只处理一部分,在下次调用epoll_wait()时,该事件将不会返回,除非再次有新的数据到来,调用epoll_wait()才会返回该事件,ET模式比LT模式性能更高(epoll_wait()的返回次数减少了),ET模式只支持非阻塞式读写。
为什么epoll的ET模式只支持非阻塞式读写?
当epoll工作于ET模式的时候,当有事件发生的时候,只会通知一次,那么当数据到来时,就需要循环read(),直到数据读取完。如果采用阻塞的方式读取,那么在最后一次read()的时候,因为缓冲区已经没有数据,read()便会阻塞等待数据的到来,此时便会影响其他文件描述符的读写及后续逻辑,但是采用非阻塞的方式read(),最后一次即使缓冲区没有数据,read()也不会阻塞,而是返回-1,并设置errno为EAGAIN或EWOULDBLOCK(这两个宏值相同),此时我们只要检测到read()返回-1并且errno为EAGAIN或EWOULDBLOCK即可说明数据读取完成,可以继续处理其他文件描述符或逻辑。
基于epoll的ECHO服务器:
LT模式下的server.c:
#include<stdio.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<string.h>
void HandleConnect(int listen_sock,int epoll_fd){
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int connect_sock = accept(listen_sock,(struct sockaddr*)&addr,&len);
if(connect_sock < 0){
perror("accept");
return;
}
printf("client %s:%d connect\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
struct epoll_event ev;
ev.data.fd = connect_sock;
ev.events = EPOLLIN;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,connect_sock,&ev);
if(ret == -1){
perror("epoll_ctl");
return;
}
return;
}
void HandleRequest(int connect_sock,int epoll_fd){
char buf[1024] = {0};
ssize_t readsize = read(connect_sock,buf,sizeof(buf)-1);
if(readsize < 0){
perror("read");
return;
}else if(readsize == 0){
printf("client say goodbay\n");
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_DEL,connect_sock,NULL);
close(connect_sock);
if(ret == -1){
perror("epoll_ctrl");
return;
}
return;
}else{
printf("client $%s",buf);
write(connect_sock,buf,strlen(buf));
return;
}
}
int main(int argc,char *argv[]){
if(argc != 3){
printf("Usage :./server ip port\n");
return -1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
perror("socket");
return -2;
}
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
int ret = bind(sock,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
if(ret < 0){
perror("bind");
return -3;
}
ret = listen(sock,5);
if(ret < 0){
perror("listen");
return -4;
}
int epoll_fd = epoll_create(10);
if(epoll_fd < 0){
perror("epoll_create");
return -5;
}
struct epoll_event ev;
ev.data.fd = sock;
ev.events = EPOLLIN;
ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,sock,&ev);
if(ret < 0){
perror("epoll_ctl");
return -6;
}
while(1){
struct epoll_event evs[10];
int size = epoll_wait(epoll_fd,evs,sizeof(evs)/sizeof(struct epoll_event),-1);
if(size < 0){
perror("epoll_wait");
continue;
}
int i = 0;
for(;i < size; i++){
if(!(evs[i].events & EPOLLIN)){
continue;
}
if(evs[i].data.fd == sock){
HandleConnect(sock,epoll_fd);
}else{
HandleRequest(evs[i].data.fd,epoll_fd);
}
}
}
return 0;
}
ET模式下的server.c:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<errno.h>
void SetNonBlock(int fd){
int fl = fcntl(fd,F_GETFL);
if(fl == -1){
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,fl|O_NONBLOCK);
}
void HandleConnect(int listen_sock,int epoll_fd){
while(1){
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int connect_sock = accept(listen_sock,(struct sockaddr*)&addr,&len);
if(connect_sock == -1){
if(errno == EWOULDBLOCK){
return;
}else{
continue;
}
}
printf("client %s:%d\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
SetNonBlock(connect_sock);
struct epoll_event ev;
ev.events = EPOLLIN |EPOLLET;
ev.data.fd = connect_sock;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,connect_sock,&ev);
if(ret == -1){
perror("epoll_ctl");
continue;
}
}
}
ssize_t NonBlockRead(int fd,char *buf,int size){
ssize_t total_size = 0;
while(1){
ssize_t cur_size = read(fd,buf+total_size,1024);
if(cur_size == 0){
return 0;
}
if(cur_size < 1024){
if(cur_size != -1){
total_size += cur_size;
break;
}else if(errno == EWOULDBLOCK){
break;
}else{
perror("read");
return -1;
}
}
total_size += cur_size;
if(total_size > size){
return -1;
}
}
return total_size;
}
void HandleRequest(int connect_sock,int epoll_fd){
char buf[1024*10] = {0};
ssize_t readsize = NonBlockRead(connect_sock,buf,sizeof(buf)-1);
if(readsize == -1){
printf("NonBlockRead error!\n");
return;
}
if(readsize == 0){
epoll_ctl(epoll_fd,EPOLL_CTL_MOD,connect_sock,NULL);
close(connect_sock);
printf("client say goodbay!\n");
return;
}
printf("client:%s",buf);
write(connect_sock,buf,strlen(buf));
}
int main(int argc,char *argv[]){
if(argc != 3){
printf("Usage :./server ip port\n");
return -1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
perror("socket");
return -2;
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
int ret = bind(sock,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
if(ret < 0){
perror("bind");
return -3;
}
ret = listen(sock,5);
if(ret < 0){
perror("listen");
return -4;
}
int epoll_fd = epoll_create(10);
if(epoll_fd < 0){
perror("epoll_create");
return -4;
}
SetNonBlock(sock);
struct epoll_event event;
event.data.fd = sock;
event.events = EPOLLIN |EPOLLET;
ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,sock,&event);
if(ret < 0){
perror("epoll_ctl");
return -5;
}
while(1){
struct epoll_event evs[10];
int size = epoll_wait(epoll_fd,evs,sizeof(evs)/sizeof(struct epoll_event),-1);
if(size < 0){
perror("epoll_wait");
continue;
}
int i = 0;
for(;i < size;i++){
if(!(evs[i].events & EPOLLIN)){
continue;
}
if(evs[i].data.fd == sock){
HandleConnect(sock,epoll_fd);
}else{
HandleRequest(evs[i].data.fd,epoll_fd);
}
}
}
return 0;
}
client.c:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
int main(int argc,char *argv[]){
if(argc != 3){
perror("Usage :./server ip port\n");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
perror("socket");
return -1;
}
int ret = connect(sock,(struct sockaddr*)&addr,sizeof(addr));
if(ret < 0){
perror("connect");
return -2;
}
while(1){
char buf[1024] = {0};
printf("> ");
fflush(stdout);
read(0,buf,sizeof(buf)-1);
ssize_t writesize = write(sock,buf,strlen(buf));
if(writesize < 0){
perror("write");
continue;
}
ssize_t readsize = read(sock,buf,sizeof(buf)-1);
if(readsize < 0){
perror("read");
continue;
}
if(readsize == 0){
printf("server close\n");
break;
}
buf[readsize] = 0;
printf("server say:%s",buf);
}
close(sock);
return 0;
}
结果演示:
LT模式:
ET模式: