python之IO model

1、事件驱动模型

在介绍协程时,遇到IO操做就切换,但何时切换回来,怎么肯定IO操做结束?javascript

不少人可能会考虑使用“线程池”或“链接池”。“线程池”旨在减小建立和销毁线程的频率,其维持必定合理数量的线程,并让空闲的线程从新承担新的执行任务。“链接池”维持链接的缓存池,尽可能重用已有的链接、减小建立和关闭链接的频率。
这两种技术均可以很好的下降系统开销,都被普遍应用不少大型系统,如websphere、tomcat和各类数据库等。可是,“线程池”和“链接池”技术也只是在必定程度上缓解了频繁调用IO接口带来的资源占用。并且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。因此使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“链接池”或许能够缓解部分压力,可是不能解决全部问题。总之,多线程模型能够方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,能够用非阻塞接口来尝试解决这个问题。html

传统的编程是以下线性模式的:java

开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束linux

每个代码块里是完成各类各样事情的代码,但编程者知道代码块A,B,C,D...的执行顺序,惟一可以改变这个流程的是数据。输入不一样的数据,根据条件语句判断,流程或许就改成A--->C--->E...--->结束。每一次程序运行顺序或许都不一样,但它的控制流程是由输入数据和你编写的程序决定的。若是你知道这个程序当前的运行状态(包括输入数据和程序自己),那你就知道接下来甚至一直到结束它的运行流程。nginx

 对于事件驱动型程序模型,它的流程大体以下:web

开始--->初始化--->等待数据库

 与上面传统编程模式不一样,事件驱动程序在启动以后,就在那等待,等待什么呢?等待被事件触发。传统编程下也有“等待”的时候,好比在代码块D中,你定义了一个input(),须要用户输入数据。但这与下面的等待不一样,传统编程的“等待”,好比input(),你做为程序编写者是知道或者强制用户输入某个东西的,或许是数字,或许是文件名称,若是用户输入错误,你还须要提醒他,并请他从新输入。事件驱动程序的等待则是彻底不知道,也不强制用户输入或者干什么。只要某一事件发生,那程序就会作出相应的“反应”。这些事件包括:输入信息、鼠标、敲击键盘上某个键还有系统内部定时器触发。编程

1.事件驱动模型驱动

一般,咱们写服务器处理模型的程序时,有如下几种模型:windows

1)每收到一个请求,建立一个新的进程,来处理该请求; 
(2)每收到一个请求,建立一个新的线程,来处理该请求; 
(3)每收到一个请求,放入一个事件列表,让主进程经过非阻塞I/O方式来处理请求

第三种就是协程、事件驱动的方式,通常广泛认为第(3)种方式是大多数网络服务器采用的方式。数组

事件驱动模型

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>

<p onclick="f()">点击</p>


<script type="text/javascript">
    function f() {
          alert(6666)
    }
</script>
</body>

</html>
鼠标点击事件

一般有两种方式对鼠标点击事件进行编程:

建立一个线程循环检测是否有鼠标点击

      那么这个方式有如下几个缺点:

  1. CPU资源浪费,可能鼠标点击的频率很是小,可是扫描线程仍是会一直循环检测,这会形成不少的CPU资源浪费;若是扫描鼠标点击的接口是阻塞的呢?
  2. 若是是堵塞的,又会出现下面这样的问题,若是咱们不但要扫描鼠标点击,还要扫描键盘是否按下,因为扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
  3. 若是一个循环须要扫描的设备很是多,这又会引来响应时间的问题; 因此,该方式是很是很差的。
     

 就是事件驱动模型 

      目前大部分的UI编程都是事件驱动模型,如不少UI平台都会提供onClick()事件,这个事件就表明鼠标按下事件。事件驱动模型大致思路以下:

  1.  有一个事件(消息)队列;
  2. 鼠标按下时,往这个队列中增长一个点击事件(消息);
  3. 有个循环,不断从队列取出事件,根据不一样的事件,调用不一样的函数,如onClick()、onKeyDown()等;
  4. 事件(消息)通常都各自保存各自的处理函数指针,这样,每一个消息都有独立的处理函数;

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

在这里咱们能够经过回调函数确认IO操做完成切换回来

