在前面的文章中讲了实现IO复用的两种方式:select和poll。今天主要讲一个更为高效的函数epoll。
epoll
epoll能显著提高在大量链接中,只有少量活跃连接时的cpu利用率。因为,首先epoll可以复用监听的文件描述符集合,而不用每次在等待事件之前重新准备被监听的文件描述符集合。其次是因为epoll获取就绪事件时,不用遍历整个监听事件的集合,而是只需要遍历那些被内核IO一步唤醒的放入ready队列的文件描述符集合。
Epoll的主要API有
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
①、int epoll_create(int size);
用来创建一个epoll句柄,底层实现也就是生成一个红黑树的树根。它的参数只有一个size,设置监听文件描述符的个数,是个建议值,epoll后期监听的文件描述符上限和size无关。需要说明的是:当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
②、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
控制某个epoll监听文件描述符上的事件
第一个参数是epoll_create()创建的epoll句柄,
第二个参数是op操作,有三种:
EPOLL_CTL_ADD,添加一个新的fd到到红黑树上,
EPOLL_CTL_MOD,修改对应fd的监听事件,
EPOLL_CTL_DEL,删除一个fd,也就是将其从红黑树上摘下来。
第三个参数是监听的文件描述符fd,
第四个参数是epoll_event结构体指针,这个结构体里面有两个成员:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
第一个是event,监听的文件描述符的事件,一般使用的事件都有一下几个:
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
第二个data也是一个epoll_data_t联合体,定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
在这个结构体中最常用的就是两个:void *ptr 和 int fd。因为这是一个联合体。同时只能使用其中的一个成员。一般情况下,我们直接使用fd这个成员,传入监听的文件描述符,和epoll_ctl函数的第三个参数保持一致。当我们想要进一步提高epoll的性能,可以使用void *ptr这个泛型指针。注册回调函数,当监听的事件满足时,直接调用该回调函数去执行相应的逻辑。
③、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
Epoll_wait函数是等待监听的事件就绪,类似于select函数和epoll函数。
第一个参数是epoll句柄,
第二个参数是epoll_event结构体类型的数组,
第三个参数是这个数组的大小,
第四个参数是设置超时,timeout。
ET模式和LT模式
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait的调用,提高应用程序效率。
LT模式
LT模式,也就是水平触发模式,是epoll的默认工作方式,相当于比较快一点的poll。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,即在这种模式下,只要缓冲区有数据就会触发epoll_wait()函数。。可以设置为阻塞版本,也可以设置为非阻塞版本。
ET模式
ET模式,边沿触发,是一种高效的工作模式,在这种模式下,当文件描述符变为就绪状态后,内核通过epoll通知,便不会在通知,及时缓冲区里面还有数据也不会再通知,在这种模式下只能使用非阻塞版本,是为了避免当一个文件句柄阻塞读或写操作时,把处理多个文件描述符的任务饿死。
只有当read或wirte函数返回EAGIAN错误码时,才需要挂起等待,但并不是说每次都需要循环读,直到读到EAGIN才结束,当我们读到的字节数小于缓冲区大小是时,就可以认为读事件处理完成。使用epoll ET模式,可以减少epoll_wait()函数的调用次数,提高效率。
示例代码
以一个epoll的服务器程序结束本文。
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<ctype.h>
#include<sys/epoll.h>
#include<fcntl.h>
#define MYPORT 8888
#define BACKLOG 10
#define MAXDATASIZE 1024
#define FILEMAX 3000
#define size 20 //监听的事件数
int main()
{
int i,j,maxi;
int listenfd,connfd,sockfd; //定义套接字描述符
int nready; //接受epool_wait返回值
int numbytes; //接受recv返回值
char buf[MAXDATASIZE]; //发送缓冲区
struct epoll_event evt; //注册监听事件
struct epoll_event ep[size]; //满足事件
//定义IPV4套接口地址结构
struct sockaddr_in seraddr; //service 地址
struct sockaddr_in cliaddr; //client 地址
int sin_size;
//初始化IPV4套接口地址结构
seraddr.sin_family =AF_INET; //指定该地址家族
seraddr.sin_port =htons(MYPORT); //端口
seraddr.sin_addr.s_addr = INADDR_ANY; //IPV4的地址
bzero(&(seraddr.sin_zero),8);
//socket()函数
if((listenfd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
//地址重复利用
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
{
perror("setsockopt");
exit(1);
}
//bind()函数
if(bind(listenfd,(struct sockaddr *)&seraddr,sizeof(struct sockaddr))==-1)
{
perror("bind");
exit(1);
}
//listen()函数
if(listen(listenfd,BACKLOG)==-1)
{
perror("listen");
exit(1);
}
int epfd = epoll_create(size); //创建句柄
if(epfd == -1)
{
perror("epoll_create errror!\n");
exit(1);
}
evt.events = EPOLLIN ;
evt.data.fd = listenfd;
//注册监听事件listenfd到epfd
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&evt);
if(ret == -1)
{
perror("epoll_ctl error!\n");
exit(1);
}
while(1)
{
nready = epoll_wait(epfd,ep,size,-1); //监听事件是否就绪
if(nready < 0)
{
perror("epoll_wait error!\n");
exit(1);
}
for(i = 0;i < nready;i++)
{
if(!(ep[i].events & EPOLLIN))
{
continue;
}
else if(ep[i].data.fd == listenfd) //listenfd就绪,客户端发起连接
{
sin_size = sizeof(cliaddr);
if((connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&sin_size))==-1)
{
perror("accept");
exit(1);
}
printf("client IP: %s\t PORT : %d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
//修改connfd为非阻塞读
evt.events = EPOLLIN | EPOLLET; //ET模式
int flag = fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
evt.data.fd = connfd;
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&evt);
if(ret == -1)
{
perror("epoll_ctl error!\n");
exit(1);
}
}
else
{
sockfd = ep[i].data.fd;
memset(buf,0,sizeof(buf));
// sockfd设置为非阻塞模式,数据还没有发给接收端时,调用recv就会返回-1,并且errno会被设为EAGAIN.
numbytes = recv(sockfd,buf,1024,0)
if(numbytes == -1 && EAGAIN != errno)
{
perror("recv error!\n");
exit(1);
}
if(numbytes == 0)//客户端断开连接
{
printf("client[%d],close!\n",i);
int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);
if(ret == -1)
{
perror("epoll_ctl error!\n");
exit(1);
}
close(sockfd);
}
if(numbytes > 0 )
{
send(sockfd,buf,numbytes,0);
numbytes = recv(sockfd,buf,1024,0);
}
}
}
}
close(listenfd);
close(epfd);
return 0;
}