Epoll模型
c++网络编程下Linux的epoll技术和Windows下的IOCP模型

c++⽹络编程下Linux的epoll技术和Windows下的IOCP模型⽬录⼀、IOCP和Epoll之间的异同1、异2、同⼆:Epoll理解与应⽤。
1、epoll是什么?2、epoll与select对⽐优化3、epoll是怎么优化select问题的三、epoll的⼏个函数的介绍:1、epoll_create函数2、epoll_ctl函数3、epoll_wait函数4、条件触发和边缘触发四、IOCP理解与应⽤1、传统服务器的⽹络IO流程2、使⽤IOCP的基本步骤⼀、IOCP和Epoll之间的异同1、异1).IOCP是WINDOWS系统下使⽤。
Epoll是Linux系统下使⽤。
2).IOCP是IO操作完毕之后,通过Get函数获得⼀个完成的事件通知。
Epoll是当你希望进⾏⼀个IO操作时,向Epoll查询是否可读或可写,若处于可读或可写状态,Epoll会通过epoll_wait进⾏通知。
3).IOCP封装了异步的消息事件的通知机制,同时封装了部分IO操作。
但Epoll仅仅封装了⼀个异步事件的通知机制,并不负责IO读写操作。
Epoll保持了事件通知和IO操作间的独⽴性,更加简单灵活。
4).基于上⾯的描述,我们可以知道Epoll不负责IO操作,所以它只告诉你当前可读可写了,并且将协议读写缓冲填充,由⽤户去读写控制,此时我们可以做出额外的许多操作。
IOCP则直接将IO通道⾥的读写操作都做完了才通知⽤户,当IO通道⾥发⽣了堵塞等状况我们是⽆法控制的。
2、同1).它们都是异步的事件驱动的⽹络模型。
2).它们都可以向底层进⾏指针数据传递,当返回事件时,除可通知事件类型外,还可以通知事件相关数据。
⼆:Epoll理解与应⽤。
1、epoll是什么?epoll是当前在Linux下开发⼤规模并发⽹络程序的热门⼈选,epoll 在Linux2.6内核中正式引⼊,和select相似,都是I/O多路复⽤(IO multiplexing)技术。
Get清风epoll学习笔记

