• 5月2日 python学习总结 IO模型


     IO模型

    1、阻塞IO

    2、非阻塞IO

    3、多路复用IO

    4、异步IO

    一、阻塞IO

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

    实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。

    所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获    得结果或者超时出错时才返回。

          

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

    二、非阻塞IO(不推荐使用)一直不断的询问

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

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

     

    在非阻塞式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)

        非阻塞IO的精髓在于完全没有阻塞!!!

        优:

          能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。

          缺:     

          1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况 

                  2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

        此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。

    三、多路复用IO(IO multiplexing)  select帮它询问,集中处理

    r1,wl,x1=select.select(r_list,w_list,[],0.5)
        """
        rlist -- wait until ready for reading
        wlist -- wait until ready for writing
        xlist -- wait for an ``exceptional condition''
        If only one kind of condition is required, pass [] for the other lists.    
        """

        

    IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

        当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
        这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

        强调:

        1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

        2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

    结论: select的优势在于可以处理多个连接,不适用于单个连接  

      

    #服务端
    from socket import *
    import select
    server = socket(AF_INET, SOCK_STREAM)
    server.bind(('127.0.0.1',8093))
    server.listen(5)
    server.setblocking(False)
    print('starting...')
    
    rlist=[server,]
    wlist=[]
    wdata={}
    
    while True:
        rl,wl,xl=select.select(rlist,wlist,[],0.5)
        print(wl)
        for sock in rl:
            if sock == server:
                conn,addr=sock.accept()
                rlist.append(conn)
            else:
                try:
                    data=sock.recv(1024)
                    if not data:
                        sock.close()
                        rlist.remove(sock)
                        continue
                    wlist.append(sock)
                    wdata[sock]=data.upper()
                except Exception:
                    sock.close()
                    rlist.remove(sock)
    
        for sock in wl:
            sock.send(wdata[sock])
            wlist.remove(sock)
            wdata.pop(sock)

        

    select监听fd变化的过程分析:

    #用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到;
    #用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。

        

        优:

              相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。

              如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

        缺: 

         1、首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。

            很多操作系统提供了更为高效的接口,

         2、其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

    四、异步IO(Asynchronous I/O)    

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

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

     

    纯种的异步IO 把 ‘ 从内核拷贝数据到进程 ’ 这个任务交给了内核来做,进程只会被通知数据到达

     

    六、 IO模型比较分析

        到目前为止,已经将四个IO Model都介绍完了。现在回过头来回答最初的那几个问题:blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪。
        先回答最简单的这个:blocking vs non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

        再说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。Stevens给出的定义(其实是POSIX的定义)是这样子的:
        A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
        An asynchronous I/O operation does not cause the requesting process to be blocked; 
        两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,四个IO模型可以分为两大类,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类,而 asynchronous I/O后一类 。

        有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

        各个IO Model的比较如图所示:

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

  • 相关阅读:
    MySQL 数据库改名的简单方法
    Apache启用Gzip压缩
    安全的Web主机iptables防火墙脚本
    iptables
    Linux服务器维护统计连接数查看外部IP
    面试相关(新)
    hdu 4946 Area of Mushroom (凸包,去重点,水平排序,留共线点)
    凸包全解
    eclipse (android环境搭建)
    wins 软件安装
  • 原文地址:https://www.cnblogs.com/95lyj/p/8997990.html
Copyright © 2020-2023  润新知