深入解析多路复用IO:select/epoll原理与事件驱动IO的工作流程
IO复用(IO复用)

这种IO方式就是事件驱动IO(eventdrivenIO)。
我们都知道select/epoll的优点是单个进程可以同时处理多个网络连接的IO。它的基本原理是select/epoll函数会不断轮询它负责的所有socket。当数据到达某个套接字时,用户进程将收到通知。其流程如下:
select 是一种多路复用
当用户进程调用select时,整个进程会被阻塞,同时内核会“监视”所有负责select的socket。
当任意一个socket中的数据准备好后,select就会返回。这时用户进程调用read操作将数据从内核复制到用户进程。
这个图其实和阻塞IO图没有太大区别。事实上,情况甚至更糟。因为这里需要用到两个系统调用(select和recvfrom),
阻塞IO只调用一个系统调用\(recvfrom\)。不过,使用select的好处是可以同时处理多个连接。
多路复用 IO 与阻塞 IO 模型相比:
1.阻塞IO经历两个阶段:等待数据、复制数据
2.复用三个阶段:等待数据、准备复制数据、复制数据
单连接socket通信阻塞IO效率高
多路复用IO select可以代理多个socket连接,多路socket通信,多路复用IO效率高。
强调:
1、如果处理的连接数不是很高,使用select/epoll的Web服务器不一定比使用多线程+阻塞IO的Web服务器表现更好,延迟可能会更大。 select/epoll的优点不是可以更快地处理单个连接,而是可以处理更多的连接。
2.在复用模型中,每个socket一般都设置为非阻塞。然而,如上图所示,整个用户进程实际上一直处于阻塞状态。只不过进程是被select函数阻塞的,而不是被socket IO阻塞的。
结论:select的优点是可以处理多个连接,性能较高,可以同时检测多个socket IO行为。它不适合单个连接。
选择网络IO模型示例
select检测多个socket 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模块检测socket IO行为,达到并发效果
select监控fd变化的过程分析:
用户进程创建一个socket对象,并将监控的fd复制到内核空间。每个fd都会对应一个系统文件表。内核空间的fd响应数据后,
将向用户进程发送数据已到达的信号;
用户进程然后发送系统调用,如(accept)将内核空间中的数据复制到用户空间,同时作为接收数据端清除内核空间中的数据。
这样,重新监听时,如果有新的数据,fd就可以再次响应(发送方是基于TCP协议的,所以需要收到响应后才清除)。
该模型的优点:
它可以同时检测多个socket,比阻塞IO和非阻塞IO效率更高。
与其他模型相比,使用select()的事件驱动模型仅使用单个线程(进程)执行,占用资源较少,不会消耗太多CPU,并且可以同时为多个客户端提供服务。
如果你正在尝试构建一个简单的事件驱动服务器程序,这个模型有一定的参考价值。
该模型的缺点:
需要通过循环列表来一一检测代理的套接字列表中的多个套接字。
在代理套接字较少的情况下,循环速度更快。然而,当select代理的socket很多时,select的效率会随着列表的增加而变得越来越慢。
首先,select()接口并不是实现“事件驱动”的最佳选择。因为当需要检测的句柄值较大时,select()接口本身需要花费大量的时间轮询每个句柄。
很多操作系统都提供了更高效的接口,比如Linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,...
如果需要实现更高效的服务器程序,更推荐使用epoll这样的接口。不幸的是,不同操作系统提供的epoll接口有很大不同。
因此,使用类似于epoll的接口来实现具有良好跨平台能力的服务器会比较困难。
其次,该模型混合了事件检测和事件响应。一旦事件响应的执行体很大,对于整个模型来说将是灾难性的。
epoll是异步实现的。提交套接字时,每个套接字都会绑定一个回调函数。当套接字准备好时,会触发回调函数,并将其索引放置在单独的列表中。对于 select 来说,只需要根据准备好的列表中的索引来获取套接字即可,所以不需要遍历每个列表。
epoll不支持windows系统
以上就是本文的全部内容。希望对大家的学习有所帮助。也希望大家支持易速云。