python学习----IO模型

一.IO模型介绍

本文讨论的背景是Linux环境下的network IO. 本文最重要的参考文献是Richard Stevens的"UNIX® Network Programming Volume 1,python

Third Edition: The Sockets Networking ", 6.2节"I/O Models ",Stevens在这节中详细说明了各类IO的特色和区别.linux

Stevens在文章中一共比较了五种IO Model:程序员

blocking IO 阻塞IOweb

nonblocking IO 非阻塞IO数据库

IO multiplexing IO多路复用编程

signal driven IO 信号驱动IOwindows

asynchronous IO 异步IO数组

因为signal driven IO (信号驱动IO) 在实际中并不经常使用, 因此主要介绍其他四种IO Model缓存

再说一下IO发生时涉及的对象和步骤。对于一个network IO (这里咱们以read举例),它会涉及到两个系统对象,一个是调用这个IO的tomcat

process (or thread),另外一个就是系统内核(kernel)。当一个read操做发生时,该操做会经历两个阶段:

#1)等待数据准备 (Waiting for the data to be ready)
#2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

 补充:

#一、输入操做:read、readv、recv、recvfrom、recvmsg共5个函数,若是会阻塞状态,
则会经历wait data和copy data两个阶段,若是设置为非阻塞则在wait 不到data时抛出异常
#二、输出操做:write、writev、send、sendto、sendmsg共5个函数,在发送缓冲区满了会
阻塞在原地,若是设置为非阻塞,则会抛出异常
#三、接收外来连接:accept,与输入操做相似 #四、发起外出连接:connect,与输出操做相似

 回顾:

同步:提交一个任务以后要等待这个任务执行完毕

异步:只管提交任务,不等待这个任务执行完毕就能够去作其余的事情

阻塞:recv、recvfrom、accept,线程阶段  运行状态-->阻塞状态-->就绪

非阻塞:没有阻塞状态

二. 阻塞IO(blocking IO)

在Linux中, 默认状况下全部的socket都是blocking, 一个典型的读操做流程大概以下图:(recvfrom和tcp里面的recv在这些IO模型里面是同样的)

上面的图形分析:两个阶段的阻塞

  当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来讲,不少时候数据在一开始尚未到

