python之基础IO模型

一、概述


 1.1 引入

  传统编程风格:按顺序执行代码块,它是线性的  开始-代码A---代码B--...--结束,为高效出现事件驱动

    def f():

      pass

     while 1:

      鼠标检测

      f()----会一直监听鼠标占用CPU,所以很占CPU,为减少CPU资源才用事件驱动

  事件驱动例如:

    <p onclick="func()">点我啊</p>-----点这个会触发某个事件函数,是js方式,js是事件驱动的方式,这步就是鼠标单击,

                    鼠标点一次前端内部就会有个队列来Put这个事件,来一个Put一个,处理就有线程用get,队列默认是先进先出

1.2 什么是IO?

  1.内存分为用户空间与内核空间:用户空间内存与内核内存是分开的,用户空间不能用内核的空间

  2.进程上下文切换

  3.进程阻塞:进程阻塞动作是进程本身发用的阻塞,如套接字accept,这个阻塞会把CPU释放,等IO数据过来才能开始获取CPU权限

        阻塞的状态是不占CPU的,CPU会交出去

  4.文件描述符:是一个非负整数,是一个内核表里维护的一个套接字表,所以处理套接字就是处理fd(文件描述符)

            import socket
            print(socket.socket())-----socket socket fd=220 family=AddrassFamily type=....(其中fd是文件描述符)
            用户空间                         内核空间
            socket.socket()                   内核与用户空间用fd来交互
                 |                            用户的进程会问内核空间的fd数据有没有准备好
                fd             

  5.过程描述

          客端数据------->用户内存--复制--内核内存(能使用网卡IO)   <>     (能使用网卡IO)内核内存->复制到--->用户内存--服务器 
          
          网络IO实例:一个网络IO涉及二个系统对象一个调用IO的process另一个就是系统内核,当发生read操作时,有二个过程
                       1.待数据准备(服等待客户端发数据)   2.将数据从内核拷到进程中(服拿到数据从内核态给用户态)

