• Python开发基础--- IO模型


    IO模型分类

    五种IO Model

        blocking IO      阻塞IO

        nonblocking IO     非阻塞IO

        IO multiplexing     IO多路复用

        signal driven IO    信号驱动IO

        asynchronous IO    异步IO

    signal driven IO(信号驱动IO)在实际中并不常用,所以只剩下四种IO Model。

    网络IO的两个过程

    对于一个network IO ,会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:

    •  等待数据准备 (Waiting for the data to be ready):等待系统接收数据
    •  将数据从内核拷贝到进程中 (Copying the data from the kernel to the process):进程从系统缓存中拿到数据

    同步IO:在这两个过程中有任意阶段出现阻塞状态。

      阻塞IO、非阻塞IO、IO多路复用都是同步IO

    异步IO:全程无阻塞的IO

       异步IO属于异步IO(真的没毛病)

    阻塞IO(Blocking IO)

    UDP包:当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

    blocking IO的特点就是在IO执行的两个阶段都被block了。

    示例:

    复制代码
     1 #服务端
     2 import socket
     3 sock=socket.socket()   #默认是TCP
     4 sock.bind(("127.0.0.1",8088))
     5 
     6 sock.listen(5)
     7 while True:
     8     conn,addr=sock.accept()   #默认是就是阻塞的方式,监听等待客户端连接(阶段一):等待中的阻塞
     9                               #客户端连接后接收数据(阶段二):socket对象和客户端地址,虽然接收数据的过程很快但是实际上也是阻塞
    10     while True:
    11         data=conn.recv(1024)    #也是两个阶段的阻塞
    12         print(data.decode('utf8'))
    13         if data.decode('utf8') =='q':
    14             break
    15         respnse=input('>>>>')
    16         conn.send(respnse.encode('utf8'))
    17 
    18 
    19 #客户端
    20 import socket
    21 sock=socket.socket()
    22 sock.connect(("127.0.0.1",8088))
    23 
    24 while True:
    25     data=input('>>>').strip()
    26     sock.send(data.encode('utf8'))
    27     s_data = sock.recv(1024)    #两个阶段的阻塞
    28     print(s_data.decode('utf8'))
    复制代码

    非阻塞IO(Non-blocking IO)

     

    当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。所以用户进程不需要等待,而是马上就得到了一个结果,用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。这个过程中,用户进程是需要不断的主动询问kernel数据好了没有。

    非阻塞实际上是将大的整片时间的阻塞分成N多的小的阻塞,每次recvform系统调用之间,可以干点别的事情,然后再发起recvform系统调用,重复的过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

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

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

    复制代码
     1 #服务端
     2 
     3 import socket
     4 import time
     5 sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #默认是TCP
     6 sock.bind(("127.0.0.1",8088))
     7 sock.listen(5)
     8 sock.setblocking(False)
     9 
    10 while True:
    11     try:
    12         print('server waiting')
    13         conn, addr = sock.accept()  # 默认是个阻塞的方式,等待客户端连接
    14         while True:
    15             data = conn.recv(1024)  #这边也是阻塞的IO
    16             print(data.decode('utf8'))
    17             if data.decode('utf8') == 'q':
    18                 break
    19             respnse = input('>>>>')
    20             conn.send(respnse.encode('utf8'))
    21     except Exception as e:
    22         print (e)
    23         time.sleep(4)
    24 
    25 #客户端
    26 import socket
    27 sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #默认是TCP
    28 
    29 while True:
    30     sock.connect(("127.0.0.1", 8088))   #因为服务端recv也是非阻塞,所以要不断重新连接
    31     data=input('>>>').strip()
    32     sock.send(data.encode('utf8'))
    33     s_data = sock.recv(1024)
    34     print(s_data.decode('utf8'))
    复制代码

    IO多路复用IO multiplexing

    IO多路复用,也叫做event driven IO,实现方式:select,poll或epoll

    IO多路复用的好处就在于单个process就可以同时处理多个网络连接的IO

    用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。这个过程中有两次system call(系统调用) select阻塞时候 和 recvfrom阻塞时候。用多路复用的的优势在于它可以同时处理大批量的connection,不适用单个或少量,少量还不如multi-threading + blocking IO。

    select示例:

    复制代码
     1 #服务端
     2 import socket
     3 import select
     4 sock=socket.socket()
     5 sock.bind(("127.0.0.1",8088))
     6 sock.listen(5)
     7 
     8 inp=[sock,] #定义监听的套接字对象列表,列表列表里可以有多个对象
     9 
    10 while True:
    11     #字段顺序:input list 、output list、error list、date(可以不写)
    12     r=select.select(inp,[],[],None) #对比的是sock.accept(),这一步只做了监听的事情,监听哪个socket对象活动,当没有客户端连接时候会阻塞
    13     # 当监听到有活动的socket对象时候,将返回值给r
    14     print('r',r)
    15     print('r',r[0])
    16     #r接收的返回是一个元组,r[0]是活动的对象列表
    17 
    18     for obj in r[0]:
    19         if obj == sock: #如果活动的对象是sock,那么将客户端对象加入监听列表,客户端再发数据时候,触发客户端的对象活动
    20             conn,addr=obj.accept()  #accept只做第二个阶段的事情,取回数据:client的socket对象和地址
    21             print(conn,addr)
    22             inp.append(conn)
    23         else:
    24             data=obj.recv(1024)
    25             print(data.decode('utf8'))
    26             resp=input('>>>')
    27             obj.send(resp.encode('utf8'))
    28 
    29 #客户端
    30 import socket
    31 sock=socket.socket()  
    32 sock.connect(("127.0.0.1", 8088))
    33 while True:
    34     data=input('>>>').strip()
    35     sock.send(data.encode('utf8'))
    36     s_data = sock.recv(1024)
    37     print(s_data.decode('utf8'))
    复制代码

    因为使用的是for循环,当多个客户端发消息给服务端,只能一个个顺序处理。

    在windows下只能用select实现多路复用

    在Linux可以使用select、poll、epoll实现,推荐使用epoll,对比:

      select和poll的监听方式为轮询方式,即每次都要循环一遍监听列表,效率低,另外select有连接数限制,poll无限

      epoll连接数无限,区别在于监听方式不同,每个socket对象绑定一个回调函数,当socket对象活动了就触发回调函数,把自己写到活动列表中,epoll直接调用活动列表

    信号驱动IO(signal driven IO)

    不常用,不做说明

    异步IO(Asynchronous I/O)

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

    IO模型区别

     

     selectors模块

     该模块能够按照系统平台,自动选择多路复用的方式。

    复制代码
     1 #服务端
     2 import selectors
     3 import socket
     4 
     5 sel=selectors.DefaultSelector()
     6 
     7 def accept(sock,mask):
     8     conn,addr=sock.accept()     #4、获取客户端的conn对象和地址
     9     print('accetped',conn,'from',addr)
    10     conn.setblocking(False)
    11     sel.register(conn,selectors.EVENT_READ,read)    #5、注册conn对象,将conn对象和函数read绑定
    12 
    13 def read(conn,mask):
    14     data=conn.recv(1024)    #9、服务端通过conn对象接收消息,进行下面的逻辑处理
    15     if data:
    16         print('echoing',repr(data),'to',conn)
    17         conn.send(data)
    18     else:
    19         print('closing',conn)
    20         sel.unregister(conn)
    21         conn.close()
    22 
    23 sock=socket.socket()
    24 sock.bind(('127.0.0.1',8088))
    25 sock.listen(100)
    26 sock.setblocking(False)
    27 sel.register(sock,selectors.EVENT_READ,accept)  #sock对象注册绑定accept函数
    28 
    29 while True:
    30     #不管是哪个方式,都是使用select方法监听活动的socket对象
    31     events=sel.select()     #1、执行sel阻塞监听,当有客户端连接,激活sock对象,返回一个存放活动sock对象相关信息的列表
    32                             #6、客户端通过conn对象发送消息,激活sel监听列表中的的conn对象,返回一个存放活动conn对象相关信息的列表
    33     print(events,type(events))
    34     for key,mask in events:
    35         print(mask)
    36         print(key.data)   #socket对象注册绑定的accept函数
    37         print(key.fileobj)
    38         callback=key.data   #2、取得返回的sock绑定的函数
    39                             #7、取得返回conn绑定的函数
    40         callback(key.fileobj,mask)  #3、key.fileobj是sock对象,执行函数
    41                                     #8、执行函数read,并传入conn对象
    42 
    43 
    44 #客户端
    45 import socket
    46 sock=socket.socket()
    47 sock.connect(("127.0.0.1", 8088))
    48 while True:
    49     data=input('>>>').strip()
    50     sock.send(data.encode('utf8'))
    51     s_data = sock.recv(1024)
    52     print(s_data.decode('utf8'))
    复制代码
  • 相关阅读:
    在ensp上配置Trunk接口
    在ensp上VLAN基础配置以及Access接口
    在ensp上的ARP及Proxy ARP
    在ensp上简单的配置交换机
    1000000 / 60S 的 RocketMQ 不停机,扩容,平滑升级!
    DE1-SOC 只要加载驱动VNC就断开(DE1-SOC 只要加载驱动串口就卡住)
    通过U盘拷贝文件到DE1-SOC 的 Linux系统
    Linux 系统响应来自 FPGA 端的中断的中断号到底怎么对应?(GIC控制器)
    HPS 访问 FPGA 方法之五—— 通过FPGA 中断访问
    HPS 访问 FPGA 方法之四—— 编写 Linux 字符设备驱动
  • 原文地址:https://www.cnblogs.com/chenqizhou/p/7359693.html
Copyright © 2020-2023  润新知