达(好比,尚未收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。

  而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。

  因此,blocking IO的特色就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

  几乎全部的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的,使用这些接口能够很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的。以下图

  ps:所谓阻塞型接口是指系统调用(通常是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用得到结果或者超时出错时才返回.

实际上,除非特别指定,几乎全部的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被

阻塞,在此期间,线程将没法执行任何运算或响应任何的网络请求。

一个简单的解决方案:

#在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每一个链接都拥有独立的线程(或进程),这样任何一个链接的阻塞都不会影响其余的链接。

 

该方案的问题是:

#开启多进程或都线程的方式,在遇到要同时响应成百上千路的链接请求,则不管多线程仍是多进程都会严重占据系统资源,下降系统对外界响应效率,并且线程与进
程自己也更容易进入假死状态。

 

改进方案:

#不少程序员可能会考虑使用“线程池”或“链接池”。“线程池”旨在减小建立和销毁线程的频率,其维持必定合理数量的线程,并让空闲的线程从新承担新的执行任务。
“链接池”维持链接的缓存池,尽可能重用已有的链接、减小建立和关闭链接的频率。这两种技术均可以很好的下降系统开销,都被普遍应用不少大型系统,
如websphere、tomcat和各类数据库等。

 

改进后方案其实也存在着问题:

#“线程池”和“链接池”技术也只是在必定程度上缓解了频繁调用IO接口带来的资源占用。并且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外
界的响应并不比没有池的时候效果好多少。因此使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

 

对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“链接池”或许能够缓解部分压力,可是不能解决全部问题。总之,

多线程模型能够方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,能够用非阻塞接口来尝试解决这个问题。

三. 非阻塞IO(nonblocking IO)

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

  从图中能够看出,当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲 ,

它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而用户就能够在本次到下

次再发起read询问的时间间隔内作其余事情,或者直接再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就

将数据拷贝到了用户内存(这一阶段仍然是阻塞的),而后返回。

  也就是说非阻塞的recvform系统调用调用以后,进程并无被阻塞,内核立刻返回给进程,若是数据还没准备好,此时会返回一个error。进程在返回以后,可

以干点别的事情,而后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程一般被称之为轮询。轮询检查内核数据,直到数据

准备好,再拷贝数据到进程,进行数据处理。须要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

 因此,在非阻塞式IO中,用户进程实际上是须要不断的主动询问kernel数据准备好了没有。

# 服务端
import socket
import time

server=socket.socket()
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(('127.0.0.1',8083))
server.listen(5)

server.setblocking(False) #设置不阻塞
r_list=[]  #用来存储全部来请求server端的conn链接
w_list={}  #用来存储全部已经有了请求数据的conn的请求数据

while 1:
    try:
        conn,addr=server.accept() #不阻塞,会报错
        r_list.append(conn)  #为了将链接保存起来,否则下次循环的时候,上一次的链接就没有了
    except BlockingIOError:
        # 强调强调强调:!!!非阻塞IO的精髓在于彻底没有阻塞!!!
        # time.sleep(0.5) # 打开该行注释纯属为了方便查看效果
        print('在作其余的事情')
        print('rlist: ',len(r_list))
        print('wlist: ',len(w_list))


        # 遍历读列表,依次取出套接字读取内容
        del_rlist=[] #用来存储删除的conn链接
        for conn in r_list:
            try:
                data=conn.recv(1024) #不阻塞,会报错
                if not data: #当一个客户端暴力关闭的时候,会一直接收b'',别忘了判断一下数据
                    conn.close()
                    del_rlist.append(conn)
                    continue
                w_list[conn]=data.upper()
            except BlockingIOError: # 没有收成功,则继续检索下一个套接字的接收
                continue
            except ConnectionResetError: # 当前套接字出异常,则关闭,而后加入删除列表,等待被清除
                conn.close()
                del_rlist.append(conn)


        # 遍历写列表,依次取出套接字发送内容
        del_wlist=[]
        for conn,data in w_list.items():
            try:
                conn.send(data)
                del_wlist.append(conn)
            except BlockingIOError:
                continue


        # 清理无用的套接字,无需再监听它们的IO操做
        for conn in del_rlist:
            r_list.remove(conn)
        #del_rlist.clear() #清空列表中保存的已经删除的内容
        for conn in del_wlist:
            w_list.pop(conn)
        #del_wlist.clear()

#客户端
import socket
import os
import time
import threading
client=socket.socket()
client.connect(('127.0.0.1',8083))

while 1:
    res=('%s hello' %os.getpid()).encode('utf-8')
    client.send(res)
    data=client.recv(1024)

    print(data.decode('utf-8'))


##多线程的客户端请求版本
# def func():
#     sk = socket.socket()
#     sk.connect(('127.0.0.1',9000))
#     sk.send(b'hello')
#     time.sleep(1)
#     print(sk.recv(1024))
#     sk.close()
#
# for i in range(20):
#     threading.Thread(target=func).start()
非阻塞IO示例

 

虽然咱们上面的代码经过设置非阻塞,规避了IO操做,可是非阻塞IO模型毫不被推荐。

nonblocking IO模型的优势:

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

缺点:

#1. 循环调用recv()将大幅度推高CPU占用率;这也是咱们在代码中留一句time.sleep(2)的缘由,不然在低配主机下极容易出现卡机状况
#2. 任务完成的响应延迟增大了,由于每过一段时间才去轮询一次read操做,而任务可能在两次轮询之间的任意时间完成。这会致使总体数据吞吐量的下降。

  此外,在这个方案中recv()更多的是起到检测“操做是否完成”的做用,实际操做系统提供了更为高效的检测“操做是否完成“做用的接口,例如select()多路复用模式,

能够一次检测多个链接是否活跃。

四. 多路复用IO(IO multiplexing)

 IO multiplexing这种IO方式有些地方也被称为事件驱动IO(event driven IO).

基本原理: select/epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。

它的流程如图:

解释图:

  当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。

这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。

   这个图和blocking IO的图其实并无太大的不一样,事实上还更差一些。由于它不只阻塞了还多须要使用两个系统调用(select和recvfrom),而blocking IO只调用了

一个系统调用(recvfrom),当只有一个链接请求的时候,这个模型还不如阻塞IO效率高。可是,用select的优点在于它能够同时处理多个connection,而阻塞IO那里不能,

我无论阻塞不阻塞,你全部的链接包括recv等操做,我都帮你监听着(以什么形式监听的呢?先不要考虑,下面会讲的~~),其中任何一个有变更(有连接,有数据),

我就告诉你用户,那么你就能够去调用这个数据了,这就是他的NB之处。这个IO多路复用模型机制是操做系统帮咱们提供的,在windows上有这么个机制叫作select,

那么若是咱们想经过本身写代码来控制这个机制或者本身写这么个机制,咱们可使用python中的select模块来完成上面这一系列代理的行为。在一切皆文件的unix下,

这些能够接收数据的对象或者链接,都叫作文件描述符fd

强调:

1. 若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll

的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。

2. 在多路复用模型中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这

个函数block,而不是被socket IO给block。

python中的select模块:

import select

fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout])