二、IO的四种模型


        1.阻塞IO:---有阻塞
          sk.accept()---这个就是阻塞IO
          服务端(1.通过方法bind listen accept发系统调用 
                  2.conn.recv就开始等待数据(阻塞) 
                  3.当服端拿到数据后从内核给用户态conn.send才行(这个过程也在阻塞,所以二个过程都阻塞,把进程阻塞住了,数据不来不向下走)
      
          
        2.非阻塞IO----有阻塞
           服端:(1.进程不断的发system call,有数据就拿没数据就继续向下走,一会再来问,这个过程它有CPU权限因它要看数据用到CPU--这个过程会有延时,
                     每次来问时不一定能看到,下次再来看到的是上次的就是延时,这个频率是人为可控的
                   2.当第n次拿到数据后,需要从内核复核到用户态(这个过程是让系统把内核数据给用户态这个是阻塞的,没有CPU权限))
                   
        3.IO多路复用(事件驱动IO或同步):--有阻塞,是事件驱动IO(select与epoll都是多路复用的机制,nginx就是用epoll实现的,nginx就有很多连接过来能监听多个客端的文件描述符,进程与内核通信就是靠文件描述符)
            服端:1.select方法或epoll方法发起系统调用,kernel等待数据,当数据来了后(阻塞了等待数据进程不能用CPU了),会通知这个调用,这时就有CPU再发系统调用
                    让把内核数据给用户内存
                  2.再次发系统调用让内核把数据复制给用户态(以上二种方法只有一次系统调用)(也阻塞了等待数据进程不能用CPU了)
                  优点:解决并发问题,select或epoll可以连接多个文件描述符来实现并发,以上二种方法不能连接多个对象,一个服务器可有多个套接字同时被监听,
                        不同端口被不同客户端来连并能响应,只有客户端来连时才会有对象返回,对象返回后才会有数据传来
                        (第一步返回对象,也同conn,addr=sk.accept()做系统调用客户端连后才会有对象返回,第二步有数据返回)
        4.异步IO:--完全没阻塞,实现很难
             服端:1.aio_read方法发起系统调用(发给内核,等待数据,内核拿到数据复核给用户,这时内核复制组用户后直接通知进程了,这二个过程中进程未阻塞)
                     整个过程没有阻塞,以三种内核不会主动通知,可自己去看

三、IO多路复用(同步IO)创建使用


        1.select方法:如何实现事件驱动的
          监听多个文件描述符时,循环监听所有的文件描述符fd,不断的循环问哪个fd的数据准备好就会把数据复制到用户空间,这个缺点fd很多时不断的循环问费时费资源,而且它最多只能监听1024个文件描述符
             
        2.poll方法:与select唯一的区别是,poll只解决了select的监听数据问题,能监听的更多但还是这样轮循。
        3.epoll方法:在poll与select的基础上解决了轮循问题,只要一个fd数据准备好,epoll就知道是谁直接把数据复制到用户空间就行,不需要一个个问是谁的数据好了
                     节省cpu的时间,这是多个fd,而对于每一个fd来说二段还是阻塞的
        实例非阻塞IO:
            server.py 
               import socket 
               sk=socket.socket()
               sk.bind(('127.0.0.1',8000))
               sk.listen(3)
               sk.setblocking(False)----1.问下面的系统调用有没有数据,这个是不断的去问
               while 1:
                 try:
                   conn,addr=sk.accept()---2.发系统调用,没有数据就报错,这个进程再发系统调用,这个是循环不断问
                   print(addr)
                   
                   data=conn.recv(1024)
                   print(data.decode('utf8'))
                   conn.sendall(data)
                   conn.close()
                except Exception as e:
                   print('没有数据',e)--上步报错就不断打印这个提示
                   time.sleep(3)
            client.py
               import socket 
               sk1=socket.socket()
               sk1.connect(('127.0.0.1',8000))
               
               while 1:
                   inp=input('>>>')--是unicon编码的
                   
                   sk1.sendall(hello.encode('utf8'))
                   data=sk1.recv(1024)
                   print(data.decode('utf8'))

        实例2:IO多路复用(通过select函数监听多个对象)----看视频
               server.py
               import socket
               import select
               sk1=socket.socket()
               sk1.bind(('127.0.0.1',8080))
               sk1.listen(3)
               
               sk2=socket.socket()
               sk2.bind(('127.0.0.1',8081))
               sk2.listen(3)
               while 1:---要循环监听
                   r,w,e=select.select([sk1,sk2],[],5)----监听socket对象,返回值有三个,如果有客户端来连就会有对象返回存在r里,r就是客户端对象与IP+端口,所以r就是sk1或sk2
            
                  for obj in r:
                     conn,addr=obj.accept()----哪个客户端连就返回哪个客户端的conn,r是服端的socket对象,conn是客端的socket对象所以二个fd是不同的,r调用accept才能打印出conn对象,conn不是r分出来的而是它调用accept方法得到的
                     conn.send(hello.encode('utf8'))          
               问题:当只有一个sk时,当客户端连上服后r=sk,当客户端第二次发数据时就进不了for了,因r没变,如何做到客户端数据变就向下走
                     select也监听conn时,当conn有变化时select就触发向下走,conn与sk都是对象,实例如下
               
               client.py
               import socket
               sk=socket.socket()
               sk.connect(('127.0.0.1',8080))
               while 1:
                   data = sk.recv(1024)
                   print(data.decode('utf8'))
                   inp=input('>>>')
                   sk.sendall(inp.encode('utf8'))

            为什么要能监听多个socket对象?
            IO多路复用的水平触发(某种状态一直触发,1或0),边缘触发(只有高低电平变化就触发,0变1或1变0),eg:select.select()---只要里面有数据就一直触发,所以select是水平触发,只有把数据用掉才不会触发
               而epoll二种触发方式都有,数据一直在或数据变化都触发
            r,w,e=select.select([sk1,sk2],5):监听5s,超时时间 
        
        实例:
            while 1:
                inputs,outputs,errors=selec.select([sk,conn],[],3)----监听后二种方式才能向下触发
                  当sk不变时不同客户端进来也会向下走,一个服可跟多少客户端对话

            对比阻塞IO与IO多路复用过程:
                阻塞IO二个阻塞过程:1.等待数据是send与recv来等的  2.复制数据到用户也是这二个发的
                IO多路复用:1.等待数据是select来等的  2.复制数据是accept来复制的,它做的是第二次系统调用,这里的数据是conn,只有select执行accept才能执行

  

原文地址:https://www.cnblogs.com/Dana-xiong/p/14318084.html