一、epoll简介

  • 概念:​epoll是Linux特有的I/O复用函数。它在实现和使用上与select、 poll有很大差异
  • 如何使用:​首先,epoll使用​一组函数​来完成任务,而​不是单个函数​。其次,epoll把用户关心的文件描述符上的事件​放在内核里的一个事件表​中,从而​无须像select和poll那样每次调用都要重复传入文件描述符集或事件集

APUE编程:25---高级I/O之(IO多路复用:epoll()函数)_IO复用

二、epoll_create函数

#include <sys/epoll.h>
int epoll_create(int size);
  • 功能:​调用epoll_create方法创建一个epoll的句柄,该句柄代表着一个事件表
  • 参数:​size参数现在并不起作用,它只是给内核一个提示,告诉内核事件表需要多大(具体有多少个事件,还是依靠后面的epoll_wait参数来处理)
  • 返回值:
  • 成功:返回epoll句柄,它会占用一个fd值(使用完也需要关闭)
  • 失败:返回-1并设置errno值

三、struct  epoll_event结构体

struct epoll_event 
{
__uint32_t events; /* Epoll事件 */
epoll_data_t data; /* 用户数据 */
};
  •  功能:​epoll_create创建的事件表中的每一个事件就是用此结构体表示的


events成员:

  • 功能:​用来描述此个事件的类型、事件类型与poll函数的基本相同,不过是在poll的类型前面加上“E”,但epoll有两个额外的事件类型——​EPOLLET 和 EPOLLONESHOT​。它们对于epoll的高效运作非常关键。类型如下:
  • EPOLLIN:​表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:​表示对应的文件描述符可以写;
  • EPOLLPRI:​表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:​表示对应的文件描述符发生错误;
  • EPOLLHUP:​表示对应的文件描述符被挂断;
  • EPOLLET:​将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
  • EPOLLONESHOT:​只监听一次事件,当监听完这次事件之后就把该事件移出epoll事件池,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

data成员

typedef union epoll_data 
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

  • 功能:​data成员用来存储用户数据,data是一个联合体
  • fd:​它指定事件所从属的目标文件描述符(使用等最多)
  • ptr成员:​可用来指定与fd相关的用户数据
  • 但由于epoll_data_t 是一个联合体,我们​不能同时使用其 ptr 成员和 fd 成员​,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t 的fd成员,而在ptr指向的用户数据中包含fd


四、epoll_ctl函数

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能:​此函数可以用来向epoll_create创建的事件表中增加、删除、修改事件
  • 返回值:
  • 成功:时返回0
  • 失败:返回-1并设置errno
  • 参数:
  • epfd:​要操作的事件表句柄
  • op:​指定操作类型
  • EPOLL_CTL_ADD(​向事件表中注册fd上的事件)
  • EPOLL_CTL_MOD(​修改fd上的注册事件)
  • EPOLL_CTL_DEL(​删除fd上的注册事件)
  • fd:​要操作的文件描述符
  • event:​此参数指定事件,它是epoll_event结构指针类型

五、epoll_wait函数

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
  • 功能:​事件表创建成功并且加入事件之后,此函数用来在一段超时时间内等待一组文件描述符上的事件
  • 参数:
  • epfd:​等待处理的事件表
  • events:​需要自己申请struct  epoll_event数组,此数组用来存放事件表中就绪的事件
  • maxevents:​指定最多监听多少个事件,它必须大于0
  • timeout:​等待的时间(单位毫秒,与poll接口相同)
  • 返回值:
  • 0:​超过了timeout等待的时间,并且没有事件就绪
  • -1:​epoll_wait函数出错,同时设置errno值
  • 大于0:​事件表中已经准备就绪的事件个数


相对于poll改进的地方

  • epoll_wait函数如果检测到事件,就将​所有就绪的事件​从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中,这个数组只用于输出epoll_wait检测到的就绪事件。而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件,这就极大地提高了应用程序索引就绪文件描述符的效
  • 下面是epoll对于epoll改进的代码示意图

APUE编程:25---高级I/O之(IO多路复用:epoll()函数)_#include_02


六、epoll与poll、select的比较

  • 在另一篇文章中还介绍过三者的比较,可以参阅:​