参数: 可接受四个参数(前三个必须)
    rlist: wait until ready for reading  #等待读的对象,你须要监听的须要获取数据的对象列表
    wlist: wait until ready for writing  #等待写的对象,你须要写一些内容的时候,input等等,也就是说我会循环他看看是否有须要发送的消息,
   若是有我取出这个对象的消息并发送出去,通常用不到,这里咱们也给一个[]。
xlist: wait for an “exceptional condition” #等待异常的对象,一些额外的状况,通常用不到,可是必须传,那么咱们就给他一个[]。 timeout: 超时时间 当超时时间 = n(正整数)时,那么若是监听的句柄均无任何变化,则select会阻塞n秒,以后返回三个空列表,若是监听的句柄有变化,则直接执行。   返回值:三个列表与上面的三个参数列表是对应的   select方法用来监视文件描述符(当文件描述符条件不知足时,select会阻塞),当某个文件描述符状态改变后,会返回三个列表 1、当参数1 序列中的fd知足“可读”条件时,则获取发生变化的fd并添加到fd_r_list中 2、当参数2 序列中含有fd时,则将该序列中全部的fd添加到 fd_w_list中 3、当参数3 序列中的fd发生错误时,则将该发生错误的fd添加到 fd_e_list中 四、当超时时间为空,则select会一直阻塞,直到监听的句柄发生变化

 

结论: select的优点在于能够处理多个链接,不适用于单个链接  

#服务端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8093))
server.listen(5)
# 设置为非阻塞
server.setblocking(False)

# 初始化将服务端socket对象加入监听列表,后面还要动态添加一些conn链接对象,当accept的时候sk就有感应,当recv的时候conn就有动静
rlist=[server,]
rdata = {}  #存放客户端发送过来的消息

wlist=[]  #等待写对象
wdata={}  #存放要返回给客户端的消息