epoll学习笔记epoll学习笔记epoll有两种模式,Edge Triggered(简称ET) 和 Level Triggered(简称LT).在采用这两种模式时要注意的是,如果采用ET模式,那么仅当状态发生变化时才会通知,而采用LT模式类似于原来的select/poll操作,只要还有没有处理的事件就会一直通知.以代码来说明问题:首先给出server的代码,需要说明的是每次accept的连接,参加可读集的时候采用的都是ET模式,而且接收缓冲区是5字节的,也就是每次只接收5字节的数据:#include <iostream>#include <sys/socket.h>#include <sys/epoll.h>#include <netinet/in.h>#include <arpa/inet.h>#include <fcntl.h>#include <unistd.h>#include <stdio.h>#include <errno.h>using namespace std;#define MAXLINE 5#define OPEN_MAX 100#define LISTENQ 20#define SERV_PORT 5000#define INFTIM 1000void 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 i, maxi, listenfd, connfd, sockfd,epfd,nfds;ssize_t n;char line[MAXLINE];socklen_t clilen;//声明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);bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;char *local_addr="127.0.0.1";inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);serveraddr.sin_port=htons(SERV_PORT);bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));listen(listenfd, LISTENQ);maxi = 0;for( ; ; ) {//等待epoll事件的发生nfds=epoll_wait(epfd,events,20,500);//处理所发生的所有事件for(i=0;i<nfds;++i){if(events[i].data.fd==listenfd){connfd = accept(listenfd,(sockaddr *)&clientaddr, &clil en);if(connfd<0){perror("connfd<0");exit(1);}//setnonblocking(connfd);char *str = inet_ntoa(clientaddr.sin_addr);cout << "accapt a connection from " << str << end l;//设置用于读操作的文件描述符ev.data.fd=connfd;//设置用于注测的读操作事件ev.events=EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注册evepoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);}else if(events[i].events&EPOLLIN){cout << "EPOLLIN" << endl;if( (sockfd = events[i].data.fd) < 0)continue;if( (n = read(sockfd, line, MAXLINE)) < 0) {if(errno == ECONNRESET) {close(sockfd);events[i].data.fd = -1;} elsestd::cout<<"readline error"<<std::endl;} else if(n == 0) {close(sockfd);events[i].data.fd = -1;}line[n] = '\0';cout << "read " << line << endl;//设置用于写操作的文件描述符ev.data.fd=sockfd;//设置用于注测的写操作事件ev.events=EPOLLOUT|EPOLLET;//修改sockfd上要处理的事件为EPOLLOUT//epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);}else if(events[i].events&EPOLLOUT){sockfd = events[i].data.fd;write(sockfd, line, n);//设置用于读操作的文件描述符ev.data.fd=sockfd;//设置用于注测的读操作事件ev.events=EPOLLIN|EPOLLET;//修改sockfd上要处理的事件为EPOLINepoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);}}}return 0;}下面给出测试所用的Perl写的client端,在client中发送10字节的数据,同时让client在发送完数据之后进入死循环, 也就是在发送完之后连接的状态不发生改变--既不再发送数据, 也不关闭连接,这样才能观察出server的状态: #!/usr/bin/perluse IO::Socket;my $host = "127.0.0.1";my $port = 5000;my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@"; my $msg_out = "1234567890";print $socket $msg_out;print "now send over, go to sleep\n";while(1){sleep(1);}送了10字节的数据,也就是说,server仅当第一次监听到了EPOLLIN事件,由于没有读取完数据,而且采用的是ET模式,状态在此之后不发生变化,因此server 再也接收不到EPOLLIN事件了.(友情提示:上面的这个测试客户端,当你关闭它的时候会再次出发IO可读事件给server,此时server就会去读取剩下的5字节数据了,但是这一事件与前面描述的ET性质并不矛盾.)如果我们把client改为这样:#!/usr/bin/perluse IO::Socket;my $host = "127.0.0.1";my $port = 5000;my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@"; my $msg_out = "1234567890";print $socket $msg_out;print "now send over, go to sleep\n";sleep(5);print "5 second gone send another line\n";print $socket $msg_out;while(1){sleep(1);}可以发现,在server接收完5字节的数据之后一直监听不到client的事件,而当client休眠5秒之后重新发送数据,server再次监听到了变化,只不过因为只是读取了5个字节,仍然有10个字节的数据(client第二次发送的数据)没有接收完.如果上面的实验中,对accept的socket都采用的是LT模式,那么只要还有数据留在buffer中,server就会继续得到通知,读者可以自行改动代码进行实验.基于这两个实验,可以得出这样的结论:ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一局部数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.补充说明一下这里一直强调的"状态变化"是什么:1)对于监听可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;对一般的socket而言,协议栈中相应的缓冲区有新的数据为状态发生变化.但是,如果在一个时间同时接收了N个连接(N>1),但是监听socket只accept了一个连接,那么其它未 accept的连接将不会在ET模式下给监听socket发出通知,此时状态不发生变化;对于一般的socket,就如例子中而言,如果对应的缓冲区本身已经有了N字节的数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化.2)对于监听可写事件时,同理可推,不再详述.而不管是监听可读还是可写,对方关闭socket连接都将造成状态发生变化,比方在例子中,如果强行中断client脚本,也就是主动中断了socket连接,那么都将造成server端发生状态的变化,从而server得到通知,将已经在本方缓冲区中的数据读出.把前面的描述可以总结如下:仅当对方的动作(发出数据,关闭连接等)造成的事件才能导致状态发生变化,而本方协议栈中已经处理的事件(包括接收了对方的数据,接收了对方的主动连接请求)并不是造成状态发生变化的必要条件,状态变化一定是对方造成的.所以在ET模式下的,必须一直处理到出错或者完全处理完毕,才能进行下一个动作,否那么可能会发生错误.另外,从这个例子中,也可以阐述一些根本的网络编程概念.首先,连接的两端中,一端发送成功并不代表着对方上层应用程序接收成功, 就拿上面的client测试程序来说,10字节的数据已经发送成功,但是上层的server并没有调用read读取数据,因此发送成功仅仅说明了数据被对方的协议栈接收存放在了相应的buffer中,而上层的应用程序是否接收了这局部数据不得而知;同样的,读取数据时也只代表着本方协议栈的对应buffer中有数据可读,而此时时候在对端是否在发送数据也不得而知.epoll精髓在linux的网络编程中,很长的时间都在使用select来做事件触发。
linux epoll 实例

