epoll_epoll详解 - CSDN
epoll 订阅
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。 展开全文
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
信息
外文名
epoll
工作方式
LT和ET
系统调用
epoll_create, epoll_ctl等
优    点
内核微调等
epoll优点
支持一个进程打开大数目的socket描述符select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译服务器代码,不过资料也同时指出这样会带来网络效率的下 降,二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。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,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。没有使用mmap加速内核与用户空间的消息传递
收起全文
精华内容
参与话题
  • 终于有人把epoll讲彻底了

    千次阅读 2019-05-16 20:03:33
    在此申明一下:我的文章有...这篇文章从头到尾介绍了epoll的原理及应用,很不错的一篇文章,分享给大家。 转载自:https://blog.csdn.net/qq_31967569/article/details/89678482 https://www.toutiao.com/i...

     

     

     

     

    在此申明一下:我的文章有自己原创的,也有转载的,转载的一般都是我看到比较优秀的文章。

     

     

    这篇文章从头到尾介绍了epoll的原理及应用,很不错的一篇文章,分享给大家。

     

     

     

    转载自:https://blog.csdn.net/qq_31967569/article/details/89678482

    https://www.toutiao.com/i6683264188661367309/

     

     

    目录

    一、从网卡接收数据说起

    二、如何知道接收了数据?

    三、进程阻塞为什么不占用cpu资源?

    四、内核接收网络数据全过程

    五、同时监视多个socket的简单方法

    六、epoll的设计思路

    七、epoll的原理和流程

    八、epoll的实现细节

    九、结论

    从事服务端开发,少不了要接触网络编程。epoll作为linux下高性能网络服务器的必备技术至关重要,nginx、redis、skynet和大部分游戏服务器都使用到这一多路复用技术。

    因为epoll的重要性,不少游戏公司在招聘服务端同学时,会问及epoll相关的问题。比如epoll和select的区别是什么?epoll高效率的原因是什么?如果只靠背诵,显然不能算上深刻的理解。

    网上虽然也有不少讲解epoll的文章,但要不是过于浅显,就是陷入源码解析,很少能有通俗易懂的。于是决定编写此文,让缺乏专业背景知识的读者也能够明白epoll的原理。文章核心思想是:

    要让读者清晰明白EPOLL为什么性能好。

    本文会从网卡接收数据的流程讲起,串联起CPU中断、操作系统进程调度等知识;再一步步分析阻塞接收数据、select到epoll的进化过程;最后探究epoll的实现细节。

     

    一、从网卡接收数据说起

    下图是一个典型的计算机结构图,计算机由CPU、存储器(内存)、网络接口等部件组成。了解epoll本质的第一步,要从硬件的角度看计算机怎样接收网络数据。

    计算机结构图(图片来源:linux内核完全注释之微型计算机组成结构)

     

     

    下图展示了网卡接收数据的过程。在①阶段,网卡收到网线传来的数据;经过②阶段的硬件电路的传输;最终将数据写入到内存中的某个地址上(③阶段)。这个过程涉及到DMA传输、IO通路选择等硬件有关的知识,但我们只需知道:网卡会把接收到的数据写入内存。

    网卡接收数据的过程

     

    ​通过硬件传输,网卡接收的数据存放到内存中。操作系统就可以去读取它们。

     

     

    二、如何知道接收了数据?

    了解epoll本质的第二步,要从CPU的角度来看数据接收。要理解这个问题,要先了解一个概念——中断。

    计算机执行程序时,会有优先级的需求。比如,当计算机收到断电信号时(电容可以保存少许电量,供CPU运行很短的一小段时间),它应立即去保存数据,保存数据的程序具有较高的优先级。

     

    一般而言,由硬件产生的信号需要cpu立马做出回应(不然数据可能就丢失),所以它的优先级很高。cpu理应中断掉正在执行的程序,去做出响应;当cpu完成对硬件的响应后,再重新执行用户程序。中断的过程如下图,和函数调用差不多。只不过函数调用是事先定好位置,而中断的位置由“信号”决定。

    中断程序调用

     

    以键盘为例,当用户按下键盘某个按键时,键盘会给cpu的中断引脚发出一个高电平。cpu能够捕获这个信号,然后执行键盘中断程序。下图展示了各种硬件通过中断与cpu交互。

    cpu中断(图片来源:net.pku.edu.cn)

     

    现在可以回答本节提出的问题了:当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。

     

     

    三、进程阻塞为什么不占用cpu资源?

    了解epoll本质的第三步,要从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select和epoll都是阻塞方法。了解“进程阻塞为什么不占用cpu资源?”,也就能够了解这一步。

    为简单起见,我们从普通的recv接收开始分析,先看看下面代码:

    //创建

    socketint s = socket(AF_INET, SOCK_STREAM, 0);

    //绑定

    bind(s, ...)

    //监听

    listen(s, ...)

    //接受客户端连接

    int c = accept(s, ...)

    //接收客户端数据

    recv(c, ...);

    //将数据打印出来

    printf(...)

     

    这是一段最基础的网络编程代码,先新建socket对象,依次调用bind、listen、accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。

     

    那么阻塞的原理是什么?

     

     

    工作队列

    操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到recv时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。

     

    下图中的计算机中运行着A、B、C三个进程,其中进程A执行着上述基础网络程序,一开始,这3个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。

    工作队列中有A、B和C三个进程

     

     

    等待队列

    当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象(如下图)。这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。

    创建socket

     

    当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中(如下图)。由于工作队列只剩下了进程B和C,依据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。

     

    socket的等待队列

     

    ps:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。

     

     

    唤醒进程

    当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。也由于socket的接收缓冲区已经有了数据,recv可以返回接收到的数据。

     

     

    四、内核接收网络数据全过程

    这一步,贯穿网卡、中断、进程调度的知识,叙述阻塞recv下,内核接收数据全过程。

    如下图所示,进程在recv阻塞期间,计算机收到了对端传送的数据(步骤①)。数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面(步骤④),再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。

    内核接收数据全过程

     

    唤醒进程的过程如下图所示。

    唤醒进程

     

    以上是内核接收数据全过程

    这里留有两个思考题,大家先想一想。

    其一,操作系统如何知道网络数据对应于哪个socket?

    其二,如何同时监视多个socket的数据?

    (——我是分割线,想好了才能往下看哦~)

    公布答案的时刻到了。

    第一个问题:因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。

    第二个问题是多路复用的重中之重,是本文后半部分的重点!

     

     

     

    五、同时监视多个socket的简单方法

    服务端需要管理多个客户端连接,而recv只能监视单个socket,这种矛盾下,人们开始寻找监视多个socket的方法。epoll的要义是高效的监视多个socket。从历史发展角度看,必然先出现一种不太高效的方法,人们再加以改进。只有先理解了不太高效的方法,才能够理解epoll的本质。

     

    假如能够预先传入一个socket列表,如果列表中的socket都没有数据,挂起进程,直到有一个socket收到数据,唤醒进程。这种方法很直接,也是select的设计思想。

     

    为方便理解,我们先复习select的用法。在如下的代码中,先准备一个数组(下面代码中的fds),让fds存放着所有需要监视的socket。然后调用select,如果fds中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。用户可以遍历fds,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。

     

    int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...)listen(s, ...)

    int fds[] = 存放需要监听的socket

    while(1){

    int n = select(..., fds, ...)

    for(int i=0; i < fds.count; i++){

    if(FD_ISSET(fds[i], ...)){

    //fds[i]的数据处理

    }

    }}

     

     

    select的流程

    select的实现思路很直接。假如程序同时监视如下图的sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。

    操作系统把进程A分别加入这三个socket的等待队列中

     

    当任何一个socket收到数据后,中断程序将唤起进程。下图展示了sock2接收到了数据的处理流程。

    ps:recv和select的中断回调可以设置成不同的内容。

    sock2接收到了数据,中断程序唤起进程A

     

    所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。如下图所示。

    将进程A从所有等待队列中移除,再加入到工作队列里面

     

    经由这些步骤,当进程A被唤醒后,它知道至少有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到就绪的socket。

    这种简单方式行之有效,在几乎所有操作系统都有对应的实现。

     

    但是简单的方法往往有缺点,主要是:

    其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。

    其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。

    那么,有没有减少遍历的方法?有没有保存就绪socket的方法?这两个问题便是epoll技术要解决的。

    补充说明:本节只解释了select的一种情形。当程序调用select时,内核会先遍历一遍socket,如果有一个以上的socket接收缓冲区有数据,那么select直接返回,不会阻塞。这也是为什么select的返回值有可能大于1的原因之一。如果没有socket有数据,进程才会阻塞。

     

     

    六、epoll的设计思路

    epoll是在select出现N多年后才被发明的,是select和poll的增强版本。epoll通过以下一些措施来改进效率。

     

    措施一:功能分离

     

    select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。如下图所示,每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见的,效率就能得到提升。

    相比select,epoll拆分了功能

     

    为方便理解后续的内容,我们先复习下epoll的用法。如下的代码中,先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据。

     

    int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...)listen(s, ...)

    int epfd = epoll_create(...);epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中

    while(1){

    int n = epoll_wait(...)

    for(接收到数据的socket){

    //处理

    }}

     

    功能分离,使得epoll有了优化的可能。

     

    措施二:就绪列表

    select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。如下图所示,计算机共有三个socket,收到数据的sock2和sock3被rdlist(就绪列表)所引用。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。

    就绪列表示意图

     

     

    七、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_wait阻塞进程

     

    当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。

    epoll唤醒进程

     

     

    八、epoll的实现细节

     

    至此,相信读者对epoll的本质已经有一定的了解。但我们还留有一个问题,eventpoll的数据结构是什么样子?

    再留两个问题,就绪队列应该应使用什么数据结构?eventpoll应使用什么数据结构来管理通过epoll_ctl添加或删除的socket?

     

    (——我是分割线,想好了才能往下看哦~)

    如下图所示,eventpoll包含了lock、mtx、wq(等待队列)、rdlist等成员。rdlist和rbr是我们所关心的。

    epoll原理示意图,图片来源:《深入理解Nginx:模块开发与架构解析(第二版)》,陶辉

     

    就绪列表的数据结构

    就绪列表引用着就绪的socket,所以它应能够快速的插入数据。

    程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。

    所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列(对应上图的rdllist)。

     

    索引结构

    既然epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。epoll使用了红黑树作为索引结构(对应上图的rbr)。

     

    ps:因为操作系统要兼顾多种功能,以及由更多需要保存的数据,rdlist并非直接引用socket,而是通过epitem间接引用,红黑树的节点也是epitem对象。同样,文件系统也并非直接引用着socket。为方便理解,本文中省略了一些间接结构。

     

     

     

    九、结论

    epoll在select和poll(poll和select基本一样,有少量改进)的基础引入了eventpoll作为中间层,使用了先进的数据结构,是一种高效的多路复用技术。

    再留一点作业!

    下表是个很常见的表,描述了select、poll和epoll的区别。读完本文,读者能否解释select和epoll的时间复杂度为什么是O(n)和O(1)?

    ​select、poll和epoll的区别。图片来源《Linux高性能服务器编程》

     

     

     

    展开全文
  • epoll原理图解

    万次阅读 2018-12-29 22:32:03
    流 I\O操作 阻塞 流 可以进行I\O操作的内核对象 文件、管道、套接字…… 流的入口:文件描述符(fd) 所有对流的读写操作,我们都可以称之为IO操作。 那么当一个流中再没有数据,read的时候,或者说 在流中已经写满...

    流 I\O操作 阻塞

    可以进行I\O操作的内核对象
    文件、管道、套接字……
    流的入口:文件描述符(fd)
    在这里插入图片描述
    所有对流的读写操作,我们都可以称之为IO操作。

    那么当一个流中再没有数据,read的时候,或者说 在流中已经写满了数据,再write,我们的IO操作就 会出现一种现象,就是阻塞现象

    阻塞

    在这里插入图片描述

    非堵塞

    在这里插入图片描述

    阻塞等待: 空出大脑可以安心睡觉。(不占用CPU宝贵的时间片)
    非阻塞,忙轮询: 浪费时间,浪费电话费,占用快递员时间(占用CPU,系统资源)

    解决阻塞死等待的办法

    阻塞死等待的缺点
    在这里插入图片描述

    办法一:非阻塞、忙轮询

    在这里插入图片描述

    在这里插入图片描述

    办法二:select

    在这里插入图片描述
    select 代收员 比较懒,她只会告诉你快递到了,但是是谁到的,你需要挨个快递员问一遍

    办法三:epoll(主角出场)

    在这里插入图片描述

    epoll特点好处:

    与select,poll一样,但是增加了对I/O多路复用的技术
    只关心“活跃”的链接,无需遍历全部描述符集合
    能够处理大量的链接请求(系统可以打开的文件数目)

    epoll API

    int epoll_create(int size);
    
    
    int epoll_ctl(int epfd, int op, int fd,
    struct epoll_event *event);
    
    struct epoll_event {
    __uint32_t events; /* epoll 事件 */
    epoll_data_t data; /* 用户传递的数据 */
    }
    
    /**
    * @param epfd 用epoll_create所创建的epoll句柄
    * @param op 表示对epoll监控描述符控制的动作
    *
    * EPOLL_CTL_ADD(注册新的fd到epfd)
    * EPOLL_CTL_MOD(修改已经注册的fd的监听事件)
    * EPOLL_CTL_DEL(epfd删除一个fd)
    *
    * @param fd 需要监听的文件描述符
    * 
    * /*
    * events : {EPOLLIN, EPOLLOUT, EPOLLPRI,
    EPOLLHUP, EPOLLET, EPOLLONESHOT}
    */
    	typedef union epoll_data {
    	void *ptr;
    	int fd;
    	uint32_t u32;
    	uint64_t u64;
    	} epoll_data_t;
    struct epoll_event new_event;
    new_event.events = EPOLLIN | EPOLLOUT;
    new_event.data.fd = 5;
    epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &new_event);
    
    

    图解:
    在这里插入图片描述

    触发模式

    水平触发与边缘触发

    水平触发

    在这里插入图片描述
    在这里插入图片描述

    水平触发优点:

    水平触发的主要特点是,如果用户在监听epoll事件,当内核有事件的时候,会拷贝
    给用户态事件,但是如果用户只处理了一次,那么剩下没有处理的会在下一次
    epoll_wait再次返回该事件。
    这样如果用户永远不处理这个事件,就导致每次都会有该事件从内核到用户的拷
    贝,耗费性能,但是水平触发相对安全,最起码事件不会丢掉,除非用户处理完

    边缘触发

    在这里插入图片描述
    在这里插入图片描述

    边缘触发,相对跟水平触发相反,当内核有事件到达, 只会通知用户一次,至于用
    户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能,但是
    相对来说,如果用户马虎忘记处理,将会产生事件丢的情况。

    select poll epoll的区别参考这个博客

    https://blog.csdn.net/qq_35433716/article/details/82588619

    展开全文
  • 我读过的最好的epoll讲解--转自”知乎

    万次阅读 多人点赞 2018-03-05 19:23:49
    首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。 不管是文件,还是套接字,还是管道,我们都可以把他们看作流。 之后我们来讨论I/O的操作,通过read,我们可以从流中读入...
        首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。

        不管是文件,还是套接字,还是管道,我们都可以把他们看作流。

        之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?

    阻塞:阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。

    非阻塞忙轮询:接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”

        很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。
        大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。

        为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。

    假设有一个管道,进程A为管道的写入方,B为管道的读出方。

    假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
        但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。

    假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”
        也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

    这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

        然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
        于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):
    while true {
    for i in stream[]; {
    if i has data
    read until unavailable
    }
    }
        我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

        为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:
    while true {
    select(streams[])
    for i in streams[] {
    if i has data
    read until unavailable
    }
    }
        于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。
        但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,没一次无差别轮询时间就越长。再次
    说了这么多,终于能好好解释epoll了
        epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
        在讨论epoll的实现细节之前,先把epoll的相关操作列出:

    epoll_create 创建一个epoll对象,一般epollfd = epoll_create()

    epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
    比如
    epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入
    epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入
    epoll_wait(epollfd,...)等待直到注册的事件发生
    (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。

    一个epoll模式的代码大概的样子是:
    while true {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
    read or write till
    }
    }
        限于篇幅,我只说这么多,以揭示原理性的东西,至于epoll的使用细节,请参考man和google,实现细节,请参阅linux kernel source。

    展开全文
  • epoll机制:epoll_create、epoll_ctl、epoll_wait、close

    万次阅读 多人点赞 2014-04-06 20:30:03
    在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时...

    在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,linux/posix_types.h头文件有这样的声明:
    #define__FD_SETSIZE   1024
             表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

     


    epoll的接口非常简单,一共就三个函数:
    1.创建epoll句柄
       int epfd = epoll_create(intsize);                                                           
     
           

           创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽
    函数声明:int epoll_create(int size)
    该 函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。随你定好了。只要你有空间。可参见上面与select之不同

    2.将被监听的描述符添加到epoll句柄或从epool句柄中删除或者对监听事件进行修改

    函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
    参数:
    epfd:由 epoll_create 生成的epoll专用的文件描述符;
    op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除

    fd:关联的文件描述符;
    event:指向epoll_event的指针;
    如果调用成功返回0,不成功返回-1

       int epoll_ctl(int epfd, intop, 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
              第四个参数是告诉内核需要监听什么事件,structepoll_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 */
    };


            events可以是以下几个宏的集合
     
            EPOLLIN:            触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
     
            EPOLLOUT:         触发该事件,表示对应的文件描述符上可以写数据;
     
           EPOLLPRI:           表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
     
           EPOLLERR:        表示对应的文件描述符发生错误;
     
            EPOLLHUP:        表示对应的文件描述符被挂断;
     
           EPOLLET:           将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
     
           EPOLLONESHOT:  只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
    如:
    struct epoll_event ev;
    //设置与要处理的事件相关的文件描述符
    ev.data.fd=listenfd;
    //设置要处理的事件类型
    ev.events=EPOLLIN|EPOLLET;
    //注册epoll事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

    3.等待事件触发,当超过timeout还没有事件触发时,就超时。
       int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout);
        
    等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合maxevents告之内核这个events有多大(数组成员的个数),这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
        该函数返回需要处理的事件数目,如返回0表示已超时。
        返回的事件集合在events数组中,数组中实际存放的成员个数是函数的返回值。返回0表示已经超时。
    函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
    该函数用于轮询I/O事件的发生;
    参数:
    epfd:由epoll_create 生成的epoll专用的文件描述符;
    epoll_event:用于回传代处理事件的数组;
    maxevents:每次能处理的事件数;
    timeout:等待I/O事件发生的超时值(单位我也不太清楚);-1相当于阻塞,0相当于非阻塞。一般用-1即可
    返回发生事件数。
    epoll_wait运行的原理是
    等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。
    并 且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。

    ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    从man手册中,得到ET和LT的具体描述如下
    EPOLL事件有两种模型:
    Edge Triggered(ET) 
          //高速工作方式,错误率比较大,支持no_block socket (非阻塞socket)
    LevelTriggered(LT) 
          //缺省工作方式,即默认的工作方式,支持blocksocketno_blocksocket,错误率比较小。

    假如有这样一个例子:(LT方式,即默认方式下,内核会继续通知,可以读数据,ET方式,内核不会再通知,可以读数据)
    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接口,在后面会介绍避免可能的缺陷。(LT方式可以解决这种缺陷)
     
       基于非阻塞文件句柄
     
     ii  只有当read(2)或者write(2)返回EAGAIN时(认为读完)才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时(即小于sizeof(buf)),就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成

    Level Triggered工作模式         (默认的工作方式)
         相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

    然后详细解释ET, LT:
            
     //没有对就绪的fd进行IO操作,内核会不断的通知
     
            LT(leveltriggered)是缺省的工作方式并且同时支持block和no-blocksocket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表
     
             //没有对就绪的fd进行IO操作,内核不会再进行通知。
     
            ET(edge-triggered)是高速工作方式只支持no-blocksocket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。

    另外,当使用epoll的ET模型(epoll的非默认工作方式)来工作时,当产生了一个EPOLLIN事件后,
            读数据的时候需要考虑的是当recv()返回的大小如果等于要求的大小,即sizeof(buf),那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取
    while(rs) 
              //ET模型
    {
     
             buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
     
             if(buflen < 0)
     
             {
     
                           //由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
     
                           // 在这里就当作是该次事件已处理处.
     
                            if(errno== EAGAIN || errno == EINT)  //即当buflen<0且errno=EAGAIN时,表示没有数据了。(读/写都是这样)
                                   break;
     
                            else
     
                            
    展开全文
  • epoll简介(一)

    2019-07-07 13:11:24
    对于大量的描述符处理,EPOLL更有优势,它提供了三个系统调用来创建管理epoll实例: epoll_create创建一个epoll实例,返回该实例的文件描述符; epoll_ctl注册感兴趣的特定文件描述符,注册的描述符集...
  • epoll内部实现

    千次阅读 2019-04-21 11:14:00
    epoll能够支持百万级别的句柄监听. Nodejs使用的libev,底层是epoll。Nginx使用的epoll边缘触发。 调用过程 1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源) 2. 调用epoll_ctl向...
  • epoll原理详解及epoll反应堆模型

    万次阅读 多人点赞 2019-03-08 15:46:38
      设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理...
  • Epoll原理解析

    万次阅读 多人点赞 2019-10-25 22:06:37
    Epoll 作为 Linux 下高性能网络服务器的必备技术至关重要,Nginx、Redis、Skynet 和大部分游戏服务器都使用到这一多路复用技术。 Epoll 很重要,但是 Epoll 与 Select 的区别是什么呢?Epoll 高效的原因是什么? ...
  • Epoll的本质(内部实现原理)

    万次阅读 多人点赞 2019-07-04 17:15:40
    从事服务端开发,少不了要接触网络编程。epoll作为linux下高性能网络服务器的必备技术至关重要,nginx、redis、...因为epoll的重要性,不少游戏公司(如某某九九)在招聘服务端同学时,可能会问及epoll相关的问题。...
  • IO多路复用之epoll

    千次阅读 2018-07-20 14:21:52
    IO多路复用之epoll 认识epoll 由于poll并没有很大程度的解决select中的缺点,而且还带来了一些额外的开销。在处理大量socket时,select和poll要进行频繁的拷贝和遍历,效率低。所以poll也不建议使用。下面介绍的...
  • epoll使用详解(精髓)

    万次阅读 多人点赞 2009-04-11 16:38:00
    epoll - I/O event notification facility在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd...
  • 高并发网络编程之epoll详解

    万次阅读 多人点赞 2017-06-24 13:43:17
    在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在大数据、高并发、集群等一些名词唱得火热之年代,select和poll的用武之地越来越有限,风头已经被...
  • epoll_event结构体

    万次阅读 2018-12-13 23:08:10
    epoll_event结构体一般用在epoll机制中,其定义如下: struct epoll_event {  uint32_t events; /* Epoll events */  epoll_data_t data; /* User data variable */ } __attribute__ ((__packed__))...
  • 看了网上一个 muduo 的网络模型 http://www.cppblog.com/Solstice/archive/2010/09/08/muduo_vs_libevent_bench.html<br />  文章说道libevent 使用epollEPOLL_CTL_ADD 来修改FD的事件比 EPOLL...
  • mac下面有epoll

    万次阅读 2018-06-19 16:57:21
    没有的,但是mac下面有kqueue,跟epoll原理是差不多的。 这个是没办法的,如果实在需要,就用Ubuntu 吧,这个也可以无缝迁移。 更多资源,更多文章由小白技术社提供(是我啦)...
  • epollwait和epollctl都是线程安全的,但是
  • epoll 水平触发和边缘触发的区别

    万次阅读 2014-06-23 21:57:05
    EPOLLLT——水平触发 EPOLLET——边缘触发 epollEPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作...
  • 探讨epoll_wait

    千次阅读 2012-07-14 14:04:55
    NAME  epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor SYNOPSIS  #include  int epoll_wait(int epfd, struct epoll_event *events,
  • epollEPOLLLT模式和EPOLLET模式比较

    千次阅读 2012-08-08 11:40:00
    epoll是linux系统最新的处理多连接的高效率模型, 工作在两种方式下, EPOLLLT方式和EPOLLET方式。 EPOLLLT是系统默认, 工作在这种方式下, 程序员不易出问题, 在接收数据时,只要socket输入缓存有数据, 都...
  • 使用epoll编写了一个接受父进程、子进程通过fifo通信的小程序,在调试的过程中发现,每次kill子进程的时候,epoll都会报错Interrupted system call,错误号为4。意思大约是epoll_wait被更高级的系统调用打断,上网上...
1 2 3 4 5 ... 20
收藏数 86,529
精华内容 34,611
关键字:

epoll