print('预备!监听!!!')
count = 0 #写着计数用的,为了看实验效果用的,没用
while True:
    # 开始 select 监听,对rlist中的服务端server进行监听,select函数阻塞进程,直到rlist中的套接字被触发(在此例中,套接字接收到客户端发来的握手信号,从而变得可读,知足select函数的“可读”条件),被触发的(有动静的)套接字(服务器套接字)返回给了rl这个返回值里面;
    rl,wl,xl=select.select(rlist,wlist,[],0.5)
    print('%s 次数>>'%(count),wl)
    count = count + 1
    # 对rl进行循环判断是否有客户端链接进来,当有客户端链接进来时select将触发
    for sock in rl:
        # 判断当前触发的是否是socket对象, 当触发的对象是socket对象时,说明有新客户端accept链接进来了
        if sock == server:
            # 接收客户端的链接, 获取客户端对象和客户端地址信息
            conn,addr=sock.accept()
            #把新的客户端链接加入到监听列表中,当客户端的链接有接收消息的时候,select将被触发,会知道这个链接有动静,有消息,那么返回给rl这个返回值列表里面。
            rlist.append(conn)
        else:
            # 因为客户端链接进来时socket接收客户端链接请求,将客户端链接加入到了监听列表中(rlist),客户端发送消息的时候这个链接将触发
            # 因此判断是不是客户端链接对象触发
            try:
                data=sock.recv(1024)
                #没有数据的时候,咱们将这个链接关闭掉,并从监听列表中移除
                if not data:
                    sock.close()
                    rlist.remove(sock)
                    continue
                print("received {0} from client {1}".format(data.decode(), sock))
                #将接受到的客户端的消息保存下来
                rdata[sock] = data.decode()

                #将客户端链接对象和这个对象接收到的消息加工成返回消息,并添加到wdata这个字典里面
                wdata[sock]=data.upper()
                #须要给这个客户端回复消息的时候,咱们将这个链接添加到wlist写监听列表中
                wlist.append(sock)
            #若是这个链接出错了,客户端暴力断开了(注意,我尚未接收他的消息,或者接收他的消息的过程当中出错了)
            except Exception:
                #关闭这个链接
                sock.close()
                #在监听列表中将他移除,由于无论什么缘由,它毕竟是断开了,不必再监听它了
                rlist.remove(sock)
    # 若是如今没有客户端请求链接,也没有客户端发送消息时,开始对发送消息列表进行处理,是否须要发送消息
    for sock in wl:
        sock.send(wdata[sock])
        wlist.remove(sock)
        wdata.pop(sock)

    # #将一次select监听列表中有接收数据的conn对象所接收到的消息打印一下
    # for k,v in rdata.items():
    #     print(k,'发来的消息是:',v)
    # #清空接收到的消息
    # rdata.clear()
代码示例server端
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8093))


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

client.close()
代码示例client端

 

select监听fd变化的过程分析:

#用户进程建立socket对象,拷贝监听的fd到内核空间,每个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到;
#用户进程再发送系统调用,好比(accept)将内核空间的数据copy到用户空间,同时做为接受数据端内核空间的数据清除,这样从新监听时fd再有新的数
据又能够响应到了(发送端由于基于TCP协议因此须要收到应答后才会清除)。

 

该模型的优势:

相比其余模型,使用select()的事件驱动模型只用单线程(进程)执行, 占用资源少, 不消耗太多CUP, 同时可以为多客户端提供服务. 

该模型的缺点:

首先select()接口并非实现"事件驱动"的最好选择. 由于当须要探测的句柄值较大时, select()接口自己须要消耗大量时间去轮询

各个句柄. 不少操做系统提供了更为高效的服务器程序, 相似epoll这样的接口更被推荐.如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。

若是须要实现更高效的服务器程序,相似epoll这样的接口更被推荐。遗憾的是不一样的操做系统特供的epoll接口有很大差别,因此使用相似于epoll的接口实现具

有较好跨平台能力的服务器会比较困难。其次,该模型将事件探测和事件响应夹杂在一块儿,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

 

select作得事情和第二阶段的阻塞没有关系,就是从内核态将数据拷贝到用户态的阻塞,始终帮你作得监听的工做,帮你节省了一些第一阶段阻塞的时间。

   IO多路复用的机制:

    select机制: Windows、Linux

    poll机制    : Linux    #和lselect监听机制同样,可是对监听列表里面的数量没有限制,select默认限制是1024个,可是他们两个都是操做系统轮询每个

    被监听的文件描述符(若是数量很大,其实效率不太好),看是否有可读操做。

    epoll机制  : Linux    #它的监听机制和上面两个不一样,他给每个监听的对象绑定了一个回调函数,你这个对象有消息,那么触发回调函数给用户,用户

    就进行系统调用来拷贝数据,并非轮询监听全部的被监听对象,这样的效率高不少。

五. 异步IO(asynchronous IO)

Linux下的asynchronous IO其实用得很少,从内核2.6版本才开始引入。先看一下它的流程:

用户进程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,

因此不会对用户进程产生任何block。而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,

告诉它read操做完成了.

