1. 介绍
Python 中的 select
模块专注于I/O
多路复用,提供了select
、poll
、epoll
三个方法,其中Windows只可以用select
,Unix 三个都可以使用,另外也提供了kqueue
方法(FreeBSD系统)。
1.1. select、poll、epoll区别
select
select
最早于1983年出现在4.2BSD中,它通过一个select()
系统调用来监视多个文件描述符的数组,当select()
返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select
目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select
的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()
所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()
会对所有socket
进行一次线性扫描,所以这也浪费了一定的开销。
poll
poll
在1986年诞生于System V Release 3,它和select
在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll
和select
同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()
和poll()
将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()
和poll()
的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered
)。
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll
,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll
可以同时支持水平触发和边缘触发(Edge Triggered
,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll
同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()
获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll
指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll
采用基于事件的就绪通知方式。在select/poll
中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll
事先通过epoll_ctl()
来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback
的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()
时便得到通知。
2. 使用select
在python中,select
函数是一个对底层操作系统的直接访问的接口。它用来监控sockets
、files
和pipes
,等待I/O
完成(Waiting for I/O completion
)。当有可读、可写或是异常事件产生时,select
可以很容易的监控到。
select.select(rlist, wlist, xlist[, timeout])
传递三个参数:
rlist
:输入而观察的文件对象列表wlist
:输出而观察的文件对象列表xlist
:观察错误异常的文件列表timeout
: 可选参数,表示超时秒数
返回3个tuple,每个tuple都是一个准备好的对象列表,它和前边的参数是一样的顺序。下面,主要结合代码,简单说说select的使用。
2.1. 实例程序
Server端:
- 该程序主要利用
socket
进行通信,接受客户端发送过来的数据,然后再发还给客户端。 - 首先建立一个
TCP/IP
的socket
,并将其设置为非阻塞,然后进行bind
和listen
- 通过
select
函数获取到三种文件列表,分别对每个列表的每个元素进行轮询,对不同socket
进行不同的处理,最外层循环直到inputs列表为空为止 - 当设置timeout参数时,如果发生了超时,select函数会返回三个空列表。