io模型发展史

1.前提理论

这里研究的io模型都是针对网络io的 , 因为现在开发的应用程序基本上都是基于网络的

"""
    Stevens在文章中一共比较了五种IO Model:
    * blocking IO           阻塞IO
    * nonblocking IO      非阻塞IO
    * IO multiplexing      IO多路复用
    * signal driven IO     信号驱动IO
    * asynchronous IO    异步IO
    由signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。
"""

#1)等待数据准备 (Waiting for the data to be ready)
#2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

同步异步
阻塞非阻塞
常见的网络阻塞状态 :
    accept
    recv
    recvfrom
    
send是主动触发的 , send的量很小 , 但是在内存copy到内存很快 
send虽然它也有io行为但是不在我们的考虑范围

image-20211017144540647

图解recv和send

image-20211017144940085

image-20211017145114033

2.阻塞IO(blocking IO)

我们之前写的都是阻塞io , 协程除外

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

image-20211017145810299

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。

而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

回顾tcp服务端

import socket

sock = socket.socket()
sock.bind(("127.0.0.1", 8081))
sock.listen(1)
print('等待客户端连接.....')
conn, addr = sock.accept()
while 1:
    msg = conn.recv(1024).decode()
    print(msg)
    if msg == "n": break
    conn.send(msg.upper().encode())
conn.close()
sock.close()

# 在服务端开设多进程或者多线程 进程池线程池 其实还是没有解决工问题
该等的地方还是得等没有规避
只不过多个人等待的彼此互不干扰

3.非阻塞IO(non-blocking IO)

Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

image-20211017150536115

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。

也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

代码实现

#服务端
from socket import *
import time
s=socket(AF_INET,SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
s.setblocking(False) #设置socket的接口为非阻塞
conn_l=[]
del_l=[]
while True:
    try:
        conn,addr=s.accept()
        conn_l.append(conn)
    except BlockingIOError:
        print(conn_l)
        for conn in conn_l:
            try:
                data=conn.recv(1024)
                if not data:
                    del_l.append(conn)
                    continue
                conn.send(data.upper())
            except BlockingIOError:
                pass
            except ConnectionResetError:
                del_l.append(conn)

        for conn in del_l:
            conn_l.remove(conn)
            conn.close()
        del_l=[]

#客户端
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'))

非阻塞IO实例

总结 :

"""
虽然非阻塞Io给你的感觉非常的牛逼
但是该模型会长时间占用CPU  并且不干活(一直在循环)   让CPU不停的空车
我们实际应用中也不会考虑使用非阻塞io模型

任何的技术点都有它存在的意
实际应用或者是思想借鉴
"""

4.IO多路复用(IO multiplexing)

image-20211017152744728

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

"""
当监管的对象只有一个的时候其实io多路复用连阻塞io都比比不上!!
但是io多路复用可以一次性监管很多个对象

监管机制是操作系统本身就有的如果你想要用该监管机制( select )
需要你导入对应的select模块

server = socket.socket()
conn , addr = server.accept()
"""
#服务端
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)

#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8081))

while True:
    msg=input('>>: ')
    if not msg:continue
    c.send(msg.encode('utf-8'))
    data=c.recv(1024)
    print(data.decode('utf-8'))

 select网络IO模型

总结

"""
监管机制其实有很多
select机制        windows   linux都有

poll机制              只在1inux有               poll 和 select都可以监管多个对象    但是po11监管的数量更多

上述 select和poll机制其实都不是很完美当监管的对象特别多的时候
可能会出现极其大的延时响应

epo11机制        只在1inux有
    它给每一个监管对象都绑定一个回调机制
    一旦有响应回调机制立刻发起提醒
    
针对不同的操作系统还需要考虑不同检測机制书写代码太多繁琐
有一个人能够根据你跑的平台的不同自动帮你选择对应的监管机制
selectors模块
"""

5.异步IO

Linux下的asynchronous IO其实用得不多,从内核2.6版本才开始引入。先看一下它的流程:

image-20211017154717046

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

"""
异步io模型是所有模型中效率最高的也是使用最广泛的  感觉上面三个是一个理论推导 , 思想迭代的过程 , 最后到了这个异步io , 由于异步io是让操作系统帮我们做一些操作 , 我们通过原生的python代码是无法实现的 需要用一门能够直接和操作系统打交道的语言(c语言)处理 , 好在有人帮你封装好了模块和框架

相关的模块和框架
模块: async模块
异步框架: sanic tronado twisted  (他们之所以快是因为内部使用的是异步io模型)
   速度快! ! ! , 快到甚至可以开发游戏服务端
"""

async模块在爬虫领域中经常使用 , 后面会详细介绍怎么使用

6.IO模型比较

image-20211017160057398

经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。异步io nb

原文地址:https://www.cnblogs.com/xcymn/p/15721381.html