因为python在copy数据这个阶段没有提供操纵操做系统的接口,因此用python无法实现这套异步IO机制,其余几个IO模型都没有解决

第二阶段的阻塞(用户态和内核态之间copy数据),可是C语言是能够实现的,由于C语言是最接近底层的.

六. IO模型比较分析

blocking IO 和nonblocking IO的区别:

调用blocking IO会一直block住对应的进程直到操做完成,而non-blocking IO在kernel还准备数据的状况下会马上返回。

synchronous IO和asynchronous IO的区别:

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阻塞。按照这个定义,四个IO模型能够分为两大类,以前所述的blocking IO,non-blocking IO,

IO multiplexing都属于synchronous IO这一类,而 asynchronous I/O后一类 。

各个IO model 的比较:

 

 通过上面的介绍,会发现non-blocking IO和asynchronous IO的区别仍是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,可是它仍然要求进程去主

动的check,而且当数据准备完成之后,也须要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则彻底不一样。它就像是用户进程将整个IO操做

交给了他人(kernel)完成,而后他人作完后发信号通知。在此期间,用户进程不须要去检查IO操做的状态,也不须要主动的去拷贝数据。

七. selectors模块

IO复用:为了解释这个名词,首先来理解下复用这个概念,复用也就是共用的意思,这样理解仍是有些抽象,为此,我们来理解下复用在通讯领域的使用,在通讯领域中为了充分利用网络链接的物理介质,每每在同一条网络链路上采用时分复用或频分复用的技术使其在同一链路上传输多路信号,到这里咱们就基本上理解了复用的含义,即公用某个“介质”来尽量多的作同一类(性质)的事,那IO复用的“介质”是什么呢?为此咱们首先来看看服务器编程的模型,客户端发来的请求服务端会产生一个进程来对其进行服务,每当来一个客户请求就产生一个进程来服务,然而进程不可能无限制的产生,所以为了解决大量客户端访问的问题,引入了IO复用技术,即:一个进程能够同时对多个客户请求进行服务。也就是说IO复用的“介质”是进程(准确的说复用的是select和poll,由于进程也是靠调用select和poll来实现的),复用一个进程(select和poll)来对多个IO进行服务,虽然客户端发来的IO是并发的可是IO所需的读写数据多数状况下是没有准备好的,所以就能够利用一个函数(select和poll)来监听IO所需的这些数据的状态,一旦IO有数据能够进行读写了,进程就来对这样的IO进行服务。

  

理解完IO复用后,咱们在来看下实现IO复用中的三个API(select、poll和epoll)的区别和联系

select,poll,epoll都是IO多路复用的机制,I/O多路复用就是经过一种机制,能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知应用程序进行相应的读写操做。但select,poll,epoll本质上都是同步I/O,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。三者的原型以下所示:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);



 1.select的第一个参数nfds为fdset集合中最大描述符值加1,fdset是一个位数组,其大小限制为__FD_SETSIZE(1024),位数组的每一位表明其对应的描述符是否须要被检查。第二三四参数表示须要关注读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出参数,可能会被内核修改用于标示哪些描述符上发生了关注的事件,因此每次调用select前都须要从新初始化fdset。timeout参数为超时时间,该结构会被内核修改,其值为超时剩余的时间。

 select的调用步骤以下:

(1)使用copy_from_user从用户空间拷贝fdset到内核空间

(2)注册回调函数__pollwait

(3)遍历全部fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据状况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工做就是把current(当前进程)挂到设备的等待队列中,不一样的设备有不一样的等待队列,对于tcp_poll 来讲,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不表明进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数 据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操做是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)若是遍历完全部的fd,尚未返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是 current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。若是超过必定的超时时间(schedule_timeout 指定),仍是没人唤醒,则调用select的进程会从新被唤醒得到CPU,进而从新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间。

总结下select的几大缺点:

(1)每次调用select,都须要把fd集合从用户态拷贝到内核态,这个开销在fd不少时会很大

(2)同时每次调用select都须要在内核遍历传递进来的全部fd,这个开销在fd不少时也很大

(3)select支持的文件描述符数量过小了,默认是1024

 

