6. IO 多路复用之 select
这一节我们说说 O 多路复用的技术 select。
6.1 select 是什么
select 是 linux 内核提供的函数,它允许同时监听多个文件描述符的读写事件,在这些事件中有一个或多个变得可用之后以及经历一段指定的时间之后通知用户进程。
6.2 使用 select
Python 中提供了 select 模块来调用系统的 select 函数:
select.select(rlist, wlist, xlist[, timeout]
select 有 4 个参数,分别是读事件监听对象,写事件监听对象,异常事件对象,超时设置。
在这个例子中,我们要创建一个回显服务,首先我们创建一个 socket 来处理连接监听:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(0)
server_address = ('localhost', 8000)
server.bind(server_address)
server.listen(10)
注意在上面的代码中,我们设置了当前的 socket 为非阻塞的 server.setblocking(0),在 Web Server 中我们始终有一个 socket 是来监听的新的 socket 连接,因而这个监听 socket 要加入读事件队列:
inputs = [server]
除此之外我们还需要处理写事件,因而我们需要一个写列表用来处理需要写入数据的 socket:
outputs = []
message_queues = {}
我们开始我们的主循环:
while inputs:
readable, writable, exceptional = select.select(inputs, outputs, inputs)
select 函数返回三个文件描述符列表,分别代表有可读事件列表,有可写事件列表,有异常事件列表,返回的这些事件列表中包括了我们作为 select 参数的那些文件描述符。
6.3 读事件列表
读事件列表根据不同的情形有如下三种处理方式。
情形一
如果一个文件描述符可读,而且这个文件描述符恰巧就是我们刚才创建的 server,说明我们侦听的 socket 有新的连接请求:
for s in readable:
if s is server:
connection, client_address = s.accept()
connection.setblocking(0)
inputs.append(connection)
message_queues[connection] = Queue.Queue()
在上面的代码中,我们通过 accept 函数获得新的连接,并且把该连接加入 inputs 列表,等待连接到该 connection 的 client 发送数据,在此我们也给这个新的 connection 创建一个写消息的队列。
情形二
如果一个描述符不是侦听 socket 说明这个描述符有数据可读,此时我们可以使用 recv
来读取数据:
else:
data = s.recv(1024)
if data:
if s not in outputs:
outputs.append(s)
message_queues[s].put(data.capitalize())
情形三
如果读取的数据是空的,说明 client 关闭了这个连接,我们需要把这个连接从我们的监控列表中移除,并且释放对应的资源:
else:
if s in outputs:
outputs.remove(s)
inputs.remove(s)
s.close()
6.4 写事件列表
对于写事件列表来说,在这个例子中需要处理的事情比较有限,如果这个描述符有数据要发送,则通过 send
函数发送数据:
for s in writable:
try:
next_msg = message_queues[s].get_nowait()
except Queue.Empty:
if s in outputs:
outputs.remove(s)
else:
s.send(next_msg)
6.5 异常列表
最后是异常列表,说明描述符有错误发生,关闭它们并且释放资源:
for s in exceptional:
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
del message_queues[s]
6.6 select 的超时设置
select 的超时设置有以下三种情形:
情形一
一直等待下去,仅在有一个描述符准备好 IO 时返回,此时参数为 null
情形二
等待一段固定的时间,在有一个描述符准备好 IO 时返回,此时参数是大于 0 的
情形三
不等待,检查完描述符之后立即返回,此时参数是 0
6.7 select 的问题
select 是早期 linux 内核提供的多路复用技术,并且伴随着 linux 设计的限制,自身也有些不足:
- select 有最大监听描述符限制
- select 是通过逐个扫描的方式获取有事件的描述符
- 在多线程环境下,select 的表现不尽如人意
正因为 select 有不足才有了下一节我们要讲的另一个 IO 多路复用技术:poll