服务端需要管理多个客户端连接,而recv
只能监视单个socket
,而且还不能阻塞。这种矛盾下,人们开始寻找监视多个socket
的方法。
假如能够预先传入一个socket
列表,如果列表中的socket
都没有数据,挂起进程,直到有一个socket
收到数据,唤醒进程。这种方法很直接,是select
的设计思想。poll
是select
的一个小改进。底层实现为链表,不再限制最大链接数。epoll
是select
和poll
的增强版本。但当处理大量的连接的读写,select
是低效的。因为kernel
每次都要对select
传入的一组socket
号做轮询。大量的cpu
时间都耗了进去。而使用epoll
这些,派一个文件描述符站岗,效率自然高了许多。
缓存I/O
缓存I/O
又称为标准I/O
,大多数文件系统的默认I/O
操作都是缓存I/O
。在Linux
的缓存I/O
机制中,操作系统会将I/O
的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
五种IO模型
阻塞式I/O模型
默认情况下,所有套接字都是阻塞的。recvfrom
等待数据准备好,从内核向进程复制数据。
非阻塞式I/O
进程把一个套接字设置成非阻塞是在通知内核,当所请求的I/O
操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误,recvfrom
总是立即返回。
I/O多路复用
虽然I/O
多路复用的函数也是阻塞的,但是其与以上两种还是有不同的,I/O多
路复用是阻塞在select
,epoll
这样的系统调用之上,而没有阻塞在真正的I/O
系统调用如recvfrom
之上。其本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。select
、poll
和epoll
都是Linux API
提供的IO
复用方式。
信号驱动式I/O
用的很少,就不做讲解了。
异步I/O
这类函数的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我。recvfrom
函数(经socket
接收数据)。
再看POSIX对同步、异步这两个术语的定义
- 同步
I/O
操作:导致请求进程阻塞,直到I/O
操作完成;- 异步
I/O
操作:不导致请求进程阻塞。
各IO运行机制
select运行机制
select()
的机制中提供一种fd_set
的数据结构,实际上是一个long
类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket
句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()
时,由内核根据IO
状态修改fd_set
的内容,由此来通知执行了select()
的进程哪一Socket
或文件可读。
从流程上来看,使用select
函数进行IO
请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket
,以及调用select
函数的额外操作,效率更差。但是,使用select
以后最大的优势是用户可以在一个线程内同时处理多个socket
的IO请求。用户可以注册多个socket
,然后不断地调用select
读取被激活的socket
,即可达到在同一个线程内同时处理多个IO
请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
poll运行机制
poll
的机制与select类似,与select
在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll
没有最大文件描述符数量的限制。
epoll运行机制
epoll
在Linux2.6
内核正式提出,是基于事件驱动的I/O
方式,相对于select
来说,epoll
没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy
只需一次。epoll
是Linux
内核为处理大批量文件描述符而作了改进的poll
,是Linux
下多路复用IO
接口select/poll
的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU
利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO
事件异步唤醒而加入Ready
队列的描述符集合就行了。epoll
除了提供select/poll
那种IO
事件的水平触发(Level Triggered)
外,还提供了边缘触发(Edge Triggered)
,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait
的调用,提高应用程序效率。
- 水平触发(LT):默认工作模式,即当
epoll_wait
检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait
时,会再次通知此事件。 - 边缘触发(ET): 当
epoll_wait
检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait
时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。LT
和ET
原本应该是用于脉冲信号的,可能用它来解释更加形象。Level
和Edge
指的就是触发点,Level
为只要处于水平,那么就一直触发,而Edge
则为上升沿和下降沿的时候触发。比如:0->1 就是Edge
,1->1 就是Level
。ET
模式很大程度上减少了epoll
事件的触发次数,因此效率比LT
模式下高。
总结
一张图总结一下select,poll,epoll
的区别:epoll
是Linux
目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select
和poll
。目前流行的高性能web
服务器Nginx
正式依赖于epoll
提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O
方式可能性能更好。
既然select,poll,epoll
都是I/O
多路复用的具体的实现,之所以现在同时存在,其实他们也是不同历史时期的产物。
- select出现是1984年在BSD里面实现的
- 14年之后也就是1997年才实现了poll,其实拖那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求
- 2002, 大神 Davide Libenzi 实现了epoll