linux epoll 实例epoll是Linux内核为处理大批量文件描述符而作了改进的poll(2),是LINUX下多路复用IO支持的一种机制,它基于事件驱动模型,用户把要发生的事件(就是文件描述符)放入内核的一个poll结构,文件描述符就放在一个数组中,每当一个有效I/O发生,加入到内核队列中,放到用户定义的epoll结构中,这个新发生的事件将立即传送给用户进程,在该进程的某一线程中,使用epoll_Wait()函数等待事件的发生,等待到发生的事件时,epoll会将发生的事件告诉用户程序,比如连接,数据可读等。
epoll的优势:1、内核利用epoll的重新把文件描述符放入有序的数据结构,比poll管理更为高效,更少的遍历处理全部的文件描述符;2、epoll模型支持IO多路复用,同时可以监听到数以千计的fd;3、epoll提供水平触发和边沿触发两种模式;4、epoll支持ET和LT两种类型;5、epoll实现轮询式监视,提升系统性能,使用更轻松和实时;在Linux中使用epoll时,首先need to initilize一个epoll实例,而epoll_create函数来完成这一初始化(epoll_create()的参数是数据要放入epoll的最大句柄数),epoll_create()返回的文件描述符(int类型),被用于后续的epoll操作;然后用epoll_ctl()来添加文件描述符至内核的epoll表头,最后,进程通过调用epoll_wait()来等待某个文件描述符因某种事件发生而Entry Ready状态,epoll_wait()阻塞等待,epoll函数会返回可以操作的文件描述符列表。
epoll能处理上千个fd事件不错过,但是也有其缺点,它不能实现即时通讯,新连接的处理是有延迟的,而且,如果epoll_wait()中断,它会只返回一个已就绪的fd,而不检查其它的fd.综上所述,epoll是LINUX下高效的多路复用IO编程的机制,它能处理上千个fd事件,用在Server开发中是一种正确选择,相对于select/poll很大提高了处理效率,且支持水平触发和边沿触发两种模式,也支持ET和LT两种类型。
epoll反应堆模型

if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
//关闭链接
eventdel(g_efd, ev);
//从红黑树 g_efd 中移除
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
}
/*创建 socket, 初始化 lfd */
void initlistensocket(int efd, short port)
struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); //bzero(&sin, sizeof(sin)) sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(port);
}
return;
}
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0);
//直接将数据 回写给客户端。未作处理
网络IO模型分析及epoll介绍

epoll_event是?
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 */ };
poll模型
• int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
epoll的特点
• 它保留了poll的两个相对与select的优点。 • epoll_wait的参数events作为出参,直接返回了有 事件发生的fd,epoll_wait的返回值既是发生事件 的个数,省略了poll中返回之后的循环操作。 • 不再象select、poll一样将标识符局限于fd,epoll 中可以将标识符扩大为指针,大大增加了epoll模 型下的灵活性。
• 服务端
– listen(int sockfd, int backlog); – accept(int sockfd, void *addr, int *addrlen);
• 客户端
– connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
p o l l 方 法 的 基 本 概 念

