美文网首页并发编程面经待整理
AIO初步整理(二)——select/poll和epoll

AIO初步整理(二)——select/poll和epoll

作者: sheen口开河 | 来源:发表于2018-06-07 11:07 被阅读24次

    select/poll和epoll

    在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在大数据、高并发、集群等一些名词唱得火热之年代,select和poll的用武之地越来越有限,风头已经被epoll占尽。

    select()和poll() IO多路复用模型

    select基本原理

    select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

    select基本流程

    select的缺点:

    1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;

    在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024

    1. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
    2. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
    3. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。.

    poll基本原理

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

    poll的缺点:
    相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

    epoll IO多路复用模型

    由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

    epoll基本原理

    epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

    epoll的优点:

    1. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
    2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
    3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

    epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

    • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
    • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

    LT和ET模式的优缺点

    1. LT模式
      LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
    2. ET模式
      ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
      ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
    3. 在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)
    • Java NIO对epoll的实现是LT模式
    • Netty实现了ET模式的epoll

    设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

    在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

    epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统。把原先的select/poll调用分成了3个部分:

    1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
    2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字
    3. 调用epoll_wait收集发生的事件的连接

    如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

    下面来看看Linux内核具体的epoll机制实现思路:

    当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

    struct eventpoll{  
        ....  
        /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/  
        struct rb_root  rbr;  
        /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/  
        struct list_head rdlist;  
        ....  
    };  
    

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

    而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

    在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

    struct epitem{  
        struct rb_node  rbn;//红黑树节点  
        struct list_head    rdllink;//双向链表节点  
        struct epoll_filefd  ffd;  //事件句柄信息  
        struct eventpoll *ep;    //指向其所属的eventpoll对象  
        struct epoll_event event; //期待发生的事件类型  
    }  
    

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

    epoll数据结构示意图

    从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

    OK,讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。

    • 第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
    • 第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
    • 第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

    select/poll和epoll对比

    支持一个进程能打开的最大连接数

    image.png

    FD剧增后带来的IO效率

    image.png

    消息传体方式

    image.png

    综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点:

    • 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
    • select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

    DMA
    DMA(Direct Memory Access),即直接存储器存取,是一种快速传送数据的机制。数据传递可以从适配卡到内存,从内存到适配卡或从一段内存到另一段内存。利用它进行数据传送时不需要CPU的参与。每台电脑主机板上都有DMA控制器,通常计算机对其编程,并用一个适配器上的ROM(如软盘驱动控制器上的ROM)来储存程序,这些程序控制DMA传送数据。一旦控制器初始化完成,数据开始传送,DMA就可以脱离CPU,独立完成数据传送。

    参考文章
    https://blog.csdn.net/davidsguo008/article/details/73556811
    https://www.cnblogs.com/jeakeven/p/5435916.html

    相关文章

      网友评论

        本文标题:AIO初步整理(二)——select/poll和epoll

        本文链接:https://www.haomeiwen.com/subject/vkobsftx.html