EPOLL的理解和深入分析

搞Linux 服务器开发的人肯定了解 select、poll、epoll,他们都是基于事件驱动的IO多路复用技术,而他们之间的区别网上已经有很多的文章了,大家可以去详细的阅读,我在这里主要想写写我对epoll的底层实现的理解。


首先还是先说说 select、poll相比与epoll来说他们效率低下的原因吧:

select、poll、epoll是Linux平台下的IO多路复用技术,适合用来管理大量的文件描述符,但是这些系统调用本身是阻塞的,而他们管理的socket描述符其实是可以阻塞,也可以非阻塞的,但是大部分情况下设置为非阻塞的要更好一些,效率会更高一些。因此,他们并不是真正的异步IO。是伪异步的。


1、select

首先,select的缺点1:是select管理的描述符的数量在不重新编译内核的情况下是一个固定的值:1024,当然,重新编译了Linux内核之后,这个数值可以继续增大到用户的需求,但是这是相对来说比较麻烦的一件事。

其次。select的缺点2:是select对于socket描述符的管理方式,因为Linux内核对select的实现方式为每次返回前都要对所有的描述符进行一遍遍历,然后将有事件发生的socket描述符放到描述符集合里,然后将这个描述符集合返回。这种情况对于描述符的数量不是很大的时候还是可以的,但是当描述符达到数十万,甚至上百万的时候,select的效率就会急剧的降低,因为这样的轮询机制会造成大量的浪费和资源开销。因为每一次的轮询都要将这些所有的socket描述符从用户态拷贝到内核态,在内核态,进行轮询,查看是否有事件发生,这是select的底层需要做的。而这些拷贝完全是可以避免的。


2、poll

poll的实现机制和select是一样的,也是采用轮询机制来查看有事件发生的socket描述符,所以效率也是很低,但是poll对select有一项改进就是能够监视的描述符是任意大小的而不是局限在一个较小的数值上(当然这个描述符的大小也是需要操作系统来支持的)。


综上:在总结一下,select与poll的实现机制基本是一样的,只不过函数不同,参数不同,但是基本流程是相同的;

1、复制用户数据到内核空间

2、估计超时时间

3、遍历每个文件并调用f_op->poll()取得文件状态

4、遍历完成检查状态

如果有就绪的文件(描述符对应的还是文件,这里就当成是描述符就可以)则跳转到5,

如果有信号产生则重新启动poll或者select

否则挂起进程并等待超时或唤醒超时或再次遍历每个文件的状态

5、将所有文件的就绪状态复制到用户空间

6、清理申请的资源

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


3、epoll

epoll改进了select的两个缺点,使用了三个数据结构从而能够在管理大量的描述符的情况下,对系统资源的使用并没有急剧的增加,而只是对内存的使用有所增加(毕竟存储大量的描述符的数据结构会占用大量内存)。

epoll在实现上的三个核心点是:1、mmap,2、红黑树,3、rdlist(就绪描述符链表)接下来一一解释这三个并且解释为什么会高效;

1、mmap是共享内存,用户进程和内核有一段地址(虚拟存储器地址)映射到了同一块物理地址上,这样当内核要对描述符上的事件进行检查的时候就不用来回的拷贝了。

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

三、共享内存

这点实际上涉及到epoll的具体实现了。内核/用户空间 内存拷贝问题,如何让内核FD消息通知给用户空间呢?

在这个问题上select采取了内存拷贝方法。Poll也是么?应该是,poll和select基本无区别?(肯定多少还有点区别,只是这些缺点是相同的)

既然是内存拷贝,因为也要拷贝啊,还是慢!

对于poll来说需要将用户传入的 pollfd 数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个O(n)操作,当事件发生,poll返回将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是O(n)。


而epoll是共享内存,拷贝都不用,相对来说应该会更快。epoll是通过内核与用户空间mmap同一块内存实现的。 

mmap是什么

mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。

(涉及到内核和用户空间的概念,共享内存有没有安全隐患?)


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

2、红黑树是用来存储这些描述符的,因为红黑树的特性,就是良好的插入,查找,删除性能O(lgN)。

     当内核初始化epoll的时候(当调用epoll_create的时候内核也是个epoll描述符创建了一个文件,毕竟在Linux中一切都是文件,而epoll面对的是一个特殊的文件,和普通文件不同),会开辟出一块内核高速cache区,这块区域用来存储我们要监管的所有的socket描述符,当然在这里面存储一定有一个数据结构,这就是红黑树,由于红黑树的接近平衡的查找,插入,删除能力,在这里显著的提高了对描述符的管理。

