IO模型
一. IO模型简介:
I/O 指的是输入输出
IO的问题: 当我们要输入数据或是输出数据通常需要很长一段时间,当然是对于CPU而言
在等待输入的过程中,CPU就处于闲置状态 没事干! 造成了资源浪费.
注意: IO其实有很多类型,例如,socket网络IO,内存到内存的copy,等待键盘输入,对比起来socket网络IO需要等待的时间是最长的,这也是咱们重点关注的地方
IO模型解决的问题是提升CPU的利用率.
二.
涉及到的步骤
1.wait_data
2.copy_data
recv accept 需要经历 wait -> copy
三.
默认情况下 你写出TCP程序就是阻塞IO模型
该模型 提高效率方式,当你执行recv/accept 会进入wait_data的阶段,
1.你的进程会主动调用一个block指令,进程进入阻塞状态,同时让出CPU的执行权,操作系统就会将CPU分配给其它的任务,从而提高了CPU的利用率
2.当数据到达时,首先会从内核将数据copy到应用程序缓冲区,并且socket将唤醒处于自身的等待队列中的所有进程
之前使用多线程 多进程 完成的并发 其实都是阻塞IO模型 每个线程在执行recv时,也会卡住
四.非阻塞IO
非阻塞IO模型与阻塞模型相反 ,在调用recv/accept 时都不会阻塞当前线程
使用方法: 将原本阻塞的socket 设置为非阻塞
该模型在没有数据到达时,会跑出异常,我们需要捕获异常,然后继续不断地询问系统内核直到,数据到达为止
可以看出,该模型会大量的占用CPU资源做一些无效的循环, 效率低于阻塞IO
五.多路复用
多个socket使用同一套处理逻辑
如果将非阻塞IO 比喻是点餐的话,相当于你每次去前台,照着菜单挨个问个遍
多路复用,直接为前台那些菜做好了,前台会给你返回一个列表,里面就是已经做好的菜
对比阻塞或非阻塞模型,增加了一个select,来帮我们检测socket的状态,从而避免了我们自己检测socket带来的开销
select会把已经就绪的放入列表中,我们需要遍历列表,分别处理读写即可
案例:
import socket import time import select s = socket.socket() s.bind(("127.0.0.1",1688)) # 设置为非阻塞 模型 s.setblocking(True) #在多路复用中 阻塞与非阻塞没有区别 因为select会阻塞直到有数据到达为止 s.listen() # 待检测是否可读的列表 r_list = [s] # 待检测是否可写的列表 w_list = [] # 待发送的数据 msgs = {} print("开始检测了") while True: read_ables, write_ables, _= select.select(r_list,w_list,[]) print("检测出结果了!") # print(read_ables,"可以收数据了") # print(write_ables,"可以发数据了") # 处理可读 也就是接收数据的 for obj in read_ables: # 拿出所有可以读数据的socket #有可能是服务器 有可能是客户端 if s == obj: # 服务器 print("来了一个客户端 要连接") client,addr = s.accept() r_list.append(client) # 新的客户端也交给select检测了 else:# 如果是客户端则执行recv 接收数据 print("客户端发来一个数据") try: data = obj.recv(1024) if not data:raise ConnectionResetError print("有个客户端说:",data) # 将要发送数据的socket加入到列表中让select检测 w_list.append(obj) # 将要发送的数据已经socket对象丢到容器中 if obj in msgs: # 由于容器是一个列表 所以需要先判断是否已经存在了列表 msgs[obj].append(data) else: msgs[obj] = [data] except ConnectionResetError: obj.close() r_list.remove(obj) # 处理可写的 也就是send发送数据 for obj in write_ables: msg_list = msgs.get(obj) if msg_list: # 遍历发送所有数据 for m in msg_list: try: obj.send(m.upper()) except ConnectionResetError: obj.close() w_list.remove(obj) break # 数据从容器中删除 msgs.pop(obj) # 将这个socket从w_list中删除 w_list.remove(obj)
多路复用对比非阻塞 ,多路复用可以极大降低CPU的占用率
注意:多路复用并不完美 因为本质上多个任务之间是串行的,如果某个任务耗时较长将导致其他的任务不能立即执行,多路复用最大的优势就是高并发
六.
非阻塞IO不等于异步IO 因为从copy的过程是一个同步任务 会卡主当前线程
而异步IO 是发起任务后 就可以继续执行其它任务,copy到应用程序缓冲区,会给你的线程发送信号 或者执行回调
七.
别人的总结:
这是我们熟悉的IO模型,一个进程在作IO操作时,非要等到数据从内核空间拷贝到用户进程空间,才会返回。这个模型的优点就是简单,而且在阻塞的时候,CPU还可以进行调度,去执行别的进程。
非阻塞IO
一开始我看是非阻塞IO,觉得应该要比阻塞IO模型先进,可是当我一看使用方法的时候,就知道这个模型是不会被实际使用的,仅仅只能作为理论上存在的IO模型。这个模型的观点是:进行IO操作的时候,不阻塞,如果没有数据准备好,就直接返回错误码(或者是别的代码)。因此,使用者就只能不断进行轮询来调用IO函数。这样的后果就是,不仅在宏观上形成了与阻塞IO一共的“阻塞”效果,而且在微观上,CPU一直被用来轮询,造成了CPU的浪费。所以,这个模型还不如阻塞IO模型实用。
IO复用
对于IO复用,我的理解有三点:
在一次系统调用中,实现了询问多个描述符的IO准备情况 —— 根据事件通知
为了实现第一点,就需要把阻塞的地方进行转移。把一次系统调用,分为两次系统调用。第一次系统调用可以询问多个描述符的IO准备情况,在这个地方进行阻塞;而第二次系统调用,是针对已经准备好IO的描述符进行调用,此时,理论上(按照我的理解),也是会发生阻塞的,只不过是此时内核已经把数据准备好了,阻塞的时间可以忽略不计罢了。
本质上,还是阻塞的。
信号IO
我们都知道,信号是UNIX提供了进程间进行通信的一种方式。我们常用的 kill -9 命令(kill是向进程传递信号量,9只是众多信号中的一个代号),或者是 Ctrl + C 的时候,就是向某个进程发出终止的信号,这样进程就退出了。
而对于信号IO的模型,我是这么理解的:进程在发起IO操作,系统调用之后,直接访问,内核会在IO数据准备好之后,以某个信号通知发起IO操作的进程,从而使得该进程的信号处理函数可以读取IO数据的操作。
本质上,这也是阻塞的IO模型,因为在信号处理函数中,同样也是要进行阻塞的,只是在在这个时候发起系统系统,内核已经把数据准备好了。
异步IO
这是真正的异步IO了。实现的机制是:用户在发起异步IO的系统调用时,会把相应的数据处理函数作为回调函数,等到IO数据准备好,内核会主动调用此回调函数。可以看出,用户进程在这种模型下,只调用了一次系统调用,而且是立即返回的,因此,就不会出现让进程阻塞的情况,也就符合了POSIX中异步IO的定义。
其实我理解起来,思路是和信号IO差不多的,唯一不同的地方,对于IO数据的操作,异步IO是由内核主动发起的,而信号IO是由用户进程发起的。