7. IO 多路复用之 poll
这小节我们介绍 IO 多路复用技术的 poll。
7.1 poll 是什么
poll 是类似于 select 的又一个 linux 提供的内核函数,但是 poll 的底层实现和 select 相比更高效,更详细的差异后面的章节会讲到。
7.2 使用 poll
Python 中 poll 的实现同样也在 select module 中,但是不同于 select,poll 是通过主动注册文件描述符和事件实现对文件描述符的监听,而且 poll 提供了一些列的事件 flag 来进行对应的事件注册:
事件 | 含义 |
---|---|
POLLIN | There is data to read |
POLLPRI | There is urgent data to read |
POLLOUT | Ready for output: writing will not block |
POLLERR | Error condition of some sort |
POLLHUP | Hung up |
POLLNVAL | Invalid request: descriptor not open |
同上文中 select 的例子一样,我们使用 poll 创建一个回显服务,区别是 poll 需要主动注册文件描述符和需要监听的事件:
READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR
READ_WRITE = READ_ONLY | select.POLLOUT
# Set up the poller
poller = select.poll()
poller.register(server, READ_ONLY)
由于 poll 返回的是一个文件描述符号和对应事件的列表,所以我们需要提前记住所有文件描述符的 fileno:
fd_to_socket = { server.fileno(): server}
poll 也有超时设置,不过 poll 的 timeout 单位是 ms,为了让 poll 不会一直阻塞,我们指定 timeout 为 1s:
TIMEOUT = 1000
开启主循环,在 timeout 之后检测返回的事件列表和对应的文件描述符:
while True:
events = poller.poll(TIMEOUT)
for fd, flag in events:
pass
7.3 读事件 POLLIN 和 POLLPRI
poll 的读事件对应的是 POLLIN 和 POLLPRI ,同 select 读事件一样也有以下的几种情形。
情形一
如果文件描述符是 server,说明有新的连接到来,在获取这个连接之后需要把新的连接通过 poll 来进行注册,这样我们就可以监听新连接的事件了:
if s is server:
connection, client_address = s.accept()
connection.setblocking(0)
fd_to_socket[ connection.fileno() ] = connection
poller.register(connection, READ_ONLY)
message_queues[connection] = Queue.Queue()
情形二
如果文件描述符不是 server,说明这个文件描述符有数据可读,此时我们可以使用 recv
来读取数据:
else:
data = s.recv(1024)
if data:
message_queues[s].put(data.capitalize())
poller.modify(s, READ_WRITE)
与此同时,我们也需要修改这个文件描述符需要被监听的事件类型,在读完数据之后我们需要进行数据回显,因此修改事件类型为 READ_WRITE
,这点与 select 不同。
情形三
如果读取的数据是空的,说明 client 关闭了这个连接,我们需要把这个连接从 poll 的注册列表中移除,并且释放对应的资源:
poller.unregister(s)
s.close()
# Remove message queue
del message_queues[s]
7.4 写事件 POLLOUT
对有写事件的文件描述符的处理和 select 类似,区别在于如果没有消息可写,我们修改描述符的监听事件为 READ_ONLY
:
elif flag & select.POLLOUT:
try:
next_msg = message_queues[s].get_nowait()
except Queue.Empty:
poller.modify(s, READ_ONLY)
else:
s.send(next_msg)
7.5 异常处理 POLLHUP 和 POLLERR
POLLHUP
说明 client 没有正确关闭连接,POLLERR
说明有错误发生,这两种情形我们选择取消对文件描述符的监听,并且释放资源:
poller.unregister(s)
s.close()
# Remove message queue
del message_queues[s]
7.6 poll 和 select 的对比
select 和 poll 都支持同时监听多个文件描述符,但是他们有着显著的区别。
select
- select 有最大文件描述符限制
- 为了获取具体哪个文件描述符有事件可用,select 需要对所有监听的文件描述符进行线性扫描
- 每次调用 select ,操作系统都需把文件描述符集合在内核和用户空间中相互拷贝
poll
- poll 没有最大文件描述符限制
- 为了获取哪个文件描述符,poll 也需要对监听的文件描述符进行线性扫描