一、IO模型介绍
二、阻塞IO(blocking IO)
三、非阻塞IO(non-blocking IO)
四、多路复用IO(IO multiplexing)
五、异步IO(Asynchronous I/O)
六 、IO模型比较
七、selectors模块
一、IO模型介绍
本文仅介绍四种IO模型
- blocking IO
- nonblocking IO
- IO multiplexing
- asynchronous IO
当IO发生时涉及的对象和步骤:对于一个network IO,它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:
# 1、等待数据准备 (Waiting for the data to be ready) # 2、将数据从内核拷贝到进程中(Copying the data from the kernel to the process) 各个阶段涉及的socket操作: #1、输入操作:read、readv、recv、recvfrom、recvmsg共5个函数,如果会阻塞状态,则会经理wait data和copy data两个阶段,如果设置为非阻塞则在wait 不到data时抛出异常 #2、输出操作:write、writev、send、sendto、sendmsg共5个函数,在发送缓冲区满了会阻塞在原地,如果设置为非阻塞,则会抛出异常 #3、接收外来链接:accept,与输入操作类似 #4、发起外出链接:connect,与输出操作类似
二、阻塞IO(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
解决方案:
- 多线程多进程
- 线程池、进程池
线程、进程均有上限,可能会面临大量访问需求,以上方法仅能解决访问较少的情况。
三、非阻塞IO(non-blocking IO)
Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。
也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
# 服务端 import socket import time server=socket.socket() server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) server.bind(('127.0.0.1',8083)) server.listen(5) server.setblocking(False) r_list=[] w_list={} while 1: try: conn,addr=server.accept() r_list.append(conn) except BlockingIOError: # 强调强调强调:!!!非阻塞IO的精髓在于完全没有阻塞!!! # time.sleep(0.5) # 打开该行注释纯属为了方便查看效果 print('在做其他的事情') print('rlist: ',len(r_list)) print('wlist: ',len(w_list)) # 遍历读列表,依次取出套接字读取内容 del_rlist=[] for conn in r_list: try: data=conn.recv(1024) if not data: conn.close() del_rlist.append(conn) continue w_list[conn]=data.upper() except BlockingIOError: # 没有收成功,则继续检索下一个套接字的接收 continue except ConnectionResetError: # 当前套接字出异常,则关闭,然后加入删除列表,等待被清除 conn.close() del_rlist.append(conn) # 遍历写列表,依次取出套接字发送内容 del_wlist=[] for conn,data in w_list.items(): try: conn.send(data) del_wlist.append(conn) except BlockingIOError: continue # 清理无用的套接字,无需再监听它们的IO操作 for conn in del_rlist: r_list.remove(conn) for conn in del_wlist: w_list.pop(conn) #客户端 import socket import os client=socket.socket() client.connect(('127.0.0.1',8083)) while 1: res=('%s hello' %os.getpid()).encode('utf-8') client.send(res) data=client.recv(1024) print(data.decode('utf-8')) 非阻塞IO示例