3、rdlist   就绪描述符链表这是一个双链表,epoll_wait()函数返回的也是这个就绪链表。

     当内核创建了红黑树之后,同时也会建立一个双向链表rdlist,用于存储准备就绪的描述符,当调用epoll_wait的时候在timeout时间内,只是简单的去管理这个rdlist中是否有数据,如果没有则睡眠至超时,如果有数据则立即返回并将链表中的数据赋值到events数组中。这样就能够高效的管理就绪的描述符,而不用去轮询所有的描述符。所以当管理的描述符很多但是就绪的描述符数量很少的情况下如果用select来实现的话效率可想而知,很低,但是epoll的话确实是非常适合这个时候使用。

      对与rdlist的维护:当执行epoll_ctl时除了把socket描述符放入到红黑树中之外,还会给内核中断处理程序注册一个回调函数,告诉内核,当这个描述符上有事件到达(或者说中断了)的时候就调用这个回调函数。这个回调函数的作用就是将描述符放入到rdlist中,所以当一个socket上的数据到达的时候内核就会把网卡上的数据复制到内核,然后把socket描述符插入就绪链表rdlist中。


补充:epoll的工作模式ET和LT

都知道epoll有两个工作模式,ET和LT,其中ET模式是高速模式,叫做边缘触发模式,LT模式是默认模式,叫做水平触发模式。

这两种工作模式的区别在于:

当工作在ET模式下,如果一个描述符上有数据到达,然后读取这个描述符上的数据如果没有将数据全部读完的话,当下次epoll_wait返回的时候这个描述符里的数据就再也读取不到了,因为这个描述符不会再次触发返回,也就没法去读取,所以对于这种模式下对一个描述符的数据的正确读取方式是用一个死循环一直读,读到么有数据可读的情况下才可以认为是读取结束。

而工作在LT模式下,这种情况就不会发生,如果对一个描述符的数据没有读取完成,那么下次当epoll_wait返回的时候会继续触发,也就可以继续获取到这个描述符,从而能够接着读。


那么这两种模式的实现方式是什么样的?

基于以上的数据结构是怎么实现这种工作模式的呢?

实现原理:当一个socket描述符的中断事件发生,内核会将数据从网卡复制到内核,同时将socket描述符插入到rdlist中,此时如果调用了epoll_wait会把rdlist中的就绪的socekt描述符复制到用户空间,然后清理掉这个rdlist中的数据,最后epoll_wait还会再次检查这些socket描述符,如果是工作在LT模式下,并且这些socket描述符上还有数据没有读取完成,那么L就会再次把没有读完的socket描述符放入到rdlist中,所以再次调用epoll_wait的时候是会再次触发的,而ET模式是不会这么干的。


ET模式在物理实现上是基于电平的高低变化来工作的,就是从高电平变成低电平,或者从低电平变成高电平的这个上升沿或者下降沿才会触发,也就是状态变化导致触发,而当一个描述符上数据未读完的时候这个状态是不会发生变化的,所以触发不了,LT模式是在只有出现高电平的时候才会触发。


高电平和低电平:

LT水平触发:

EPOLLIN的触发事件:当输入缓冲区为空-->低电平,当输入缓冲区不为空-->高电平

高电平的时候触发EPOLLIN事件,如果没有把缓冲区的数据读取完,下次还会触发的,因为始终是高电平

EPOLLOUT的触发事件:当发送缓冲区满-->低电平,当发送缓冲区不满-->高电平

高电平的时候触发EPOLLOUT事件,所以在一开始的时候不要关注EPOLLOUT时间,因为发送缓冲区是不满的所以会导致CPU忙等待,每次都触发。什么时候关注EPOLLOUT事件呢? 当write的时候没有写完全,因为发送缓冲区满了,这个时候才关注EPOLLOUT事件直到下次把所有数据都发送完毕了,才取消EPOLLOUT事件

ET边缘触发:

EPOLLIN事件发生的条件:

有数据到来(输入缓冲区初始为空,为低电平,有数据到来变成了高电平)

EPOLLout事件发生的条件:

内核发送缓冲区不满(当发送缓冲区出现满之后为低电平,然后内核发送出去了部分数据后变成了不满,也就是高电平)


你可能感兴趣的:(网络通信)