大前提:
# 前提需知道:操作系统的内核态和用户态 ,通信将数据缓存到内核,然后操作系统执行accept到用户态
# IO发生时涉及的对象:
# 对于一个network IO (这里我们以read举例),它会涉及到两个系统对象:
# 一个是调用这个IO的进程或者线程
# 另一个就是系统内核(kernel)
#
#
# IO发生时涉及的步骤:
# 当一个read操作发生时,它会经历两个阶段:
# 1 等待数据准备 (Waiting for the data to be ready)
# 2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
# 记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。
# 在linux中,默认情况下所有的socket都是blocking,即阻塞IO。
1、blocking IO (阻塞IO):
当用户进程调用了recvfrom这个系统调用,内核kernel就开始了IO的第一个阶段:准备数据。
对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),
这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。
当内核kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,
用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
简单总结:数据没来,一直等,没数据,复制也不行,一直等
2、non-blocking IO(非阻塞IO)
linux下,可以通过设置socket使其变为non-blocking。
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。
用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。
所以,用户进程其实是需要不断的主动询问kernel数据好了没有。进程在返回之后,可以干点别的事情,然后继续轮询。
注意,复制数据阶段仍然处于阻塞阶段。
3、IO multiplexing(IO多路复用)
IO multiplexing这个词可能有点陌生,但是如果我说select,epoll,大概就都能明白了。
有些地方也称这种IO方式为event driven IO。我们都知道,
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,
当某个socket有数据到达了,就通知用户进程
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,
当任何一个socket中的数据准备好了,select就会返回。
这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定
比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。
select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
注意1:select函数返回结果中如果有文件可读了,那么进程就可以通过调用accept()或recv()
来让kernel将位于内核中准备到的数据copy到用户区。
注意2: select的优势在于可以处理多个连接,不适用于单个连接
案例代码:
# IO多路复用server 端
import socket
import select
sock=socket.socket()
sock.bind(("127.0.0.1",8888))
sock.listen(5)
sock.setblocking(False)
inputs=[sock,]
while 1:
r,w,e=select.select(inputs,[],[])
for obj in r:
if obj == sock:
conn,addr=obj.accept()
print("conn",conn)
inputs.append(conn)
else:
data = obj.recv(1024)
print(data.decode("utf8"))
msg=input(">>>:")
obj.send(msg.encode("utf8"))
# IO多路复用client 端
import socket
sock=socket.socket()
sock.connect(("127.0.0.1",8888))
while 1:
msg=input(">>:")
sock.send(msg.encode("utf8"))
data=sock.recv(1024)
print(data.decode("utf8"))
sock.close()
4、AsynchronousI / O(异步IO):
用户进程发起read操作之后,立刻就可以开始去做其它的事。
而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,
所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,
当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
小总结:
# 阻塞IO:等数据,数据不来,不走,全程阻塞
# 一次accept,全程阻塞
# 非阻塞io:问你有没有数据,有带着数据走,没有,带着错误走
# socket.socket().setblocking(False)
# 缺点:发多次系统调用,拿到的数据不一定是实时的
# 优点:等待数据时无阻塞
# 两个阶段:
# 等待数据非阻塞
# 复制数据阻塞
# IO多路复用:(重点)
# conn没有新链接监听
# select监听有变化的套接字,有新链接
# select监听数据,数据到了告诉他,不到就阻塞
# 套接字对象(文件描述符):
# 是一个非零整数,不会变
# 收发数据的时候,对于接收端而言,数据先到内核空间,然后 copy
# 到用户空间,同时,内核空间数据清除。
# 2次调用,全程阻塞,select一次,accept一次
# 特点:
# 1全程阻塞(wait for data,copy)
# 2能监听多个文件描述符
# 3实现并发
# 异步IO:
# linux 下用的很少,实现复杂,用的少
# 有一个阻塞就是同步,
# 同步:阻塞io,非阻塞io,io多路复用
# 异步:异步io