epoll精髓
c++epoll底层原理
c++epoll底层原理
C++中的epoll是一个高效的I/O事件通知机制。
它是Linux系统的一部分,可以用于处理大量的并发连接。
epoll底层原理如下:
1. epoll使用了红黑树来管理所有的I/O事件,并将其存储在内核空间中。
2. 当一个文件描述符(socket)准备好进行读或写操作时,内核会把这个事件放入到一个内核事件表中。
3. 用户程序可以通过epoll_ctl()函数向内核事件表中注册感兴趣的事件和相关文件描述符。
4. 内核在每次有事件到来时会通知用户程序,用户程序则可以通过epoll_wait()函数从内核事件表中读取事件。
5. 用户程序可以根据事件的类型进行相关的操作,例如读取数据、写入数据等。
epoll的底层原理可以总结为以下几点:
- epoll利用了操作系统的异步通知机制,如epoll_wait()函数,减少了用户程序的轮询操作,提高了效率。
- epoll使用红黑树来管理所有的I/O事件,通过红黑树的平衡性能,可以快速地进行事件查询、插入和删除。
- epoll通过将文件描述符和事件注册到内核事件表中,实现了高效的I/O事件监听和处理。
- epoll可以同时处理大量的并发连接,可以支持上万个连接的并发处理。
- epoll使用了边缘触发(EPOLLET)模式,只有当文件描述符的状态发生变化时才会通知用户程序,避免了不必要的事件
通知。
总的来说,epoll底层原理的关键在于通过内核事件表的管理和使用异步通知机制来实现高效的I/O事件处理。
javaepoll模型_IO多路复用技术详解之epoll模型
javaepoll模型_IO多路复⽤技术详解之epoll模型epoll的优点:1.⽀持⼀个进程打开⼤数⽬的socket描述符(FD)select 最不能忍受的是⼀个进程所打开的FD是有⼀定限制的,由FD_SETSIZE设置,默认值是2048。
对于那些需要⽀持的上万连接数⽬的IM服务器来说显然太少了。
这时候你⼀是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来⽹络效率的下降,⼆是可以选择多进程的解决⽅案(传统的 Apache⽅案),不过虽然linux上⾯创建进程的代价⽐较⼩,但仍旧是不可忽视的,加上进程间数据同步远⽐不上线程间同步的⾼效,所以也不是⼀种完美的⽅案。
不过 epoll则没有这个限制,它所⽀持的FD上限是最⼤可以打开⽂件的数⽬,这个数字⼀般远⼤于2048,举个例⼦,在1GB内存的机器上⼤约是10万左右,具体数⽬可以cat/proc/sys/fs/file-max察看,⼀般来说这个数⽬和系统内存关系很⼤。
2.IO效率不随FD数⽬增加⽽线性下降传统的select/poll另⼀个致命弱点就是当你拥有⼀个很⼤的socket集合,不过由于⽹络延时,任⼀时间只有部分的socket是"活跃"的,但是select/poll每次调⽤都会线性扫描全部的集合,导致效率呈现线性下降。
但是epoll不存在这个问题,它只会对"活跃"的socket进⾏操作---这是因为在内核实现中epoll是根据每个fd上⾯的callback函数实现的。
那么,只有"活跃"的socket才会主动的去调⽤ callback函数,其他idle状态socket则不会,在这点上,epoll实现了⼀个"伪"AIO,因为这时候推动⼒在os内核。
在⼀些benchmark中,如果所有的socket基本上都是活跃的---⽐如⼀个⾼速LAN环境,epoll并不⽐select/poll有什么效率,相反,如果过多使⽤epoll_ctl,效率相⽐还有稍微的下降。
epoll 原理
epoll 原理
epoll是Linux系统提供的一种高效I/O多路复用方式。
它采用事件驱动的方式实现I/O多路复用,可以同时监控多个文件描述符的状态,并在文件描述符就绪时通知应用程序进行读写操作。
与传统的select 和 poll 系统调用相比,epoll 具有更高的性能和更好的可扩展性。
epoll 基于内核中的事件驱动机制,通过注册回调函数实现对事件的监听和处理。
应用程序可以将一个或多个文件描述符注册到epoll 对象中,当所关注的文件描述符就绪时,内核会通知 epoll 对象,epoll 对象再调用应用程序注册的回调函数进行处理。
epoll 主要包含三个系统调用:epoll_create、epoll_ctl 和epoll_wait。
其中,epoll_create 用于创建 epoll 对象,epoll_ctl 用于向 epoll 对象中添加、修改或删除文件描述符,epoll_wait 则是阻塞等待 epoll 对象中的文件描述符就绪。
与 select 和 poll 不同的是,epoll 不需要在每次调用
epoll_wait 时重新向内核传递文件描述符集合,而是在注册文件描述符时将其添加到内核中的事件表中,这样每次调用 epoll_wait 时只需要从事件表中取出就绪的文件描述符即可,大大减少了内核与用户空间的数据交换次数,提高了系统的效率。
总之,epoll 是 Linux 平台上一种高效的 I/O 多路复用机制,采用事件驱动的方式实现对文件描述符的监控和处理。
它相对于传统的 select 和 poll 有更好的性能和可扩展性,是实现高并发网络编
程的重要工具之一。
c语言epoll详解
c语言epoll详解摘要:1.简介- 什么是C 语言epoll- epoll 的优势2.epoll 原理- epoll 的工作机制- epoll 的事件处理3.epoll 的使用- 安装epoll 模块- 创建epoll 实例- 添加、修改、删除事件- 查询事件- 处理事件4.epoll 的例子- 简单的epoll 例子- 更复杂的epoll 例子5.epoll 的应用场景- 网络编程- 服务器开发正文:C 语言epoll 详解C语言epoll是一种高效的I/O事件处理机制,相较于传统的select和poll,epoll在性能上有很大的优势,因此被广泛应用于网络编程和服务器开发等领域。
1.简介epoll是Linux下的一种I/O事件处理机制,它能够实现对大量I/O进行监控,只有发生变化的I/O才会通知用户进程。
这使得用户进程可以更加高效地处理I/O事件,避免了不必要的上下文切换和资源浪费。
2.epoll 原理epoll 的工作机制类似于一个事件驱动的系统。
它包含一个内核模块和一个用户进程。
内核模块负责管理I/O 资源,用户进程通过epoll_create、epoll_ctl 等系统调用与内核模块进行交互,实现对I/O 资源的监控和事件处理。
当用户进程调用epoll_wait 时,内核模块会遍历所有注册的I/O 资源,检查它们的状态是否发生变化。
如果某个I/O 资源的状态发生了变化,内核模块就会将这个变化通知给用户进程。
用户进程可以根据收到的通知来执行相应的操作。
3.epoll 的使用要在C 语言中使用epoll,首先需要安装epoll 模块。
安装方法如下:```#include <linux/module.h>#include <linux/kernel.h>#include <linux/fs.h>MODULE_LICENSE("GPL");MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("An epoll module");module_init(epoll_init);module_exit(epoll_exit);```接下来,可以创建epoll 实例、添加、修改、删除事件以及查询事件。
关于Epoll,你应该知道的那些细节
关于Epoll,你应该知道的那些细节Epoll,位于头文件sys/epoll.h,是Linux系统上的I/O事件通知基础设施。
epoll API为Linux系统专有,于内核2.5.44中首次引入,glibc于2.3.2版本加入支持。
其它提供类似的功能的系统,包括FreeBSD kqueue,Solaris /dev/poll等。
Epoll APIEpoll API实现了与poll类似的功能:监测多个文件描述符上是否可以执行I/O操作。
支持边缘触发ET和水平触发LT,相比poll支持监测数量更多的文件描述符。
以下API用于创建和管理epoll实例:epoll_create:创建Epoll实例,并返回Epoll实例关联的文件描述符。
(最新的epoll_create1扩展了epoll_create的功能)create_ctl:注册关注的文件描述符。
注册于同一epoll实例的一组文件描述符被称为epoll set,可以通过进程对应的/proc/[pid]/fdinfo目录查看。
epoll_wait:等待I/O事件,如果当前没有任何注册事件处于可用状态,调用线程会被阻塞。
水平触发LT与边缘触发ETEpoll事件分发接口可以使用ET和LT两种模式。
两种模式的差别描述如下。
典型场景:1 管道(pipe)读端的文件描述符(rfd)注册于Epoll实例。
2 写者(Writer)向管道(pipe)写端写2KB的数据。
3 epoll_wait调用结束,返回rfd作为就绪的文件描述符。
4 管道读者(pipe reader) 从rfd读1KB的数据。
5 下一次epoll_wait调用。
如果rfd文件描述符使用EPOLLET(边缘触发)标记加入Epoll接口,第5步对epoll_wait 的调用可能会挂住,尽管文件输入缓冲区中仍然有可用数据;与此同时,远端实体由于已经发送数据,可能正在等待回应。
其原因是边缘触发模式仅在所监控的文件描述符状态发。
epoll实验报告心得
epoll实验报告心得《epoll实验报告心得》在操作系统课程的学习中,我们进行了一次关于epoll的实验。
epoll是Linux 中的一种I/O多路复用机制,它可以有效地管理大量的文件描述符,提高系统的性能和效率。
在这次实验中,我们深入了解了epoll的原理和使用方法,同时也进行了相应的实验操作。
首先,我们对epoll的原理进行了深入的学习。
epoll采用了事件驱动的机制,通过监听文件描述符的事件状态来进行I/O操作,避免了传统的轮询方式带来的性能损耗。
我们了解到epoll主要包括三个系统调用:epoll_create、epoll_ctl 和epoll_wait,通过这些系统调用可以实现对文件描述符的监听和处理。
在实验中,我们通过编写简单的C语言程序来实现对epoll的使用。
我们首先创建了一个epoll实例,然后通过epoll_ctl将需要监听的文件描述符添加到epoll实例中。
接着,我们使用epoll_wait来等待事件的发生,并对事件进行处理。
通过这些实验操作,我们更加深入地理解了epoll的使用方法和原理。
在实验过程中,我们也遇到了一些问题和挑战。
例如,对于epoll的使用方法和参数设置需要仔细研究和理解,否则容易出现错误。
同时,对于大规模的并发连接的处理,epoll可以提供更好的性能和效率,但也需要合理的设计和调优。
总的来说,通过这次epoll实验,我们对于Linux系统中I/O多路复用机制有了更深入的了解,同时也学会了如何使用epoll来提高系统的性能和效率。
这次实验不仅增强了我们对操作系统的理解,也为我们今后的工作和学习提供了更多的知识和经验。
希望通过不断的学习和实践,我们可以更好地掌握epoll的使用方法,为我们的工作和项目提供更好的支持和帮助。
epoll使用细解
epoll相关的系统调用有:epoll_create, epoll_ctl和epoll_wait。
Linux-2.6.19又引入了可以屏蔽指定信号的epoll_wait: epoll_pwait。
至此epoll家族已全。
其中epoll_create用来创建一个epoll文件描述符,epoll_ctl用来添加/修改/删除需要侦听的文件描述符及其事件,epoll_wait/epoll_pwait接收发生在被侦听的描述符上的,用户感兴趣的IO事件。
epoll文件描述符用完后,直接用close关闭即可,非常方便。
事实上,任何被侦听的文件符只要其被关闭,那么它也会自动从被侦听的文件描述符集合中删除,很是智能。
每次添加/修改/删除被侦听文件描述符都需要调用epoll_ctl,所以要尽量少地调用epoll_ctl,防止其所引来的开销抵消其带来的好处。
有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈。
A:IO效率。
在大家苦苦的为在线人数的增长而导致的系统资源吃紧上的问题正在发愁的时候,Linux 2.6内核中提供的System Epoll为我们提供了一套完美的解决方案。
传统的select以及poll的效率会因为在线人数的线形递增而导致呈二次乃至三次方的下降,这些直接导致了网络服务器可以支持的人数有了个比较明显的限制。
自从Linux提供了/dev/epoll的设备以及后来2.6内核中对/dev/epoll设备的访问的封装(System Epoll)之后,这种现象得到了大大的缓解,如果说几个月前,大家还对epoll不熟悉,那么现在来说的话,epoll的应用已经得到了大范围的普及。
那么究竟如何来使用epoll呢?其实非常简单。
通过在包含一个头文件#include <sys/epoll.h>以及几个简单的API 将可以大大的提高你的网络服务器的支持人数。
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来做事件触发。
EPOLL原理详解(图文并茂)
EPOLL原理详解(图⽂并茂)⽂章核⼼思想是:要清晰明⽩EPOLL为什么性能好。
本⽂会从⽹卡接收数据的流程讲起,串联起CPU中断、操作系统进程调度等知识;再⼀步步分析阻塞接收数据、select到epoll的进化过程;最后探究epoll的实现细节。
⼀、从⽹卡接收数据说起下图是⼀个典型的计算机结构图,计算机由CPU、存储器(内存)、⽹络接⼝等部件组成。
了解epoll本质的第⼀步,要从硬件的⾓度看计算机怎样接收⽹络数据。
下图展⽰了⽹卡接收数据的过程。
在①阶段,⽹卡收到⽹线传来的数据;经过②阶段的硬件电路的传输;最终将数据写⼊到内存中的某个地址上(③阶段)。
这个过程涉及到DMA传输、IO通路选择等硬件有关的知识,但我们只需知道:⽹卡会把接收到的数据写⼊内存。
通过硬件传输,⽹卡接收的数据存放到内存中。
操作系统就可以去读取它们。
⼆、如何知道接收了数据?了解epoll本质的第⼆步,要从CPU的⾓度来看数据接收。
要理解这个问题,要先了解⼀个概念——中断。
计算机执⾏程序时,会有优先级的需求。
⽐如,当计算机收到断电信号时(电容可以保存少许电量,供CPU运⾏很短的⼀⼩段时间),它应⽴即去保存数据,保存数据的程序具有较⾼的优先级。
⼀般⽽⾔,由硬件产⽣的信号需要cpu⽴马做出回应(不然数据可能就丢失),所以它的优先级很⾼。
cpu理应中断掉正在执⾏的程序,去做出响应;当cpu完成对硬件的响应后,再重新执⾏⽤户程序。
中断的过程如下图,和函数调⽤差不多。
只不过函数调⽤是事先定好位置,⽽中断的位置由“信号”决定。
以键盘为例,当⽤户按下键盘某个按键时,键盘会给cpu的中断引脚发出⼀个⾼电平。
cpu能够捕获这个信号,然后执⾏键盘中断程序。
下图展⽰了各种硬件通过中断与cpu交互。
现在可以回答本节提出的问题了:当⽹卡把数据写⼊到内存后,⽹卡向cpu发出⼀个中断信号,操作系统便能得知有新数据到来,再通过⽹卡中断程序去处理数据。
三、进程阻塞为什么不占⽤cpu资源?了解epoll本质的第三步,要从操作系统进程调度的⾓度来看数据接收。
epoll原理详解及epoll反应堆模型
epoll原理详解及epoll反应堆模型⽂章⽬录⼀、epoll原理详解⼆、epoll的两种触发模式三、epoll反应堆模型 设想⼀个场景:有100万⽤户同时与⼀个进程保持着TCP连接,⽽每⼀时刻只有⼏⼗个或⼏百个TCP连接是活跃的(接收TCP包),也就是说在每⼀时刻进程只需要处理这100万连接中的⼀⼩部分连接。
那么,如何才能⾼效的处理这种场景呢?进程是否在每次询问操作系统收集有事件发⽣的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发⽣的⼏百个连接呢?实际上,在Linux2.4版本以前,那时的select或者poll事件驱动⽅式是这样做的。
这⾥有个⾮常明显的问题,即在某⼀时刻,进程收集有事件的连接时,其实这100万连接中的⼤部分都是没有事件发⽣的。
因此如果每次收集事件时,都把100万连接的套接字传给操作系统(这⾸先是⽤户态内存到内核态内存的⼤量复制),⽽由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨⼤的资源浪费,然后select和poll就是这样做的,因此它们最多只能处理⼏千个并发连接。
⽽epoll不这样做,它在Linux内核中申请了⼀个简易的⽂件系统,把原先的⼀个select或poll调⽤分成了3部分:1int epoll_create(int size);2int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);3int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);1. 调⽤epoll_create建⽴⼀个epoll对象(在epoll⽂件系统中给这个句柄分配资源);2. 调⽤epoll_ctl向epoll对象中添加这100万个连接的套接字;3. 调⽤epoll_wait收集发⽣事件的连接。
IO模型(epoll)--详解-03
IO模型(epoll)--详解-03写在前⾯ epoll是开发linux⾼性能服务器的必备技术⾄,epoll本质,是服务端程序员的必须掌握的知识。
七、epoll的原理和流程 本节会以⽰例和图表来讲解epoll的原理和流程。
创建epoll对象 如下图所⽰,当某个进程调⽤epoll_create⽅法时,内核会创建⼀个eventpoll对象(也就是程序中epfd所代表的对象)。
eventpoll对象也是⽂件系统中的⼀员,和socket⼀样,它也会有等待队列。
内核创建eventpoll对象 创建⼀个代表该epoll的eventpoll对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为eventpoll的成员。
维护监视列表 创建epoll对象后,可以⽤epoll_ctl添加或删除所要监听的socket。
以添加socket为例,如下图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
添加所要监听的socket 当socket收到数据后,中断程序会操作eventpoll对象,⽽不是直接操作进程。
接收数据 当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket引⽤。
如下图展⽰的是sock2和sock3收到数据后,中断程序让rdlist引⽤这两个socket。
给就绪列表添加引⽤ eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,⽽是通过改变eventpoll的就绪列表来改变进程状态。
当程序执⾏到epoll_wait时,如果rdlist已经引⽤了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程。
阻塞和唤醒进程 假设计算机中正在运⾏进程A和进程B,在某时刻进程A运⾏到了epoll_wait语句。
如下图所⽰,内核会将进程A放⼊eventpoll的等待队列中,阻塞进程。
epoll内核原理
epoll内核原理
epoll 是 Linux 内核提供的一种高效的 I/O 事件通知机制,用于处理大量的并发 I/O。
它是基于事件驱动的,能够监视大量的文件描述符,当文件描述符上有事件发生时,会立即通知应用程序进行处理,从而避免了传统的轮询方式带来的性能损耗。
在内核中,epoll 通过三个系统调用来实现其功能,
epoll_create、epoll_ctl 和 epoll_wait。
当应用程序调用
epoll_create 时,内核会创建一个 epoll 实例,并返回一个文件描述符,应用程序可以通过这个文件描述符来操作 epoll 实例。
然后,应用程序可以使用 epoll_ctl 来向 epoll 实例中注册文件描述符以及关注的事件类型。
最后,应用程序可以使用 epoll_wait 来等待事件的发生,当有事件发生时,epoll_wait 会返回就绪的文件描述符列表,应用程序可以对这些文件描述符进行 I/O 操作。
在实现上,epoll 采用了红黑树和双链表的数据结构来管理文件描述符和事件。
当文件描述符上有事件发生时,内核会遍历红黑树,找到相应的文件描述符节点,并将其加入到就绪链表中,然后通知应用程序。
这种设计使得 epoll 能够高效地处理大量的并发连接,而不需要像传统的 select 和 poll 一样需要遍历所有的文件
描述符。
总的来说,epoll 的内核原理主要包括了基于事件驱动的设计、红黑树和双链表的数据结构以及三个系统调用的实现。
这些特点使
得 epoll 成为了处理大规模并发 I/O 的首选机制,为高性能的网
络应用提供了强大的支持。
高并发网络编程之epoll详解
⾼并发⽹络编程之epoll详解epoll的简介: epoll是Linux内核为处理⼤批量⽂件描述符⽽作了改进的poll,是Linux下多路复⽤IO接⼝select/poll的增强版本,它能显著提⾼程序在⼤量并发连接中只有少量活跃的情况下的系统CPU利⽤率。
另⼀点原因就是获取事件的时候,它⽆须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒⽽加⼊Ready队列的描述符集合就⾏了。
epoll除了提供select/poll那种IO事件的⽔平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得⽤户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调⽤,提⾼应⽤程序效率。
在linux 没有实现epoll事件驱动机制之前,我们⼀般选择⽤select或者poll等IO多路复⽤的⽅法来实现并发服务程序。
在⼤数据、⾼并发、集群等⼀些名词唱得⽕热之年代,select和poll的⽤武之地越来越有限,风头已经被epoll占尽。
本⽂便来介绍epoll的实现机制,并附带讲解⼀下select和poll。
通过对⽐其不同的实现机制,真正理解为何epoll能实现⾼并发。
select()和poll() IO多路复⽤模型select的缺点:1. 单个进程能够监视的⽂件描述符的数量存在最⼤限制,通常是1024,当然可以更改数量,但由于select采⽤轮询的⽅式扫描⽂件描述符,⽂件描述符数量越多,性能越差;(在linux内核头⽂件中,有这样的定义:#define __FD_SETSIZE 1024)2. 内核 / ⽤户空间内存拷贝问题,select需要复制⼤量的句柄数据结构,产⽣巨⼤的开销;3. select返回的是含有整个句柄的数组,应⽤程序需要遍历整个数组才能发现哪些句柄发⽣了事件;4. select的触发⽅式是⽔平触发,应⽤程序如果没有完成对⼀个已经就绪的⽂件描述符进⾏IO操作,那么之后每次select调⽤还是会将这些⽂件描述符通知进程。
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的优点呢。
2.1 PPC/TPC模型
这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我。只是P PC是为它开了一个进程,而TPC开了一个线程。可是别烦我是有代价的,它要时间和 空间啊,连接多了之后,那么多的进程/线程切换,这开销就上来了;因此这类模型能 接受的最大连接数都不会高,一般在几百个左右。
epoll_data_t data;
// User datavariable
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t; 可见epoll_data是一个union结构体,借助于它应用程序可以保存很多类型的信息:fd、指 针等等。有了它,应用程序就可以直接定位目标了。 6. 使用Epoll 既然Epoll相比select这么好,那么用起来如何呢?会不会很繁琐啊…先看看下面的三 个函数吧,就知道Epoll的易用了。
3.1. Epoll没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大 于2048, 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/filemax察看。 3.2. 效率提升,Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关 ,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 3.3. 内存拷贝,Epoll在这点上使用了“共享内存”,这个内存拷贝也省略了。 4. Epoll为什么高效 Epoll的高效和其数据结构的设计是密不可分的,这个下面就会提到。 首先回忆一下select模型,当有I/O事件到来时,select通知应用程序有事件到了快去处 理,而应用程序必须轮询所有的FD集合,测试每个FD是否有事件发生,并处理事件; 代码像下面这样:
epoll 的原理
epoll的原理
epoll的原理如下:
(1)epoll实现了单进程,单进程,不堵塞服务器。
对于资源占用是最小的和协程中gevent的原理类似
(2)工作原理为在进程内存空间和Kernel操作系统内存空间中开辟了一个新的内存空间。
这个内存空间用来存储浏览器客户端的访问网络套接字。
进程和操作系统都可以访问。
优化在于原来的访问列表中客户端服务的套接字列表需要先拷贝到操作系统的内存空间中,当列表很大时,效率会下降。
(3)在询问套接字是否接收到客户端发来的信息时使用的方法是事件通知,比较与轮询的方式效率要大很多。
c语言epoll详解
c语言epoll详解摘要:1.简介- 什么是C 语言epoll- epoll 的作用- epoll 与select、poll 的关系2.epoll 的工作原理- epoll 的事件驱动模型- epoll 的文件描述符集合- epoll 的回调函数3.epoll 的安装与配置- epoll 的编译与安装- epoll 的配置选项- epoll 的错误处理4.使用epoll进行I/O多路复用- 创建epoll 实例- 添加/修改/删除事件- 查询事件- 处理事件5.epoll 的高级特性- epoll 的边缘触发(ET) 与水平触发(LT)- epoll 的批量处理- epoll 的效率与性能6.epoll 在实际项目中的应用- 网络通信应用- 服务器应用- 客户端应用正文:C 语言epoll 详解1.简介C语言epoll是Linux系统下的一种I/O多路复用技术,它允许程序监视多个文件描述符,在某个文件描述符就绪时,就能够进行相应的读写操作。
epoll相比传统的select和poll技术,具有更高的性能和更低的资源消耗。
在Linux系统下,epoll被广泛应用于网络通信、服务器和客户端等场景。
2.epoll 的工作原理epoll 的工作原理主要包括事件驱动模型、文件描述符集合和回调函数。
首先,epoll 会创建一个文件描述符集合,程序可以将需要监视的文件描述符添加到该集合中。
当文件描述符就绪时,epoll 会通过回调函数通知程序进行相应的操作。
这种机制使得程序能够高效地处理I/O 事件,而无需轮询等待。
3.epoll 的安装与配置在编译和安装epoll 时,需要确保相关的库文件和头文件已经正确配置。
此外,epoll 提供了多种配置选项,如设置最大文件描述符数量、超时时间等。
在配置过程中,还需要注意错误处理,以便在出现问题时能够及时发现和处理。
4.使用epoll进行I/O多路复用使用epoll进行I/O多路复用的过程主要包括创建epoll实例、添加/修改/删除事件、查询事件和处理事件。
多路复用之epoll总结
IO 多路复用之epoll 总结1、基本知识epoll 是在2.6 内核中提出的,是之前的select 和poll 的增强版本。
相对于select 和poll 来说,epoll 更加灵活,没有描述符限制。
epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy 只需一次。
2、epoll 接口epoll 操作过程需要三个接口,分别如下:#include <sys/epoll.h>int epoll_create(intsize);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);(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 的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
c语言epoll详解
c语言epoll详解【引言】在网络编程中,事件驱动编程模式越来越受到开发者的喜爱。
C语言作为一种广泛应用于网络编程的语言,其对事件驱动编程的支持相对有限。
然而,随着epoll技术的出现,C语言在处理大量并发事件方面有了新的突破。
本文将详细介绍C语言中的epoll技术,包括其工作原理、使用方法以及实战案例。
【epoll简介】epoll是Linux系统中一种高性能的事件驱动I/O处理机制,它允许开发者通过一个文件描述符监听多个文件描述符的事件,如TCP连接的读写事件。
相较于传统的select、poll等I/O多路复用技术,epoll具有更高的效率和更好的可扩展性。
【epoll的工作原理】epoll的工作原理可以分为两个主要部分:epoll实例和epoll事件。
首先,开发者需要创建一个epoll实例,并通过配置相应的文件描述符监听事件。
其次,当文件描述符上有事件发生时,epoll会通知开发者进行处理。
这个过程可以概括为以下几个步骤:1.创建epoll实例2.添加文件描述符到epoll实例3.监控文件描述符的事件4.处理文件描述符上的事件5.移除文件描述符from epoll实例【epoll的使用方法】在C语言中,可以使用以下步骤来实现epoll的功能:1.初始化epoll实例2.添加文件描述符到epoll实例3.循环监控文件描述符的事件4.处理文件描述符上的事件5.关闭epoll实例以下是一个简单的epoll使用示例:```c#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/epoll.h>#include <arpa/inet.h>int main(){// 初始化epoll实例int epoll_fd = epoll_create1(0);// 添加文件描述符到epoll实例struct epoll_event event;event.data.fd = socket_fd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);// 循环监控文件描述符的事件struct epoll_event *events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);while (1){int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);for (int i = 0; i < num_events; i++){// 处理文件描述符上的事件if (events[i].data.fd == socket_fd){// 处理TCP连接的读事件handle_read(socket_fd);}}}// 关闭epoll实例close(epoll_fd);return 0;}```【实战案例】在上面的示例中,我们使用epoll监听TCP连接的读事件。
epoll机制方法
epoll机制方法
Epoll机制是一种高效的I/O多路复用技术,它可以同时监控多个文件描述符,当其中任意一个文件描述符有数据可读或可写时,就会通知应用程序进行相应的处理。
相比于传统的select和poll机制,epoll机制具有更高的性能和更好的扩展性。
Epoll机制的核心是一个epoll文件描述符,它是一个内核对象,用于管理所有被监控的文件描述符。
应用程序可以通过epoll_create 函数创建一个epoll文件描述符,然后通过epoll_ctl函数向其中添加或删除被监控的文件描述符。
当有数据可读或可写时,内核会将相应的事件通知给应用程序,应用程序可以通过epoll_wait函数获取这些事件并进行相应的处理。
Epoll机制的优点在于它使用了一个事件驱动的模型,可以避免不必要的轮询操作,从而减少了系统调用的次数和CPU的占用率。
此外,epoll机制还支持边缘触发和水平触发两种模式,可以根据应用程序的需要进行选择。
边缘触发模式只在文件描述符状态发生变化时通知应用程序,而水平触发模式则在文件描述符状态保持不变时也会不断通知应用程序。
Epoll机制的缺点在于它需要维护一个epoll文件描述符和一个事件列表,这会占用一定的内存空间。
此外,epoll机制对于大量的文件描述符的管理可能会出现性能瓶颈,需要进行一定的优化。
总的来说,Epoll机制是一种高效的I/O多路复用技术,可以大大提高应用程序的性能和可扩展性。
在实际应用中,应该根据具体的需求选择合适的触发模式,并进行适当的优化,以达到最佳的性能表现。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
epoll精髓在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:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);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()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
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;elsereturn;}else if(buflen == 0){// 这里表示对端的socket已正常关闭.}if(buflen == sizeof(buf)rs = 1; // 需要再次读取elsers = 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;return tmp;}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, &clilen);if(connfd<0){perror("connfd<0");exit(1);}//setnonblocking(connfd);char *str = inet_ntoa(clientaddr.sin_addr);cout << "accapt a connection from " << str << endl;//设置用于读操作的文件描述符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);}运行server和client发现,server仅仅读取了5字节的数据,而client其实发送了10字节的数据,也就是说,server仅当第一次监听到了EPOLLIN事件,由于没有读取完数据,而且采用的是ET模式,状态在此之后不发生变化,因此server再也接收不到EPOLLIN事件了.如果我们把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为什么这么快epoll是多路复用IO(I/O Multiplexing)中的一种方式,但是仅用于linux2.6以上内核,在开始讨论这个问题之前,先来解释一下为什么需要多路复用IO.以一个生活中的例子来解释.假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面.如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的.现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色.进一步解释select和epoll模型的差异.select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了,于是在实际的代码中,select版大妈做的是以下的事情:int n = select(&readset,NULL,NULL,100);for (int i = 0; n > 0; ++i){if (FD_ISSET(fdarray[i], &readset)){do_something(fdarray[i]);--n;}}epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.于是epoll版大妈做的事情可以用如下的代码表示:n=epoll_wait(epfd,events,20,500);for(i=0;i<n;++i){do_something(events[n]);}在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 */};可以看到,epoll_data是一个union结构体,它就是epoll版大妈用于保存同学信息的结构体,它可以保存很多类型的信息:fd,指针,等等.有了这个结构体,epoll大妈可以不用吹灰之力就可以定位到同学甲.别小看了这些效率的提高,在一个大规模并发的服务器中,轮询IO是最耗时间的操作之一.再回到那个例子中,如果每到来一个朋友楼管大妈都要全楼的查询同学,那么处理的效率必然就低下了,过不久楼底就有不少的人了.对比最早给出的阻塞IO的处理模型, 可以看到采用了多路复用IO之后, 程序可以自由的进行自己除了IO操作之外的工作, 只有到IO状态发生变化的时候由多路复用IO进行通知, 然后再采取相应的操作, 而不用一直阻塞等待IO状态发生变化了.从上面的分析也可以看出,epoll比select的提高实际上是一个用空间换时间思想的具体应用.多进程服务器中,epoll的创建应该在创建子进程之后看我的测试代码,似乎应该是在创建子进程之后创建epoll的fd,否则程序将会有问题,试将代码中两个CreateWorker函数的调用位置分别调用,一个在创建epoll fd之前,一个在之后,在调用在创建之前的代码会出问题,在我的机器上(linux内核2.6.26)表现的症状就是所有进程的epoll_wait函数返回0, 而客户端似乎被阻塞了:服务器端:#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>#include <sys/types.h>#include <sys/wait.h>using namespace std;#define MAXLINE 5#define OPEN_MAX 100#define LISTENQ 20#define SERV_PORT 5000#define INFTIM 1000typedef struct task_t{int fd;char buffer[100];int n;}task_t;int CreateWorker(int nWorker) {if (0 < nWorker){bool bIsChild;pid_t nPid;while (!bIsChild){if (0 < nWorker){nPid = ::fork();if (nPid > 0){bIsChild = false;--nWorker;}else if (0 == nPid){bIsChild = true;printf("create worker %d success!\n", ::getpid()); }else{printf("fork error: %s\n", ::strerror(errno));return -1;}}else{int nStatus;if (-1 == ::wait(&nStatus)){++nWorker;}}}}return 0;}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 i, maxi, listenfd, connfd, sockfd,epfd,nfds; ssize_t n;char line[MAXLINE];socklen_t clilen;struct epoll_event ev,events[20];struct sockaddr_in clientaddr;struct sockaddr_in serveraddr;listenfd = socket(AF_INET, SOCK_STREAM, 0);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);// 地址重用int nOptVal = 1;socklen_t nOptLen = sizeof(int);if (-1 == ::setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &nOptVal, nOptLen)) {return -1;}setnonblocking(listenfd);bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));listen(listenfd, LISTENQ);CreateWorker(5);//把socket设置为非阻塞方式//生成用于处理accept的epoll专用的文件描述符 epfd=epoll_create(256);//设置与要处理的事件相关的文件描述符ev.data.fd=listenfd;//设置要处理的事件类型ev.events=EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注册epoll事件epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);//CreateWorker(5);maxi = 0;task_t task;task_t *ptask;while(true){//等待epoll事件的发生nfds=epoll_wait(epfd,events,20,500);//处理所发生的所有事件for(i=0;i<nfds;++i){if(events[i].data.fd==listenfd){connfd = accept(listenfd,NULL, NULL);if(connfd<0){printf("connfd<0, listenfd = %d\n", listenfd);printf("error = %s\n", strerror(errno));exit(1);}setnonblocking(connfd);//设置用于读操作的文件描述符memset(&task, 0, sizeof(task));task.fd = connfd;ev.data.ptr = &task;//设置用于注册的读操作事件ev.events=EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注册evepoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);}else if(events[i].events&EPOLLIN){cout << "EPOLLIN" << endl;ptask = (task_t*)events[i].data.ptr;sockfd = ptask->fd;if ( (ptask->n = read(sockfd, ptask->buffer, 100)) < 0) { if (errno == ECONNRESET) {close(sockfd);events[i].data.ptr = NULL;} elsestd::cout<<"readline error"<<std::endl; } else if (ptask->n == 0) {close(sockfd);events[i].data.ptr = NULL;}ptask->buffer[ptask->n] = '\0';cout << "read " << ptask->buffer << endl;//设置用于写操作的文件描述符ev.data.ptr = ptask;//设置用于注测的写操作事件ev.events=EPOLLOUT|EPOLLET;//修改sockfd上要处理的事件为EPOLLOUTepoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); }else if(events[i].events&EPOLLOUT){cout << "EPOLLOUT" << endl;ptask = (task_t*)events[i].data.ptr;sockfd = ptask->fd;write(sockfd, ptask->buffer, ptask->n);//设置用于读操作的文件描述符ev.data.ptr = ptask;//修改sockfd上要处理的事件为EPOLINepoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ev); cout << "write " << ptask->buffer;memset(ptask, 0, sizeof(*ptask));close(sockfd);}}}return 0;}测试客户端:#!/usr/bin/perluse strict;use Socket;use IO::Handle;sub echoclient{my $host = "127.0.0.1";my $port = 5000;my $protocol = getprotobyname("TCP");$host = inet_aton($host);socket(SOCK, AF_INET, SOCK_STREAM, $protocol) or die "socket() failed: $!";my $dest_addr = sockaddr_in($port, $host);connect(SOCK, $dest_addr) or die "connect() failed: $!";SOCK->autoflush(1);my $msg_out = "hello world\n";print "out = ", $msg_out;print SOCK $msg_out;my $msg_in = <SOCK>;print "in = ", $msg_in;close SOCK;}#&echoclient;#exit(0);for (my $i = 0; $i < 9999; $i++){echoclient;}我查看了lighttpd的实现,也是在创建完子进程之后才创建的epoll的fd.请问谁知道哪里有讲解这个的文档?这是美丽的分割线:-----------------------------------------------------------------------感谢luke, 他帮我解释了这个问题的原因:假如fd1是由A进程加入epfd的,而且用的是ET模式,那么加入通知的是进程B,显然B 进程不会对fd1进行处理,所以以后fd1的事件再不会通知,所以经过几次循环之后,所有的fd都没有事件通知了,所以epoll_wait在timeout之后就返回0了。