1.要理解事件驱动和程序,就须要与非事件驱动的程序进行比较。实际上,现代的程序大可能是事件驱动的,好比多线程的程序,确定是事件驱动的。早期则存在许多非事件驱动的程序,这样的程序,在须要等待某个条件触发时,会不断地检查这个条件,直到条件知足,这是很浪费cpu时间的。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,固然程序也可自行决定不释放cpu),当事件触发时被操做系统唤醒,这样就能更加有效地使用cpu.
2.再说什么是事件驱动的程序。一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照必定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。
3.事件驱动的程序,一定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件。
4.事件驱动的程序的行为,彻底受外部输入的事件控制,因此,事件驱动的系统中,存在大量这种程序,并以事件做为主要的通讯方式。
5.事件驱动的程序,还有一个最大的好处,就是能够按照必定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性每每被用于保证某些过程的原子化。
6.目前windows,linux,nucleus,vxworks都是事件驱动的,只有一些单片机多是非事件驱动的。
事件驱动注解

注意事件驱动的监听事件是由操做系统调用的cpu来完成的

2、IO多路复用

咱们能够认为,实现事件驱动,IO自动阻塞的切换就是多路复用。好比socketserver,多个客户端链接,单线程下实现并发效果,就叫多路复用。 (本文讨论的背景是Linux环境下的network IO。

在解释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资源的。

文件描述符fd

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

缓存 I/O

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

注意:为何数据必定要先到内核区,直接到用户内存不是更直接吗?

操做硬件系统只能是操做系统能够调用,因此要先到内核态。
缓存 I/O 的缺点: 

数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。

 本文介绍了五种 IO Model:

  •  blocking IO
  • nonblocking IO
  •  IO multiplexing
  • signal driven IO
  • asynchronous IO

因为signal driven IO(信号驱动IO)在实际中并不经常使用,因此我这只说起剩下的四种IO Model。
再说一下IO发生时涉及的对象和步骤。
      对于一个network IO (这里咱们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另外一个就是系统内核(kernel)。当一个read操做发生时,它会经历两个阶段:
  1 等待数据准备 (Waiting for the data to be ready)
  2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,由于这些IO Model的区别就是在两个阶段上各有不一样的状况。

1.blocking IO(阻塞IO)

在linux中,默认状况下全部的socket都是blocking,一个典型的读操做流程大概是这样:

 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来讲,不少时候数据在一开始尚未到达(好比,尚未收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。
因此,blocking IO的特色就是在IO执行的两个阶段都被block了。

这里本文用socket的示例代码

import  socket

sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen(5)

while 1:

    conn,addr = sk.accept()
    while 1:
        conn.send("hello client".encode('utf8'))
        data= conn.recv(1024)
        print(data.decode('utf8'))
server(blocking IO)
#blocking IO
import socket

sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while 1:
    data= sk.recv(1024)
    print(data.decode('utf8'))
    sk.send('hello sever'.encode('utf8'))
client

1.non-blocking IO(非阻塞IO)

linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:

 从图中能够看出,当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
因此,用户进程实际上是须要不断的主动询问kernel数据好了没有。

注意:

      在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不同,”非阻塞将大的整片时间的阻塞分红N多的小的阻塞, 因此进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是能够作其余事情的,

      也就是说非阻塞的recvform系统调用调用以后,进程并无被阻塞,内核立刻返回给进程,若是数据还没准备好,此时会返回一个error。进程在返回以后,能够干点别的事情,而后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程一般被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。须要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',8080))
sk.listen(5)
sk.setblocking(False)##########setblocking(False)
print ('waiting client connection .......')
while 1:
    try:
        conn,addr = sk.accept()
        client_msg = conn.recv(1024)
        print(client_msg.decode('utf8'))
        conn.close()
    except Exception as e:
        print(e)
        time.sleep(4)
server(non—blocking IO)
import socket,time
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while 1:
    sk.connect(('127.0.0.1', 8080))
    sk.send('hello server'.encode('utf8'))
    time.sleep(2)
    break
client

3.IO multiplexing(IO多路复用)

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

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。(多说一句。因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。)
在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

注意1:select函数返回结果中若是有文件可读了,那么进程就能够经过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。

注意2: select的优点在于能够处理多个链接,不适用于单个链接.

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)

    for i in r :
        conn,addr = i.accept()
        print(conn)
        print('hello')
        inp.append(conn)
    print(">>>>>>")
server
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'))
client

4.  Asynchronous I/O(异步IO)

linux下的asynchronous IO其实用得不多。先看一下它的流程:

 

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

 

 区别:

      到目前为止,已经将四个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阻塞。按照这个定义,以前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,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。

       注意:因为我们接下来要讲的select,poll,epoll都属于IO多路复用,而IO多路复用又属于同步的范畴,故,epoll只是一个伪异步而已。

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

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

5.select 、poll、 epoll IO多路复用介绍

首先列一下,select、poll、epoll三者的区别

  • select 
    select最先于1983年出如今4.2BSD中,它经过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程能够得到这些文件描述符从而进行后续的读写操做。 
    select目前几乎在全部的平台上支持 
      
    select的一个缺点在于单个进程可以监视的文件描述符的数量存在最大限制,在Linux上通常为1024,不过能够经过修改宏定义甚至从新编译内核的方式提高这一限制。 
      
    另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增加。同时,因为网络响应时间的延迟使得大量TCP链接处于非活跃状态,但调用select()会对全部socket进行一次线性扫描,因此这也浪费了必定的开销。
  • poll 
    它和select在本质上没有多大差异,可是poll没有最大文件描述符数量的限制。 
    通常也不用它,至关于过渡阶段
  • epoll 
    直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持 

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

    (了解)epoll能够同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,若是咱们没有采起行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,可是代码实现至关复杂。 
    另外一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。 

    因此市面上上见到的所谓的异步IO,好比nginx、Tornado、等,咱们叫它异步IO,其实是IO多路复用。

select与epoll

#首先咱们来定义流的概念,一个流能够是文件,socket,pipe等等能够进行I/O操做的内核对象。
# 无论是文件,仍是套接字,仍是管道,咱们均可以把他们看做流。
# 以后咱们来讨论I/O的操做,经过read,咱们能够从流中读入数据;经过write,咱们能够往流写入数据。如今假
# 定一个情形,咱们须要从流中读数据,可是流中尚未数据,(典型的例子为,客户端要从socket读如数据,可是
# 服务器尚未把数据传回来),这时候该怎么办?
# 阻塞。阻塞是个什么概念呢?好比某个时候你在等快递,可是你不知道快递何时过来,并且你没有别的事能够干
# (或者说接下来的事要等快递来了才能作);那么你能够去睡觉了,由于你知道快递把货送来时必定会给你打个电话
# (假定必定能叫醒你)。
# 非阻塞忙轮询。接着上面等快递的例子,若是用忙轮询的方法,那么你须要知道快递员的手机号,而后每分钟给他挂
# 个电话:“你到了没?”
# 很明显通常人不会用第二种作法,不只显很无脑,浪费话费不说,还占用了快递员大量的时间。
# 大部分程序也不会用第二种作法,由于第一种方法经济而简单,经济是指消耗不多的CPU时间,若是线程睡眠了,
# 就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。
#
# 为了了解阻塞是如何进行的,咱们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为
# 了减小频繁I/O操做而引发频繁的系统调用(你知道它很慢的),当你操做一个流时,更多的是以缓冲区为单位进
# 行操做,这是相对于用户空间而言。对于内核来讲,也须要缓冲区。
# 假设有一个管道,进程A为管道的写入方,B为管道的读出方。
# 假设一开始内核缓冲区是空的,B做为读出方,被阻塞着。而后首先A往管道写入,这时候内核缓冲区由空的状态变
# 到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
# 可是“缓冲区非空”事件通知B后,B却尚未读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写
# 入的数据会滞留在内核缓冲区中,若是内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候
# 会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,咱们把这个事件定义为“缓冲区满”。
# 假设后来B终于开始读数据了,因而内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你能够从
# 长眠中醒来了,继续写数据了,咱们把这个事件叫作“缓冲区非满”
# 也许事件Y1已经通知了A,可是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告
# 诉B,你须要阻塞了!,咱们把这个时间定为“缓冲区空”。
# 这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四
# 个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(若是不能理解“同步”是
# 什么概念,请学习操做系统的锁,信号量,条件变量等任务同步方面的相关知识)。
#
# 而后咱们来讲说阻塞I/O的缺点。可是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。若是想要同时处理多
# 个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
# 因而再来考虑非阻塞忙轮询的I/O方式,咱们发现咱们能够同时处理多个流了(把一个流从阻塞模式切换到非阻塞
# 模式再此不予讨论):
# while true {
# for i in stream[]; {
# if i has data
# read until unavailable
# }
# }
# 咱们只要不停的把全部流从头至尾问一遍,又从头开始。这样就能够处理多个流了,但这样的作法显然很差,由于
# 若是全部的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻
# 塞或者唤醒,而非阻塞模式下则把I/O事件交给其余对象(后文介绍的select以及epoll)处理甚至直接忽略。
#
# 为了不CPU空转,能够引进了一个代理(一开始有一位叫作select的代理,后来又有一位叫作poll的代理,不
# 过二者的本质是同样的)。这个代理比较厉害,能够同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻
# 塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,因而咱们的程序就会轮询一遍全部的流(因而咱们可
# 以把“忙”字去掉了)。代码长这样:
# while true {
# select(streams[])
# for i in streams[] {
# if i has data
# read until unavailable
# }
# }
# 因而,若是没有I/O事件产生,咱们的程序就会阻塞在select处。可是依然有个问题,咱们从select那里仅仅知
# 道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至所有),咱们只能无差异轮询全部流,
# 找出能读出数据,或者写入数据的流,对他们进行操做。
# 可是使用select,咱们有O(n)的无差异轮询复杂度,同时处理的流越多,每一次无差异轮询时间就越长。再次
# 说了这么多,终于能好好解释epoll了
# epoll能够理解为event poll,不一样于忙轮询和无差异轮询,epoll之会把哪一个流发生了怎样的I/O事件通知我
# 们。此时咱们对这些流的操做都是有意义的。
# 在讨论epoll的实现细节以前,先把epoll的相关操做列出:
# epoll_create 建立一个epoll对象,通常epollfd = epoll_create()
# epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增长/删除某一个流的某一个事件
# 好比
# epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
# epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
# epoll_wait(epollfd,...)等待直到注册的事件发生
# (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。
# 而epoll只关心缓冲区非满和缓冲区非空事件)。
# 一个epoll模式的代码大概的样子是:
# while true {
# active_stream[] = epoll_wait(epollfd)
# for i in active_stream[] {
# read or write till unavailable
# }
# }


# 举个例子:
#    select:
#          班里三十个同窗在考试,谁先作完想交卷都要经过按钮来活动,他按按钮做为老师的我桌子上的灯就会变红.
#          一旦灯变红,我(select)我就能够知道有人交卷了,可是我并不知道谁交的,因此,我必须跟个傻子似的轮询
#          地去问:嘿,是你要交卷吗?而后我就能够以这种效率极低地方式找到要交卷的学生,而后把它的卷子收上来.
#
#
#    epoll:
#         此次再有人按按钮,我这不光灯会亮,上面还会显示要交卷学生的名字.这样我就能够直接去对应学生那收卷就
#         好了.固然,同时能够有多人交卷.
View Code

IO多路复用的触发方式

# 在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别以下:
#
# 水平触发:若是文件描述符已经就绪能够非阻塞的执行IO操做了,此时会触发通知.容许在任意时刻重复检测IO的状态,
# 没有必要每次描述符就绪后尽量多的执行IO.select,poll就属于水平触发.
#
# 边缘触发:若是文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽量
# 多的执行IO操做,由于若是在一次通知中没有执行完IO那么就须要等到下一次新的IO活动到来才能获取到就绪的描述
# 符.信号驱动式IO就属于边缘触发.
#
# epoll既能够采用水平触发,也能够采用边缘触发.
#
# 你们可能还不能彻底了解这两种模式的区别,咱们能够举例说明:一个管道收到了1kb的数据,epoll会当即返回,此时
# 读了512字节数据,而后再次调用epoll.这时若是是水平触发的,epoll会当即返回,由于有数据准备好了.若是是边
# 缘触发的不会当即返回,由于此时虽然有数据可读可是已经触发了一次通知,在此次通知到如今尚未新的数据到来,
# 直到有新的数据到来epoll才会返回,此时老的数据和新的数据均可以读取到(固然是须要此次你尽量的多读取).


# 下面咱们还从电子的角度来解释一下:
# 
#     水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能获得通知.上面提到的只要
# 有数据可读(描述符就绪)那么水平触发的epoll就当即返回.
# 
#     边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即便有数据
# 可读,可是没有新的IO活动到来,epoll也不会当即返回.
水平触发与边沿触发

select是水平触发

示例:

1.server端并发聊天:

import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",8801))
sk.listen(5)
inputs=[sk,]
while True:
    r,w,e=select.select(inputs,[],[],5)
    print(len(r))

    for obj in r:
        if obj==sk:
            conn,add=obj.accept()
            print(conn)
            inputs.append(conn)
        else:
            data_byte=obj.recv(1024)
            print(str(data_byte,'utf8'))
            inp=input('回答%s号客户>>>'%inputs.index(obj))
            obj.sendall(bytes(inp,'utf8'))

    print('>>',r)
server
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',8801))

while True:
    inp=input(">>>>")
    sk.sendall(bytes(inp,"utf8"))
    data=sk.recv(1024)
    print(str(data,'utf8'))
client

2.select 模拟一个socket server,socket在非阻塞状况下才能实现IO多路复用的例子

# select 模拟一个socket server,注意socket必须在非阻塞状况下才能实现IO多路复用。
# 接下来经过例子了解select 是如何经过单进程实现同时处理多个非阻塞的socket链接的。
#server端


import select
import socket
import queue

server = socket.socket()
server.bind(('localhost',9000))
server.listen(1000)

server.setblocking(False)  # 设置成非阻塞模式,accept和recv都非阻塞
# 这里若是直接 server.accept() ,若是没有链接会报错,因此有数据才调他们
# BlockIOError:[WinError 10035] 没法当即完成一个非阻塞性套接字操做。
msg_dic = {}
inputs = [server,]  # 交给内核、select检测的列表。
# 必须有一个值,让select检测,不然报错提供无效参数。
# 没有其余链接以前,本身就是个socket,本身就是个链接,检测本身。活动了说明有连接
outputs = []  # 你往里面放什么,下一次就出来了

while True:
    readable, writeable, exceptional = select.select(inputs, outputs, inputs)  # 定义检测
    #新来链接                                        检测列表         异常(断开)
    # 异常的也是inputs是: 检测那些链接的存在异常
    print(readable,writeable,exceptional)
    for r in readable:
        if r is server:  # 有数据,表明来了一个新链接
            conn, addr = server.accept()
            print("来了个新链接",addr)
            inputs.append(conn)  # 把链接加到检测列表里,若是这个链接活动了,就说明数据来了
            # inputs = [server.conn] # 【conn】只返回活动的链接,但怎么肯定是谁活动了
            # 若是server活动,则来了新链接,conn活动则来数据
            msg_dic[conn] = queue.Queue()  # 初始化一个队列,后面存要返回给这个客户端的数据
        else:
            try :
                data = r.recv(1024)  # 注意这里是r,而不是conn,多个链接的状况
                print("收到数据",data)
                # r.send(data) # 不能直接发,若是客户端不收,数据就没了
                msg_dic[r].put(data)  # 往里面放数据
                outputs.append(r)  # 放入返回的链接队列里
            except ConnectionResetError as e:
                print("客户端断开了",r)
                if r in outputs:
                    outputs.remove(r) #清理已断开的链接
                inputs.remove(r) #清理已断开的链接
                del msg_dic[r] ##清理已断开的链接

    for w in writeable:  # 要返回给客户端的链接列表
        data_to_client = msg_dic[w].get()  # 在字典里取数据
        w.send(data_to_client)  # 返回给客户端
        outputs.remove(w)  # 删除这个数据,确保下次循环的时候不返回这个已经处理完的链接了。

    for e in exceptional:  # 若是链接断开,删除链接相关数据
        if e in outputs:
            outputs.remove(e)
        inputs.remove(e)
        del msg_dic[e]
server
import socket
client = socket.socket()

client.connect(('localhost', 9000))

while True:
    cmd = input('>>> ').strip()
    if len(cmd) == 0 : continue
    client.send(cmd.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode())

client.close()
client
相关文章
相关标签/搜索