2.  poll与select不一样,经过一个pollfd数组向内核传递须要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只须要被初始化一次。

 poll的实现机制与select相似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,而后对pollfd中的每一个描述符进行poll,相比处理fdset来讲,poll效率更高。poll返回后,须要对pollfd中的每一个元素检查其revents值,来得指事件是否发生。

 

3.直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。epoll能够同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,若是咱们没有采起行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,可是代码实现至关复杂。epoll一样只告知那些就绪的文件描述符,并且当咱们调用epoll_wait()得到就绪文件描述符时,返回的不是实际的描述符,而是一个表明就绪描述符数量的值,你只须要去epoll指定的一个数组中依次取得相应数量的文件描述符便可,这里也使用了内存映射(mmap)技术,这样便完全省掉了这些文件描述符在系统调用时复制的开销。另外一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。

 

epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此以前,咱们先看一下epoll 和select和poll的调用接口上的不一样,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函 数,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一个epoll句柄;epoll_ctl是注 册要监听的事件类型;epoll_wait则是等待事件的产生。

  对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定 EPOLL_CTL_ADD),会把全部的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每一个fd在整个过程当中只会拷贝 一次。

  对于第二个缺点,epoll的解决方案不像select或poll同样每次都把current轮流加入fd对应的设备等待队列中,而只在 epoll_ctl时把current挂一遍(这一遍必不可少)并为每一个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调 函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工做实际上就是在这个就绪链表中查看有没有就绪的fd(利用 schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是相似的)。

  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大能够打开文件的数目,这个数字通常远大于2048,举个例子, 在1GB内存的机器上大约是10万左右,具体数目能够cat /proc/sys/fs/file-max察看,通常来讲这个数目和系统内存关系很大。

总结:

(1)select,poll实现须要本身不断轮询全部fd集合,直到设备就绪,期间可能要睡眠和唤醒屡次交替。而epoll其实也须要调用 epoll_wait不断轮询就绪链表,期间也可能屡次睡眠和唤醒交替,可是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,可是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就好了,这节省了大量的CPU时间,这就是回调机制带来的性能提高。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,而且要把current往设备等待队列中挂一次,而epoll只要 一次拷贝,并且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并非设备等待队列,只是一个epoll内 部定义的等待队列),这也能节省很多的开销。
select,poll,epoll

 这三种IO多路复用模型在不一样的平台有着不一样的支持,而epoll在windows下就不支持,好在咱们有selectors模块,帮咱们默认选择当前平台下最合适的,咱们只须要写监听

谁,而后怎么发送消息接收消息,可是具体怎么监听的,选择的是select仍是poll仍是epoll,这是selector帮咱们自动选择的。

#服务端
from socket import *
import selectors

sel=selectors.DefaultSelector()
def accept(server_fileobj,mask):
    conn,addr=server_fileobj.accept()
    sel.register(conn,selectors.EVENT_READ,read)

def read(conn,mask):
    try:
        data=conn.recv(1024)
        if not data:
            print('closing',conn)
            sel.unregister(conn)
            conn.close()
            return
        conn.send(data.upper()+b'_SB')
    except Exception:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()



server_fileobj=socket(AF_INET,SOCK_STREAM)
server_fileobj.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server_fileobj.bind(('127.0.0.1',8088))
server_fileobj.listen(5)
server_fileobj.setblocking(False) #设置socket的接口为非阻塞
sel.register(server_fileobj,selectors.EVENT_READ,accept) #至关于网select的读列表里append了一个文件句柄server_fileobj,而且绑定了一个回调函数accept

while True:
    events=sel.select() #检测全部的fileobj,是否有完成wait data的
    for sel_obj,mask in events:
        callback=sel_obj.data #callback=accpet
        callback(sel_obj.fileobj,mask) #accpet(server_fileobj,1)

#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8088))

while True:
    msg=input('>>: ')
    if not msg:continue
    c.send(msg.encode('utf-8'))
    data=c.recv(1024)
    print(data.decode('utf-8'))
selector代码示例
#!/usr/bin/env python  
import select  
import socket  

