zoukankan      html  css  js  c++  java
  • 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多路复用 都是同步

  • 相关阅读:
    Centos7下安装pip
    Docker进入容器后使用ifconfig等命令“command not found”解决办法
    安装包安装npm
    grafna与饼状图
    Postgresql导出数据报版本不对
    添加动物欢迎语
    zabbix性能优化记
    CPU使用情况之平均负载
    centos7以rpm方法装mysql5.7及大坑
    光速搭lvs + keepalived + nginx
  • 原文地址:https://www.cnblogs.com/steven223-z/p/12780667.html
Copyright © 2011-2022 走看看