python学习笔记-IO模型

一、事件驱动模型介绍

传统的编程是如下线性的:
开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束
事件驱动型程序模型:
开始--->初始化--->等待

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。
另外两种常见的编程范式是(单线程)同步以及多线程编程

二、IO模型

先要熟悉的几个概念

1.用户空间和内核空间
2.进程切换
3.进程的阻塞
4.文件描述符
5.缓存I/O

用户空间和内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。 
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。 
为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。 
针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换,这种切换是由操作系统来完成的。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。 
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:

  • 保存处理机上下文,包括程序计数器和其他寄存器。
  • 更新PCB信息。
  • 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
  • 选择另一个进程执行,并更新其PCB。
  • 更新内存管理的数据结构。
  • 恢复处理机上下文。 

注:总而言之就是很耗资源的


进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
文件描述符

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。 
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
缓存I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝 

三、五种IO模型

五种IO模型:

blocking IO
nonblocking IO
IO multiplexing
signal driven IO 不常用
asychronous IO

阻塞IO

非阻塞IO

例子1

#--服务端
import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',6667))
sk.listen(5)
sk.setblocking(False) #设置非阻塞
print('waiting client connection .......')
while True:
    try:
        connection,address = sk.accept()   # 进程主动轮询
        print("+++",address)
        client_messge = connection.recv(1024)
        print(str(client_messge,'utf8'))
        connection.close()
    except Exception as e:
        print (e)
        time.sleep(4)
#--客户端
import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:
    sk.connect(('127.0.0.1',6667))
    print("hello")
    sk.sendall(bytes("hello","utf8"))
    time.sleep(2)
    break

这种方式的两个不足之处:发了太多的系统调用,数据处理不及时

IO多路复用

elect poll epoll IO多路复用区别

select: win Linux unix mac通用的 缺点是监听的最大连接数1024
poll:linux 
epoll:linux

epoll是最好的一种实现方式。
没有最大文件描述符数量的限制。 
比如100个连接,有两个活跃了,epoll会告诉用户这两个两个活跃了,直接取就ok了,而select是循环一遍。

例子1,使用select

import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",9904))
sk.listen(5)

while True:
    r,w,e=select.select([sk,],[],[],5)#参数分别表示:input output errorput,5表示监听5秒钟。不设置就一直监听
    #使用select来监听sk. 有连接了对应sk有变化(通过文件描述符),
    for i in r:
        conn,add=i.accept()#注释这两行执行结果:每隔5秒输出hello 和>>>>>>
        print(conn)#客服端连接后,执行完。在下一次循环r也有值。因为select是水平触发
        #没注释时,通过accept接收到了用户态,循环时就监听到没数据。注释后,没接受,在内核态就一直有变化数据
        print("hello")
    print('>>>>>>')
#客户端
import socket
sk=socket.socket()
sk.connect(("127.0.0.1",9904))

while 1:
    inp=input(">>").strip()
    sk.send(inp.encode("utf8"))
    data=sk.recv(1024)
    print(data.decode("utf8"))

补充:触发方式

在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别如下:

水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态,
没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发.

边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能
多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述
符.信号驱动式IO就属于边缘触发.

epoll既可以采用水平触发,也可以采用边缘触发.

从电子的角度理解:

水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.(只要
有数据可读(描述符就绪)那么水平触发的epoll就立即返回)

边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.(即使有数据
可读,但是没有新的IO活动到来,epoll也不会立即返回.)

IO多路复用优势:

同时可以监听多个连接
实现并发,socketservice里就用到了select。

例子2,实现并发

#服务端
import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",9904))
sk.listen(5)
inp=[sk,]
while True:
    r,w,e=select.select(inp,[],[],5) #[sk,conn]能监听两个socket,一是本机的,而是连接的
    #有新的连接的时候,sk有变化,r里有sk。当收到客户端的消息时,r里有conn
    for obj in r:
        if obj==sk:
            conn,add=obj.accept()
            print(conn)
            inp.append(conn)
        else:
            data_byte=obj.recv(1024)
            print(str(data_byte,"utf8"))
            res = input('回答%s号客户>>>' %inp.index(obj))
            obj.sendall(bytes(res, 'utf8'))

    print('>>>>>>')
#客户端
import socket
sk=socket.socket()
sk.connect(("127.0.0.1",9904))

while 1:
    inp=input(">>").strip()
    sk.send(inp.encode("utf8"))
    data=sk.recv(1024)
    print(data.decode("utf8"))

例子3,使用selectors模块

#服务端
import selectors
import socket

sel = selectors.DefaultSelector()#根据操作系统判断支持的IO多路复用的方式,使用。

def accept(sock, mask):
    conn, addr = sock.accept()  # Should be ready
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)#conn与read进行绑定

def read(conn, mask):
    data = conn.recv(1000)  # Should be ready
    if data:
        print('echoing', repr(data), 'to', conn)
        conn.send(data)  # Hope it won't block
    else:
        print('closing', conn)#linux关闭客户端时data是空走else。win下要加异常处理。略
        sel.unregister(conn) #解除
        conn.close()

sock = socket.socket()
sock.bind(('localhost', 9904))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept) #绑定:文件描述符和accept绑定
#sock只要有活动,直接去调用accept方法
while True:
    events = sel.select()#监听:
    for key, mask in events:
        callback = key.data  #函数名字,accept (read)
        callback(key.fileobj, mask)#key.fileobj 就是socket对象 (conn)

#客户端
import socket
sk=socket.socket()
sk.connect(("127.0.0.1",9904))

while 1:
    inp=input(">>").strip()
    sk.send(inp.encode("utf8"))
    data=sk.recv(1024)
    print(data.decode("utf8"))

异步IO

异步最大的特点 全程无阻塞
blocking 阻塞,nonblocking 非阻塞,IO multiplexing IO多路复用 都是同步

原文地址:https://www.cnblogs.com/steven223-z/p/12780667.html