IO模型
一、IO模型简介
'''
我们这里研究的IO模型都是针对网络IO的
IO 模型:
多路复用IO(IO multiplexing)
异步IO(asynchronous IO)
阻塞IO(blocking IO)
非阻塞IO(non-blocking IO)
信号驱动IO(ignal driven IO)
信号驱动IO在实际中并不常用,所以主要介绍其余四种IO 模型。
'''
IO发生时涉及的对象和步骤,以网络IO的读取操作举例,它一般会涉及两个系统对象:
1.调用这个IO的进程或线程(Process/thread)
2.系统内核(kernel)
会经历以下两个阶段:
1.等待数据准备 (Waiting for the data to be ready)
2.将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
其他IO模型间的区别就是在以上两个阶段上有所区别
'''
常见的网络IO操作:
1、输入操作:read、readv、recv、recvfrom、recvmsg共5个函数,如果会阻塞状态,则会经历wait data和copy data两个阶段,如果设置为非阻塞则在wait 不到data时抛出异常
2、输出操作:write、writev、send、sendto、sendmsg共5个函数,在发送缓冲区满了会阻塞在原地,如果设置为非阻塞,则会抛出异常
3、接收外来链接:accept,与输入操作类似
4、发起外出链接:connect,与输出操作类似
'''
1、阻塞IO(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被阻塞
1.1 socket模块中各种接口的阻塞
在我们学习socket模块时,所用到的recv()、send()等方法调用的接口大多都是阻塞型的
ps:所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。
实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
1.2 简单规避IO的方法
使用多进程、多线程、进程池、线程池、协程等操作,在执行IO时将CPU分配给其他任务使用,从而在侧面上减少了IO操作所浪费的时间影响
#在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
缺点:
#开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。
改进方案
#很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。
缺点:
#“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
2、非阻塞IO(non-blocking IO)
Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
对于数据拷贝操作,依然是处于阻塞阶段
在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
非阻塞IO模型绝不被推荐,缺点如下:
#1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
#2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。
2.1 非阻塞IO代码实例
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1',8000))
server.listen(5)
server.setblocking(False)
r_list = []
del_list = []
while True:
try:
a, b = server.accept()
r_list.append(a)
except BlockingIOError as e:
print('未获取到有效连接')
for i in r_list:
try:
data = i.recv(1024)
if len(data) == 0:
i.close()
del_list.append(i)
continue
i.send(data.upper())
except BlockingIOError:
continue
except ConnectionResetError:
i.close()
del_list.append(i)
for i in del_list:
r_list.remove(i)
del_list.clear()
2.2 setblocking()
设置socket为非阻塞
- 语法:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setblocking(bool)
'''
参数介绍:
bool:填入一个bool类型的值,表示是否开启阻塞IO
默认值:True,开启阻塞
可以设置为False,即非阻塞
'''
3、多路复用IO(multiplexing IO)
强调:
1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
结论: select的优势在于可以处理多个连接,不适用于单个连接
'''
1.当监管的对象只有一个的时候,其实IO多路复用连阻塞IO、非阻塞IO的效率都比不上!
2.但是IO多路复用可以一次性监管很多个对象,在对待批量对象时,多路复用IO的效率将超过阻塞IO和非阻塞IO
3.多路复用IO的监管机制是操作系统本身就有的,如果你想要使用该监管机制(select)
select需要导入对应的select模块
'''
总结:
'''
监管机制其实有很多:
select机制:windows、linux中都有
poll机制:只有在linux中,poll和select都可以监管多个对象,但是poll监管的数量更多
上述select和poll机制其实都不是很完美,当监管的对象特别多的时候可能会出现极其大的延迟响应
epoll机制:只在linux有
他给每一个监管对象都绑定了一个回调机制
一旦有响应,回调机制立刻发起提醒
针对不同的操作系统还需要考虑不通过监测机制,书写代码太过繁琐
selectors模块可以根据代码所在平台的不同自动选择对应的监管机制
'''
3.1 IO多路复用代码实例
from socket import *
import select
s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('127.0.0.1',8081))
s.listen(5)
s.setblocking(False) #设置socket的接口为非阻塞
read_l=[s,]
while True:
r_l,w_l,x_l=select.select(read_l,[],[])
print(r_l)
for ready_obj in r_l:
if ready_obj == s:
conn,addr=ready_obj.accept() #此时的ready_obj等于s
read_l.append(conn)
else:
try:
data=ready_obj.recv(1024) #此时的ready_obj等于conn
if not data:
ready_obj.close()
read_l.remove(ready_obj)
continue
ready_obj.send(data.upper())
except ConnectionResetError:
ready_obj.close()
read_l.remove(ready_obj)
3.2 select模块
select模块负责监听可执行对象,如果该对象被触发,则代表有信息交互,则返回该可执行对象
- 语法:
import select
s = select.select(r_list, w_list, x_list)
'''
参数介绍:我们只需要用到r_list即可
r_list:是一个列表的数据类型,该列表中填入我们需要监测的对象,例如socket对象,conn对象
返回值:
select.select的返回值是一个元组(x, y, z)
x:是一个列表数据类型,其中存放的就是r_list中我们放入的可执行对象
y:对应w_list
z:对应x_list
'''
select监听fd变化的过程分析:
#用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到;
#用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。
该模型的优点:
#相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
该模型的缺点:
#首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。#很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。#如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,#所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。
#其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
4、异步IO(Asynchronous IO)
Linux下的asynchronous IO其实用得不多,从内核2.6版本才开始引入。先看一下它的流程:
'''
异步IO模型是所有模型中效率最高的,也是应用最广泛的
相关的模块和框架
模块:asyncio模块
异步框架:sanic、tronado、twisted
速度快!!!
'''
二、IO模型比较