详细说说select poll epoll(以下内容来自网络和自己的总结,再次感谢网络中的大神们提供的见解)在探索select poll? epoll之前我们首先要知道什么叫多路复用:下来探索一下为什么会用到多路复用:首先我们看看一个客户端请求服务器的完整过程。
首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。
具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操作,如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。
阻塞调用会进入内核等待,cpu就会让出去给其他进程使用了,你可能会说那么加进程数呀,当读写事件十分多的时候会创建很多的进程,此时进程的上下文切换会占用过多的cpu资-源。
有人会说那么用线程,其实线程的上下文切换也会占用过多资-源,而且还会引入线程之间同步和互斥的问题,因为线程之间看到的是同一块内存资-源。
所以我么就会思考能不能用一个进程来查看很多的IO事件,比如每一个人都在钓鱼每一个鱼上钩都比做是一个事件发生的话,那么一百个事件发生你可以让一百个人在那里一人拿一个鱼竿进行钓鱼,你自己负责进行鱼的收集。
此时如果没有鱼上钩,那一百个人就在那阻塞等待,你自己为了收鱼也在空闲着。
这里的你自己可以比作CPU,一般个人可以比作多个进程,此时如果不是所有鱼都上钩,你就十分空闲其他人也在那拿着鱼竿空闲等待着,如果同时有多个鱼上钩了,多个人会像你汇报,此时汇报的顺序问题就是形成混乱。
此时我们可以进行一下改进,比如专门找一个人拿着许多鱼竿,当一个鱼竿上的鱼上钩以后再拉起鱼竿,这样节约了人力,还解决了问题。
下来讲一个真实的故事吧:假设你是一个机场的空管,你需要管理到你机场的所有的航线,包括进港,出港,有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。
你会怎么做?最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机,从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
p o l l 方 法 的 基 本 概 念
IO多路复用select,poll epoll以及区别要弄清问题先要知道问题的出现原因由于进程的执行过程是线性的(也就是顺序执行),当我们调用低速系统I-O(read,write,accept等等),进程可能阻塞,此时进程就阻塞在这个调用上,不能执行其他操作.阻塞很正常. 接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服务器端read(sockfd1,bud,bufsize),此时客户端进程没有发送数据,那么read(阻塞调用)将阻塞直到客户端调用write(sockfd,but,size) 发来数据. 在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服务器阻塞于其中一个客户sockfd1,当另一个客户的数据到达套接字sockfd2时,服务器不能处理,仍然阻塞在read(sockfd1.)上;此时问题就出现了,不能及时处理另一个客户的服务,咋么办?I-O多路复用来解决!I-O多路复用:继续上面的问题,有多个客户连接,sockfd1,sockfd2,sockfd3.sockfdn同时监听这n个客户,当其中有一个发来消息时就从select的阻塞中返回,然后就调用read读取收到消息的sockfd,然后又循环回select这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息那这样子,在读取socket1的数据时,如果其它socket有数据来,那么也要等到socket1读取完了才能继续读取其它socket的数据吧。
那不是也阻塞住了吗?而且读取到的数据也要开启线程处理吧,那这和多线程IO有什么区别呢?3.跟多线程相比较,线程切换需要切换到内核进行线程切换,需要消耗时间和资-源. 而I-O多路复用不需要切换线-进程,效率相对较高,特别是对高并发的应用nginx就是用I-O多路复用,故而性能极佳.但多线程编程逻辑和处理上比I-O多路复用简单.而I-O多路复用处理起来较为复杂.这些名词比较绕口,理解涵义就好。
linux epoll原理
linux epoll原理EPoll是Linux内核的一种I/O多路复用的API (ApplicationProgrammingInterface),它可以按需控制大量并发连接。
它的优势在于其运行效率高,性能高,特别是在处理大量并发连接时,更加高效。
EPoll也被成为I/O复用技术,该技术可以帮助程序处理大量的I/O任务(例如文件I/O,网络I/O等),而不用担心将系统的计算资源消耗殆尽。
EPoll的优势在于提供了一种高效的机制来处理大量的I/O,它能够帮助程序大幅提高处理并发连接的效率,进而提高系统的吞吐量。
EPoll原理EPoll原理非常简单,它是当用户进程将文件描述符添加到EPoll实例中,内核会在控制结构中为该文件描述符分配一个新的节点,该节点中会存储一些有关该文件描述符的相关信息,这些信息可以用于检测文件描述符是否有可读、可写等状态,而且可以立即被用户进程使用。
当内核发现可读/可写事件发生时,它会把该事件保存在一个队列中,以便于用户进程可以立即把内核通知的事件转换为应用层的可读/可写操作。
为了有效地提高I/O处理效率,EPoll将事件保存在内存中,然后马上返回给用户,从而减少了系统调用过程中可能导致的严重延迟。
EPoll相对于select模型,主要有以下优势:1、EPoll不需要复制监控事件列表,所以可以更有效地利用内存;2、比select模型更不容易出现描述符数量超过最大限制的问题;3、提供了水平触发与边缘触发的模型以及异步I/O的支持;4、EPoll模型可以支持无限的文件描述符数量,而select模型则受限于最大描述符数量。
EPoll在Linux内核中的应用由于EPoll比select模型有更好的性能和更高的利用效率,因此,它正在越来越多的被用于Linux内核中,例如它被用于Linux服务器的网络I/O中,以及内核中的系统调用调度、文件I/O等系统服务中。
EPoll的缺点由于EPoll的优势,它的应用越来越多,在一些系统服务中已经取代了传统的select模型,但是该模型也有一些缺点,例如它的处理效率会受到文件描述符的数量的影响,而且由于它需要进行大量的系统调用,所以在一些低端硬件上,性能会受到影响。
epoll
epoll_create
process
epoll_create函数
函数声明:int epoll_create(int size) 该函数生成一个epoll专用的文件描述符, 其中的参数是指定生成描述符的最大范围
2、epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件, 删除事件。 参数:epfd:由 epoll_create 生成的epoll专用的文件描述符; op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注 册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除 fd:关联的文件描述符; event:指向epoll_event的指针; 例如: ev.data.fd = newSocket; ev.events = EPOLLIN | EPOLLERR | EPOLLOUT; 如果调用成功返回0,不成功返回-1
数据发送
问题:为什么不处理 EPOLLOUT 事件?
int CTCPCtrl::CheckWaitSendData() { for(i = 0; i < MAXSENDPKGS; i++)/*一次最多发送MAXSENDPKGS个数据包*/ { nCodeLength = MAX_BUF_LEN; iTempRet = m_pS2CPipe->GetHeadCode((BYTE * )m_szMsgBuf, &nCodeLength); if(0 > iTempRet) { if (ERRPACKETLENGTH == iTempRet) { ErrorLog("Error: In S2C pipe a packet len is %d so long.", nCodeLength); break; } else { Log("Get head code failed, error code = %d.", iTempRet); break; } } if( SendClientData((BYTE *)m_szMsgBuf, nCodeLength) )continue; } return 0; }
Spice工作原理及代码剖析:02Spice网络事件处理模型
Spice⼯作原理及代码剖析:02Spice⽹络事件处理模型〇、概述⽹络事件处理是libspice设计中最关键的部分,可以说是整个Spice的⾻架,⽤以⽀撑Spice的运⾏,是理解Spice运作⽅式的切⼊⼝之⼀(VDI是另⼀个阅读代码的切⼊⼝)。
Spice的server和client通信⽅式采⽤了三种框架:1、 Qemu的main函数中采⽤⾮阻塞select⽅式轮训⽹络事件2、 Libspice中有⼀个专门的线程,采⽤⾮阻塞epoll模型监听⽹络事件3、 Qemu中采⽤定时器⽅式进⾏⽹络数据发送⼀、select模型处理Spice中最基本的⽹络事件处理均采⽤select模型,即⼤部分的⽹络事件是在Qemu的主函数中进⾏捕获的。
直接看代码:void main_loop_wait(int nonblocking){IOHandlerRecord *ioh;fd_set rfds, wfds, xfds;int ret, nfds;nfds = -1;FD_ZERO(&rfds);FD_ZERO(&wfds);FD_ZERO(&xfds);// FD_SET 对队列中的所有节点进⾏处理QLIST_FOREACH(ioh, &io_handlers, next) {if (ioh->deleted)continue;FD_SET(ioh->fd, &rfds);FD_SET(ioh->fd, &wfds);}// selectret = select(nfds + 1, &rfds, &wfds, &xfds, &tv);// 调⽤节点对应的回调函数进⾏⽹络事件处理if (ret > 0) {IOHandlerRecord *pioh;QLIST_FOREACH_SAFE(ioh, &io_handlers, next, pioh) {if (ioh->fd_read && FD_ISSET(ioh->fd, &rfds)) {ioh->fd_read(ioh->opaque);}if (ioh->fd_write && FD_ISSET(ioh->fd, &wfds)) {ioh->fd_write(ioh->opaque);}}}qemu_run_all_timers();}以上代码遵循了select模型的基本处理步骤:FD_SET、select、process,所以⾮常容易理解。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
1 / 10 在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。 相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明: #define __FD_SETSIZE 1024 表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。
epoll的接口非常简单,一共就三个函数: 1. int epoll_create(int size); 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。 第一个参数是epoll_create()的返回值, 第二个参数表示动作,用三个宏来表示: EPOLL_CTL_ADD:注册新的fd到epfd中; EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd; 第三个参数是需要监听的fd, 第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下: struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
events可以是以下几个宏的集合: EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT: 表示对应的文件描述符可以写; EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); 2 / 10 EPOLLERR: 表示对应的文件描述符发生错误; EPOLLHUP: 表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
从man手册中,得到ET和LT的具体描述如下 EPOLL事件有两种模型: Edge Triggered (ET) 边缘触发 只有数据到来,才触发,不管缓存区中是否还有数据。 Level Triggered (LT) 水平触发 只要有数据都会触发。
假如有这样一个例子: 1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符 2. 这个时候从管道的另一端被写入了2KB的数据 3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作 4. 然后我们读取了1KB的数据 5. 调用epoll_wait(2)......
Edge Triggered 工作模式: 如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回 3 / 10 的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
Level Triggered 工作模式 相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。 然后详细解释ET, LT: LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表. ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。
在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)
另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后, 读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: while(rs) { buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0); if(buflen < 0) { // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读 // 在这里就当作是该次事件已处理处. if(errno == EAGAIN) break; else return; } else if(buflen == 0) { 4 / 10 // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.
ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; }