response = b''  

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
serversocket.bind(('0.0.0.0', 8080))  
serversocket.listen(1)  
# 由于socket默认是阻塞的,因此须要使用非阻塞(异步)模式。  
serversocket.setblocking(0)  

# 建立一个epoll对象  
epoll = select.epoll()  
# 在服务端socket上面注册对读event的关注。一个读event随时会触发服务端socket去接收一个socket链接  
epoll.register(serversocket.fileno(), select.EPOLLIN)  

try:  
    # 字典connections映射文件描述符(整数)到其相应的网络链接对象  
    connections = {}  
    requests = {}  
    responses = {}  
    while True:  
        # 查询epoll对象,看是否有任何关注的event被触发。参数“1”表示,咱们会等待1秒来看是否有event发生。  
        # 若是有任何咱们感兴趣的event发生在此次查询以前,这个查询就会带着这些event的列表当即返回  
        events = epoll.poll(1)  
        # event做为一个序列(fileno,event code)的元组返回。fileno是文件描述符的代名词,始终是一个整数。  
        for fileno, event in events:  
            # 若是是服务端产生event,表示有一个新的链接进来  
            if fileno == serversocket.fileno():  
                connection, address = serversocket.accept()  
                print('client connected:', address)  
                # 设置新的socket为非阻塞模式  
                connection.setblocking(0)  
                # 为新的socket注册对读(EPOLLIN)event的关注  
                epoll.register(connection.fileno(), select.EPOLLIN)  
                connections[connection.fileno()] = connection  
                # 初始化接收的数据  
                requests[connection.fileno()] = b''  

            # 若是发生一个读event,就读取从客户端发送过来的新数据  
            elif event & select.EPOLLIN:  
                print("------recvdata---------")  
                # 接收客户端发送过来的数据  
                requests[fileno] += connections[fileno].recv(1024)  
                # 若是客户端退出,关闭客户端链接,取消全部的读和写监听  
                if not requests[fileno]:  
                    connections[fileno].close()  
                    # 删除connections字典中的监听对象  
                    del connections[fileno]  
                    # 删除接收数据字典对应的句柄对象  
                    del requests[connections[fileno]]  
                    print(connections, requests)  
                    epoll.modify(fileno, 0)  
                else:  
                    # 一旦完成请求已收到,就注销对读event的关注,注册对写(EPOLLOUT)event的关注。写event发生的时候,会回复数据给客户端  
                    epoll.modify(fileno, select.EPOLLOUT)  
                    # 打印完整的请求,证实虽然与客户端的通讯是交错进行的,但数据能够做为一个总体来组装和处理  
                    print('-' * 40 + '\n' + requests[fileno].decode())  

            # 若是一个写event在一个客户端socket上面发生,它会接受新的数据以便发送到客户端  
            elif event & select.EPOLLOUT:  
                print("-------send data---------")  
                # 每次发送一部分响应数据,直到完整的响应数据都已经发送给操做系统等待传输给客户端  
                byteswritten = connections[fileno].send(requests[fileno])  
                requests[fileno] = requests[fileno][byteswritten:]  
                if len(requests[fileno]) == 0:  
                    # 一旦完整的响应数据发送完成,就再也不关注写event  
                    epoll.modify(fileno, select.EPOLLIN)  

            # HUP(挂起)event代表客户端socket已经断开(即关闭),因此服务端也须要关闭。  
            # 没有必要注册对HUP event的关注。在socket上面,它们老是会被epoll对象注册  
            elif event & select.EPOLLHUP:  
                print("end hup------")  
                # 注销对此socket链接的关注  
                epoll.unregister(fileno)  
                # 关闭socket链接  
                connections[fileno].close()  
                del connections[fileno]  
finally:  
    # 打开的socket链接不须要关闭,由于Python会在程序结束的时候关闭。这里显式关闭是一个好的代码习惯  
    epoll.unregister(serversocket.fileno())  
    epoll.close()  
    serversocket.close()  

---------------------

本文来自 richard1ybb 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/richard1ybb/article/details/74573200?utm_source=copy 
epoll实现代码的示例(能够不用看,做为了解)
相关文章
相关标签/搜索