select模型

  • 最大并发数限制:由于一个进程所打开的fd(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此,select模型的最大并发数就被限制了。
  • 效率问题:​每次进行select调用都会线性扫描全部的fd集合。这样,效率就会呈现线性下降。
  • 内核/用户空间内存拷贝问题:select在解决将fd消息传递给用户空间时采用了内存拷贝的方式。这样,其处理效率不高



poll模型

  • 对于poll模型,其虽然解决了select最大并发数的限制,但依然没有解决掉select的效率问题和内存拷贝问题



epoll做了以下的改进

支持一个进程打开较大数目的文件描述符(fd)


select模型对一个进程所打开的文件描述符是有一定限制的,其由FD_SETSIZE设置,默认为1024/2048。这对于那些需要支持上万连接数目的高并发服务器来说显然太少了,这个时候,可以选择两种方案:一是可以选择修改FD_SETSIZE宏然后重新编译内核,不过这样做也会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然Linux中创建线程的代价比较小,但仍然是不可忽视的,加上进程间数据同步远不及线程间同步的高效,所以也不是一种完美的方案。

但是,epoll则没有对描述符数目的限制,它所支持的文件描述符上限是整个系统最大可以打开的文件数目,例如,在1GB内存的机器上,这个限制大概为10万左右


IO效率不会随文件描述符(fd)的增加而线性下降


传统的select/poll的一个致命弱点就是当你拥有一个很大的socket集合时,不过任一时间只有部分socket是活跃的,select/poll每次调用都会线性扫描整个socket集合,这将导致IO处理效率呈现线性下降。

但是,epoll不存在这个问题,它只会对活跃的socket进行操作,这是因为在内核实现中,epoll是根据每个fd上面的callback函数实现的。因此,只有活跃的socket才会主动去调用callback函数,其他idle状态socket则不会。在这一点上,epoll实现了一个伪AIO,其内部推动力在内核。

在一些benchmark中,如果所有的socket基本上都是活跃的,如高速LAN环境,epoll并不比select/poll效率高,相反,过多使用epoll_ctl,其效率反而还有稍微下降。但是,一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了


使用mmap加速内核与用户空间的消息传递

无论是select,poll还是epoll,它们都需要内核把fd消息通知给用户空间。因此,如何避免不必要的内存拷贝就很重要了。对于该问题,epoll通过内核与用户空间mmap同一块内存来实现。


内核微调


这一点其实不算epoll的优点了,而是整个Linux平台的优点,Linux赋予开发者微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么,可以在运行期间动态调整这个内存池大小(skb_head_pool)来提高性能,该参数可以通过使用echo xxxx > /proc/sys/net/core/hot_list_length来完成。再如,可以尝试使用最新的NAPI网卡驱动架构来处理数据包数量巨大但数据包本身很小的特殊场景。


七、LT和ET模式


epoll对文件描述符的操作有​两种模式​:

  • ①LT (Level Trigger,水平触发)模式
  • LT模式是epoll的​默认的工作模式​,这种模式下epoll 相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符,ET模式是epoll的高效工作模式。对于采用LT工作模式的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait会再次向应用程序通告此事件,直到该事件被处理。
  • ②ET (Edge Trigger,边缘触发)模式
  • 对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait用将不再向应用程序通知这一事件,可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高
  • 可以用下面一张图理解:
  • 0代表无事件,1代表有事件。当epoll监听的套接字从无事件变为有事件就代表1次触发
  • 边缘触发:事件只触发一次,在后面的时间线就消失了
  • 水平触发:事件触发之后,如果不处理那么该事件依然存在,随着时间线往后延长,直至你处理完为止

APUE编程:25---高级I/O之(IO多路复用:epoll()函数)_IO复用_03

  • 一些相关使用场景:
  • 大数据处理:因为大数据的数据量比较多,因此一次可能处理不完,可以使用水平触发,来多次处理数据
  • 小数据处理:小数据调用边缘触发即可,一次处理完就行
  • 服务器的监听套接字:使用水平触发。当有客户端连接时如果这次不处理,可以放到下一次来处理。但是如果使用边缘触发,本次不处理,下次再处理就消失了,从而失去了这个客户端的连接


  • epoll处理套接字在LT、ET不同模式下的数据演示案例:

八、EPOLLONESHOT事件

  • 何时使用EPOLLONESHOT事件:​即使我们使用ET模式,一个socket上的某个事件​还是可能被触发多次​。这在并发程序中就会引起一个问题:比如一个线程(或进程)在读取完某个socket上​的数据后开始处理这些数据​,而在数据的处理过程中该socket上​又有新数据可读(EPOLLIN再次被触发)​,此时另外一个线程(或进程)被唤醒来读取这些新的数据。于是就出现了两个线程​同时操作一个​socket的局面。这当然不是我们期望的,我们期望的是一个socket连接在任一时刻都只被一个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现
  • EPOLLONESHOT事件的作用:​对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其注册的一个可读、可写或者异常事件,且只触发一次,在触发的时候,除非我们重新将这个文件描述符封装为一个事件再次放入epoll事件池。这样,当一个线程在处理某个socket的时候,其他线程是不可能有机会操作该socket的
  • 因此,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程​就应该立即重置(epoll_ctl(,EPOLL_CTL_MOD,,))​这个socket的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket
  • 使用EPOLLONESHOT事件的演示案例:

九、使用案例

#include<stdio.h>
#include<stdlib.h>
#include<strings.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>

int createSocket(char *ip,char *port);
int set_noblock(int fd);

int main(int argc,char *argv[])
{
if(argc!=3){
perror("please enter [IP] [PORT]");
exit(1);
}

int sockFd,epollFd;
sockFd=createSocket(argv[1],argv[2]);

//创建一个epoll事件表
epollFd=epoll_create(256);
if(epollFd<0){
perror("epoll_create");
exit(7);
}

//创建一个事件,服务端监听socket作为事件的fd
struct epoll_event ep_ev;
ep_ev.events=EPOLLIN;
ep_ev.data.fd=sockFd;


if(epoll_ctl(epollFd,EPOLL_CTL_ADD,sockFd,&ep_ev)<0){
perror("epoll_ctl");
exit(8);
}

struct epoll_event ready_ev[128];
int maxnum=128;
int timeout=1000;
int ret;

while(1)
{
switch(ret=epoll_wait(epollFd,ready_ev,maxnum,timeout))
{
case -1:
perror("epoll_wait");
break;
case 0:
printf("timeout\n");
break;
default:
{
int i;
for(i=0;i<ret;++i)
{
int fd=ready_ev[i].data.fd;
if((fd==sockFd) && (ready_ev[i].events&EPOLLIN)){
struct sockaddr_in acceptAddr;
socklen_t len=sizeof(acceptAddr);
int acceptSock=accept(fd,(struct sockaddr*)&acceptAddr,&len);
if(acceptSock<0){
perror("accept");
continue;
}
printf("get connect ip:%s,port:%d\n",inet_ntoa(acceptAddr.sin_addr),ntohs(acceptAddr.sin_port));

ep_ev.events=EPOLLIN | EPOLLET;
ep_ev.data.fd=acceptSock;

set_noblock(acceptSock);

if(epoll_ctl(epollFd,EPOLL_CTL_ADD,acceptSock,&ep_ev)<0){
perror("epoll_ctl");
close(acceptSock);
continue;
}
}
else{
if(ready_ev[i].events & EPOLLIN){
char buff[1024];
ssize_t _s=recv(fd,buff,sizeof(buff),0);
if(_s<0){
perror("recv");
continue;
}else if(_s==0){
printf(" client close...\n");

if(epoll_ctl(epollFd,EPOLL_CTL_DEL,fd,NULL)<0){
perror("epoll_ctl");
}
close(fd);
continue;
}else{
printf("client:%s",buff);
fflush(stdout);

ep_ev.events=EPOLLOUT|EPOLLET;
ep_ev.data.fd=fd;

if(epoll_ctl(epollFd,EPOLL_CTL_MOD,fd,&ep_ev)<0){
perror("epoll_ctl");
continue;
}
}

}else if(ready_ev[i].events & EPOLLOUT){
char *msg="I am server";
send(fd,msg,strlen(msg),0);
if(epoll_ctl(epollFd,EPOLL_CTL_DEL,fd,NULL)<0){
perror("epoll_ctl");
continue;
}
close(fd);
}
}
}
}
break;
}
}
close(sockFd);
return 0;
}

int createSocket(char *ip,char *port)
{
int socketFd=socket(AF_INET,SOCK_STREAM,0);
if(socketFd<0){
exit(2);
}

printf("create socket success\n");
int opt=1;
if(setsockopt(socketFd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))<0){
perror("setsockopt");
exit(3);
}

struct sockaddr_in localAddr;
bzero(&localAddr,sizeof(localAddr));
localAddr.sin_family=AF_INET;
localAddr.sin_port=htons(atoi(port));
if(inet_aton(ip,(struct in_addr*)&localAddr.sin_addr)<=0){
perror("inet_aton");
exit(4);
}

if(bind(socketFd,(struct sockaddr*)&localAddr,sizeof(localAddr))<0){
perror("bind");
exit(5);
}
printf("bind success\n");

if(listen(socketFd,10)<0){
perror("listen");
exit(6);
}
printf("liste success\n");

return socketFd;
}

int set_noblock(int fd)
{
int fl=fcntl(fd,F_GETFL);
return fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}