阻塞IO
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。
- 而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存, 然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞
只有当该系统调用获得结果或者超时出错时才返回。- 实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
- 简单的解决办法
- 在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
- 该方案的问题是
- 开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态.
- 简单的解决办法
非阻塞IO
- 非阻塞的
recvform
系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error
。 - 进程在返回之后,可以干点别的事情,然后再发起
recvform
系统调用。重复上面的过程,循环往复的进行recvform
系统调用。这个过程通常被称之为轮询。 - 轮询检查操作系统内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。
- 需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。但是由于是本地操作,所以速度很快,根本察觉不到;
#服务端
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind((‘127.0.0.1‘,8099))
server.listen(5)
server.setblocking(False)
rlist=[]
wlist=[]
while True:
try:
conn, addr = server.accept()
rlist.append(conn)
print(rlist)
except BlockingIOError:
del_rlist=[]
for sock in rlist:
try:
data=sock.recv(1024)
if not data:
del_rlist.append(sock)
wlist.append((sock,data.upper()))
except BlockingIOError:
continue
except Exception:
sock.close()
del_rlist.append(sock)
del_wlist=[]
for item in wlist:
try:
sock = item[0]
data = item[1]
sock.send(data)
del_wlist.append(item)
except BlockingIOError:
pass
for item in del_wlist:
wlist.remove(item)
for sock in del_rlist:
rlist.remove(sock)
server.close()
#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect((‘127.0.0.1‘,8080))
while True:
msg=input(‘>>: ‘)
if not msg:continue
c.send(msg.encode(‘utf-8‘))
data=c.recv(1024)
print(data.decode(‘utf-8‘))
I0模型之多路复用
- 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
- 这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
- 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
# 服务端,可以实现同时服务多个客户端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind((‘127.0.0.1‘, 8800))
server.listen(5)
server.setblocking(False)
print(‘等待连接.....‘)
r_list = [server, ]
w_list = []
w_data = {}
while True:
rl, wl, el = select.select(r_list, w_list, [], 0.5)
# 调用select方法,开始监听socket, 主要是accept连接状态, conn的接收和发送数据
print(rl)
print(wl)
# 处理rl列表,等待和接收数据
for sock in rl:
if sock == server:
# 处理accept阻塞, 更新rl列表
conn, addr = sock.accept()
r_list.append(conn) # 将读取数据就绪状态的conn放入rl列表中
else:
# 处理recv阻塞, 更新wl列表,rl列表和wlist字典
try:
data = sock.recv(1024) # 当前conn接收到数据
if not data:
# 客户端连接断开, 首先关闭服务端的管道
sock.close()
# 从接收数据列表中删除conn对象
r_list.remove(sock)
continue
w_list.append(sock) # 将发送数据就绪的conn放入wl列表中
w_data[sock] = data.upper()
except Exception:
sock.close()
r_list.remove(sock)
# 处理wl列表,发送数据,更新wl列表和wdata字典
for sock in wl:
sock.send(w_data[sock])
w_list.remove(sock)
w_data.pop(sock)
# 客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect((‘127.0.0.1‘, 8800))
while True:
msg = input(‘>>> ‘).strip()
if not msg: continue
client.send(msg.encode(‘utf-8‘))
data = client.recv(1024)
print(data.decode(‘utf-8‘))
IO模型比较分析
blocking vs non-blocking
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
synchronous IO和asynchronous IO的区别
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes
An asynchronous I/O operation does not cause the requesting process to be blocked