Linux中epoll模型学习

epoll是Linux2.6及以后版本中的一种IO复用技术。可以非常高效的处理数以百万计的socket句柄,今天学习一下它为何能高速处理这么多并发连接。学习epoll之前,先介绍一下Linux中其它处理并发的模型的缺点,包括PPC/TPC,select IO复用,poll IO复用。IO复用就是同时监听多个描述符。

 

一、常用模型的缺点

1、 PPC/TPC模型

Process PerConnection简称PPC,Thread Per Connection简称TPC,这两种模型思想类似,就是让每一个连接自己做事,只是PPC为它开了一个进程,而TPC开了一个线程。这是需要时间和空间代价的,连接多了之后,进程/线程切换开销很大。因此这类模型能接受的最大连接数都不高,一般几百个。

2、 select模型

(1)   最大并发数限制。一个进程打开的文件描述符数量是有限的,由FD_SETSIZE设置,默认值为1024/2048,因此select模型最大并发数被限制。增加FD_SETSIZE可提高并发数,但是效率会线性下降。

(2)   效率问题。select每次调用都会扫描全部fd集合,效率随并发数线性下降。IO事件到了后,select通知应用程序有事件到需要处理,而应用程序必须轮询所有fd集合,测试每个fd是否有事件发生,并处理事件。

(3)   内核/用户空间内存拷贝。内核把fd消息通知用户空间,使用了内存拷贝。

总结:连接数受限、查找配对速度慢、内存拷贝

3、 poll模型

效率基本与select相同,select缺点(2)(3)它同样具有。

 

二、epoll的优点

1、 epoll没有最大连接限制;

2、 效率提升,它只关注”活跃”的连接,而跟连接总数无关,实际网络中效率远高于select。

3、 epoll使用共享内存,不需拷贝。

 

三、epoll实现原理

epoll总共有三个系统调用:

(1)   int epoll_create(int size);

(2)   int epoll_ctl(int epfd, int op,int fd, struct epoll_event* event);

(3)   int epoll_wait(int epfd, structepoll_events, int maxevents, int timeout);

epoll_create建立一个epoll对象。size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核不保证效果。

epoll_ctl可以操纵上述epoll对象,例如将socket加入epoll让其监控或者移除socket,不再监控等。

epoll_wait调用时,在给定的timeout时间内,当监控的所有句柄中有事件发生时,返回到用户态进程。

select和poll中,每次都要传递要监控的socket给select/poll调用,也就是需要将用户态socket列表拷贝到内核,效率很低。而调用epoll_wait就不用传递socket句柄给内核,因为内核已经在epoll_ctl时拿到了要监控的句柄列表。epoll_create后,内核就在内核态开始准备存储要监控的句柄了。每次调用epoll_ctl只是往内核数据结构加入新的socket句柄。

epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个要监控的socket,这些socket以红黑树的形式保存在内核cache中,以支持快速查找插入删除。这个内核高速cache区,就是连续的物理内存页,然后在之上建立slab层,简单说就是物理上分配好想要的size的内存对象,每次使用时使用空闲的已分配好的对象。

即使有数百万的socket句柄,epoll_wait仍然可以快速返回发生事件的句柄。这是由于epoll_create时,内核还建立了一个list链表,用于存储准备就绪的事件。当epoll_wait调用时,仅仅观察这个list链表有没有数据即可。有数据就返回,没数据就sleep,等到timeout时间到后没数据也返回。所以,epoll_wait非常高效。通常情况下大量句柄中仅有少量活动的句柄。这个list链表是如何维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll红黑树上之外,还给内核中断处理程序注册了一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。

一棵红黑树,一张准备就绪list链表,少量内核cache,就帮我们解决了大并发下socket处理问题。执行epoll_create创建红黑树和就绪链表,执行epoll_ctl增加socket句柄,检查是否在红黑树中存在,存在立即返回,不存在添加到红黑树并向内核注册回调函数,用于当中断事件到来时向就绪链表插入数据。执行epoll_wait时立刻返回准备就绪链表的数据即可。

epoll有两种工作模式LT(默认)和ET。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式下仅在第一次返回。这是怎么实现的呢?当一个socket句柄上有事件时,内核会把该句柄插入到就绪list链表,我们调用epoll_wait,会把就绪socket拷贝到用户态内存,然后清空list链表。最后,epoll_wait检查这些socket,如果不是ET模式且socket确实有未处理的事件时,又把该句柄放回就绪链表。所以非ET句柄只要它上面有事件,epoll_wait每次都要返回,而ET模式的句柄,除非有新中断到来,即使socket上的事件没有处理完,也不会次次从epoll_wait返回。


在这里最重要的莫过于select模型和Asynchronous I/O模型。从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方案。最著名的windows完成端口实现的AIO,实际上也只是内部用线程池实现的罢了,最后的结果是IO有个线程池,你的应用程序也需要一个线程池...... 很多文档其实已经指出了这引发的线程context-switch所带来的代价。在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI。但是一直到2.6内核发布,网络模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支持(即:就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当的帮助)。 所以,剩下的select模型基本上就成为我们在linux上面的唯一选择,其实,如果加上no-block socket的配置,可以完成一个"伪"AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。 最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值