前篇: 网络编程之IO模型和IO多路复用
select, poll, epoll 在历史上是先后按顺序出现的,后者的提出都是为了解决前者遗留的问题。select是POSIX早期提出的规范,windows和linux等各大标准库都有实现。poll和epoll都是linux上的实现,其中poll和select差别很小, 在高并发和性能上有很大局限,而epoll则解决了大部分问题。
select
select原型函数:
1 | /* |
select存在以下限制:
1 调用select函数会阻塞进程,直到有描述副就绪,或者超时
2 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024;想要修改限制只能通过修改内核宏定义;
3 由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
4 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大,这个开销也很大;
5 select返回的是含有整个fd集合,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
6 select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
poll
poll 和 select 实现机制相似,只是内核用结构链表取代固定数组保存文件描述符集合,也就没有了最大文件描述数量的限制;但是select面临的其他问题,poll上依然存在。
poll原型函数:
1 | /* |
可以看到,poll对select参数传值(readfds, writefds)稍有改进,pollfd结构包含了要监视的event和发生的event。和select函数一样,poll返回后,依然需要轮询pollfd数组来获取就绪的描述符。
epoll
鉴于select和poll的缺陷,epoll采用了完全不同的事件驱动机制,上述select和poll存在的问题在epoll上不复存在。
epoll在Linux内核中建立一个简易的文件系统(文件系统一般用什么数据结构实现?B+树),用来管理epoll对象。epoll对象封装了对于网络IO的所有操作,这样用户对IO的操作就转变为对epoll对象的操作。
epoll对用户提供三个调用接口:
1 | /* 创建一个epoll对象 |
简单来说,epoll的工作机制是这样的:
- 当调用epoll_create时,内核会创建一个epoll对象,epoll对象通过红黑树管理监听事件,通过双向链表存放发生的事件。
- 用户通过调用epoll_ctl向epoll对象注册/销毁监听事件;对于每一个添加到epoll对象中的事件,epoll都会向设备驱动程序注册回调(ep_poll_callback)。当驱动程序有事件发生时会调用该回调函数,它会将发生的事件添加到双向链表中。
- 当用户调用epoll_wait时,内核只需要检查epoll对象中的双向链表是否有事件。如果事件不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。(注意,epoll_wait依然可能造成阻塞。)
由此可见,epoll通过红黑树,双向链表等数据结构,以及事件回调机制,保证了其高效和应对并发的能力。
另外,epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
水平触发(LT):默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。
边缘触发(ET): 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。
总结
可以用以下表格对比来看select, poll, epoll的区别:
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 红黑树 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。