Python多路复用IO模型:Select机制原理与实战解析
本文深入解析Python中的多路复用IO模型,重点阐述Select机制的工作原理。通过对比阻塞IO与非阻塞IO,说明Select如何在单进程中高效处理多个网络连接。文章包含Select函数的使用示例、底层fd监听机制分析,并探讨其局限性及Epoll等替代方案,帮助开发者理解高并发场景下的IO处理策略。
多路复用IO模型概述
多路复用IO(IO multiplexing)
这种IO方式属于事件驱动IO(event driven IO)。
Select与Epoll的核心优势在于,单个进程(process)即可同时处理多个网络连接的IO操作。其基本原理是:Select或Epoll函数会持续轮询其所负责的所有Socket连接,一旦某个Socket有数据到达,便通知用户进程。具体流程如图所示:

Select是多路复用的一种实现方式。
当用户进程调用Select时,整个进程会被阻塞。与此同时,内核(kernel)会“监视”所有由Select负责的Socket。当其中任何一个Socket的数据准备就绪时,Select函数返回。此时,用户进程再调用Read操作,将数据从内核拷贝到用户空间。该过程与阻塞IO的图示差异不大,甚至看似更差,因为这里涉及两个系统调用(Select和Recvfrom),而阻塞IO仅调用一个系统调用(Recvfrom)。然而,Select的优势在于它能够同时处理多个连接。
多路复用IO与阻塞IO模型的对比:
1. 阻塞IO经历两个阶段:等待数据(wait data)和拷贝数据(copy data)。
2. 多路复用涉及三个阶段:等待数据(wait data)、就绪后拷贝数据(ready copy data)和最终拷贝数据(copy data)。
对于单连接套接字通信,阻塞IO的效率更高。
多路复用IO中,Select可以代理多个套接字连接。在处理多个套接字通信时,多路复用IO的效率显著高于阻塞IO。
需要强调:
1. 如果处理的连接数不高,使用Select或Epoll的Web服务器性能未必优于多线程加阻塞IO的Web服务器,甚至可能延迟更大。Select/Epoll的优势并非处理单个连接更快,而是能处理更多的连接。
2. 在多路复用模型中,每个Socket通常设置为非阻塞(non-blocking)。但如图所示,整个用户进程实际上一直处于阻塞状态,只是被Select函数阻塞,而非被Socket IO阻塞。
结论:Select的优势在于能够处理多个连接,性能较高,并能同时检测多个套接字的IO行为,但不适用于单个连接的场景。
Select模型原理与代码实现
Select网络IO模型示例
Select用于检测多个套接字的IO行为,包括Accept和Recv。
IO行为主要分为两种:
1. 接收他人传来的数据。
2. 向他人发送数据。
Timeout参数指定超时时间。
系统每隔0.5秒询问操作系统数据是否已准备好。
def select(rlist, wlist, xlist, timeout=None):
pass
# [] 传的空列表是出异常的列表
# 返回值3个列表 收列表,发列表,异常列表
rl,wl,xl = select.select(rlist, wlist, [], 0.5)
客户端代码示例:
from socket import *
client = socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8000))
while True:
msg = input(">>>:").strip()
if not msg:continue
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print(data.decode("utf-8"))
client.close()
服务端代码示例:
from socket import *
import select
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8000))
server.listen(5)
# 设置socket接口为 非阻塞IO接口
# 默认是True 为阻塞
server.setblocking(False)
# 专门存着收消息套接字
rlist = [server,]
# 存放发送消息套接字
wlist = []
# 存放发送的数据
wdata = {}
while True:
# 返回值3个列表 收列表,发列表,异常列表
rl,wl,xl = select.select(rlist, wlist, [], 0.5)
print("rl",rl)
print("wl",wl)
for sock in rl:
if sock == server:
conn,addr = sock.accept()
rlist.append(conn)
else:
try:
data = sock.recv(1024)
if not data:
sock.close()
rlist.remove(sock)
continue
# 收的套接字加到列表
wlist.append(sock)
# 把数据加到字典 做一个 套接字对应数据
wdata[sock] = data.upper()
except Exception:
sock.close()
rlist.remove(sock)
# 发送数据
for sock in wl:
sock.send(wdata[sock])
wlist.remove(sock)
wdata.pop(sock)
server.close()
基于Select模块检测套接字IO行为,从而实现并发效果。
Select内部机制与优缺点分析
Select监听文件描述符(fd)变化的过程分析:
用户进程创建Socket对象,并将监听的fd拷贝到内核空间。每个fd对应一张系统文件表,当内核空间的fd响应到数据时,会向用户进程发送信号,提示数据已到。
随后,用户进程发送系统调用(如Accept),将内核空间的数据拷贝到用户空间。同时,作为接收端,内核空间的数据被清除,以便重新监听时新的数据能再次响应(发送端因基于TCP协议,需收到应答后才会清除数据)。
该模型的优点:
可以同时检测多个套接字,效率高于阻塞IO和非阻塞IO。
相比其他模型,使用Select的事件驱动模型仅需单线程(进程)执行,资源占用少,CPU消耗低,同时能为多客户端提供服务。
若试图建立简单的Event Driven服务器程序,该模型具有一定的参考价值。
该模型的缺点:
代理套接字列表中的多个套接字需要循环遍历,逐个检测。
在代理套接字较少的情况下,循环速度较快。但当Select代理的套接字数量非常大时,随着列表增大,效率会显著降低。
首先,Select接口并非实现“事件驱动”的最佳选择。因为当需要探测的句柄值较大时,Select接口本身需消耗大量时间轮询各个句柄。
许多操作系统提供了更高效的接口,例如Linux提供Epoll,BSD提供kQueue,Solaris提供/dev/poll等。若需实现更高效的服务器程序,推荐使用类似Epoll的接口。遗憾的是,不同操作系统提供的Epoll接口差异较大,因此实现具有良好跨平台能力的服务器较为困难。其次,该模型将事件探测和事件响应混合在一起,一旦事件响应的执行体庞大,将对整个模型造成灾难性影响。
Epoll与Select的对比
Epoll采用异步方式实现,提交套接字时,每个套接字绑定一个回调函数。当某个套接字就绪时,触发回调函数,将其索引放入单独列表。对于Select而言,只需从就绪列表中根据索引获取套接字,无需遍历整个列表。
Epoll不支持Windows系统。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









