html5网站制作实战,贝锐免费域名,网站安全建设目的是,品牌官方网站建设一、IO模型介绍 同步(synchronous) IO和异步(asynchronous) IO#xff0c;阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么#xff0c;到底有什么区别#xff1f;这个问题其实不同的人给出的答案都可能不同#xff0c;比如wiki#xff0c;就认为asynchronous IO和…一、IO模型介绍 同步(synchronous) IO和异步(asynchronous) IO阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么到底有什么区别这个问题其实不同的人给出的答案都可能不同比如wiki就认为asynchronous IO和non-blocking IO是一个东西。这其实是因为不同的人的知识背景不同并且在讨论这个问题的时候上下文(context)也不相同。所以为了更好的回答这个问题我先限定一下本文的上下文。本文讨论的背景是Linux环境下的network IO。本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”6.2节“I/O Models ”Stevens在这节中详细说明了各种IO的特点和区别如果英文够好的话推荐直接阅读。Stevens的文风是有名的深入浅出所以不用担心看不懂。本文中的流程图也是截取自参考文献。Stevens在文章中一共比较了五种IO Model* blocking IO 阻塞IO* nonblocking IO 非阻塞IO* IO multiplexing IO多路复用* signal driven IO 信号驱动IO(不常见不讲)* asynchronous IO 异步IO由signal driven IO(信号驱动IO)在实际中并不常用所以主要介绍其余四种IO Model。 再说一下IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read、recv举例)它会涉及到两个系统对象一个是调用这个IO的process (or thread)另一个就是系统内核(kernel)。当一个read/recv读数据的操作发生时该操作会经历两个阶段1)等待数据准备 (Waiting for the data to be ready)2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)记住这两点很重要因为这些IO模型的区别就是在两个阶段上各有不同的情况。补充#1、输入操作read、readv、recv、recvfrom、recvmsg共5个函数如果会阻塞状态则会经理wait data和copy data两个阶段如果设置为非阻塞则在wait 不到data时抛出异常#2、输出操作write、writev、send、sendto、sendmsg共5个函数在发送缓冲区满了会阻塞在原地如果设置为非阻塞则会抛出异常#3、接收外来链接accept与输入操作类似#4、发起外出链接connect与输出操作类似二、阻塞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了。这里我们回顾一下同步/异步/阻塞/非阻塞同步提交一个任务之后要等待这个任务执行完毕异步只管提交任务不等待这个任务执行完毕就可以去做其他的事情阻塞recv、recvfrom、accept线程阶段 运行状态--阻塞状态--就绪非阻塞没有阻塞状态在一个线程的IO模型中我们recv的地方阻塞我们就开启多线程但是不管你开启多少个线程这个recv的时间是不是没有被规避掉不管是多线程还是多进程都没有规避掉这个IO时间。 几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的使用这些接口可以很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的。如下图ps所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞只有当该系统调用获得结果或者超时出错时才返回。实际上除非特别指定几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题如在调用recv(1024)的同时线程将被阻塞在此期间线程将无法执行任何运算或响应任何的网络请求。一个简单的解决方案在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程)这样任何一个连接的阻塞都不会影响其他的连接。该方案的问题是开启多进程或都线程的方式在遇到要同时响应成百上千路的连接请求则无论多线程还是多进程都会严重占据系统资源降低系统对外界响应效率而且线程与进程本身也更容易进入假死状态。改进方案很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率其维持一定合理数量的线程并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销都被广泛应用很多大型系统如websphere、tomcat和各种数据库等。改进后方案其实也存在着问题“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且所谓“池”始终有其上限当请求大大超过上限时“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模并根据响应规模调整“池”的大小。对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求“线程池”或“连接池”或许可以缓解部分压力但是不能解决所有问题。总之多线程模型可以方便高效的解决小规模的服务请求但面对大规模的服务请求多线程模型也会遇到瓶颈可以用非阻塞接口来尝试解决这个问题。三、非阻塞IO(non-Blocking 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数据准备好了没有。非阻塞IO示例服务端# 服务端import socketimport timeserversocket.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,addrserver.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:dataconn.recv(1024) #不阻塞会报错if not data: #当一个客户端暴力关闭的时候会一直接收b别忘了判断一下数据conn.close()del_rlist.append(conn)continuew_list[conn]data.upper()except BlockingIOError: # 没有收成功则继续检索下一个套接字的接收continueexcept 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 socketimport osimport timeimport threadingclientsocket.socket()client.connect((127.0.0.1,8083))while 1:res(%s hello %os.getpid()).encode(utf-8)client.send(res)dataclient.recv(1024)print(data.decode(utf-8))##多线程的客户端请求版本# def func():# sk socket.socket()# sk.connect((127.0.0.1,9000))# sk.send(bhello)# time.sleep(1)# print(sk.recv(1024))# sk.close()## for i in range(20):# threading.Thread(targetfunc).start()虽然我们上面的代码通过设置非阻塞规避了IO操作但是非阻塞IO模型绝不被推荐。我们不能否定其优点能够在等待任务完成的时间里干其他活了(包括提交其他任务也就是 “后台” 可以有多个任务在“”同时“”执行)。但是也难掩其缺点#1. 循环调用recv()将大幅度推高CPU占用率这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况#2. 任务完成的响应延迟增大了因为每过一段时间才去轮询一次read操作而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。此外在这个方案中recv()更多的是起到检测“操作是否完成”的作用实际操作系统提供了更为高效的检测“操作是否完成“作用的接口例如select()多路复用模式可以一次检测多个连接是否活跃。四、多路复用IO(IO multiplexing)(重点) IO multiplexing这个词可能有点陌生但是如果我说select/epoll大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket当某个socket有数据到达了就通知用户进程。它的流程如图先看解释图里面的select就像个代理。 用户进程调用了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。1.Python中的select模块import selectfd_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中4、当超时时间为空则select会一直阻塞直到监听的句柄发生变化结论: select的优势在于可以处理多个连接不适用于单个连接2.select网络IO示例服务端#服务端from socket import *import selectserver 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,xlselect.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,addrsock.accept()#把新的客户端连接加入到监听列表中当客户端的连接有接收消息的时候select将被触发会知道这个连接有动静有消息那么返回给rl这个返回值列表里面。rlist.append(conn)else:# 由于客户端连接进来时socket接收客户端连接请求将客户端连接加入到了监听列表中(rlist)客户端发送消息的时候这个连接将触发# 所以判断是否是客户端连接对象触发try:datasock.recv(1024)#没有数据的时候我们将这个连接关闭掉并从监听列表中移除if not data:sock.close()rlist.remove(sock)continueprint(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()客户端#客户端from socket import *clientsocket(AF_INET,SOCK_STREAM)client.connect((127.0.0.1,8093))while True:msginput(: ).strip()if not msg:continueclient.send(msg.encode(utf-8))dataclient.recv(1024)print(data.decode(utf-8))client.close()select监听fd变化的过程分析#用户进程创建socket对象拷贝监听的fd到内核空间每一个fd会对应一张系统文件表内核空间的fd响应到数据后就会发送信号给用户进程数据已到#用户进程再发送系统调用比如(accept)将内核空间的数据copy到用户空间同时作为接受数据端内核空间的数据清除这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。该模型的优点#相比其他模型使用select() 的事件驱动模型只用单线程(进程)执行占用资源少不消耗太多 CPU同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序这个模型有一定的参考价值。该模型的缺点#首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口如linux提供了epollBSD提供了kqueueSolaris提供了/dev/poll…。如果需要实现更高效的服务器程序类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。#其次该模型将事件探测和事件响应夹杂在一起一旦事件响应的执行体庞大则对整个模型是灾难性的。select做得事情和第二阶段的阻塞没有关系就是从内核态将数据拷贝到用户态的阻塞始终帮你做得监听的工作帮你节省了一些第一阶段阻塞的时间。IO多路复用的机制select机制 Windows、Linuxpoll机制 Linux #和lselect监听机制一样但是对监听列表里面的数量没有限制select默认限制是1024个但是他们两个都是操作系统轮询每一个被监听的文件描述符(如果数量很大其实效率不太好)看是否有可读操作。epoll机制 Linux #它的监听机制和上面两个不同他给每一个监听的对象绑定了一个回调函数你这个对象有消息那么触发回调函数给用户用户就进行系统调用来拷贝数据并不是轮询监听所有的被监听对象这样的效率高很多。五、异步IO(Asynchronous IO)Linux下的asynchronous IO其实用得不多从内核2.6版本才开始引入。先看一下它的流程用户进程发起read操作之后立刻就可以开始去做其它的事。而另一方面从kernel的角度当它受到一个asynchronous read之后首先它会立刻返回所以不会对用户进程产生任何block。然后kernel操作系统会等待数据(阻塞)准备完成然后将数据拷贝到用户内存当这一切都完成之后kernel会给用户进程发送一个signal告诉它read操作完成了。貌似异步IO这个模型很牛~~但是你发现没有这不是我们自己代码控制的都是操作系统完成的而python在copy数据这个阶段没有提供操纵操作系统的接口所以用python没法实现这套异步IO机制其他几个IO模型都没有解决第二阶段的阻塞(用户态和内核态之间copy数据)但是C语言是可以实现的因为大家都知道C语言是最接近底层的虽然我们用python实现不了但是python仍然有异步的模块和框架(tornado、twstied高并发需求的时候用)这些模块和框架很多都是用底层的C语言实现的它帮我们实现了异步你只要使用就可以了但是你要知道这个异步是不是很好呀不需要你自己等待了操作系统帮你做了所有的事情你就直接收数据就行了就像你有一张银行卡银行定期给你打钱一样。六、IO模型比较分析 到目前为止已经将四个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阻塞。按照这个定义四个IO模型可以分为两大类之前所述的blocking IOnon-blocking IOIO multiplexing都属于synchronous IO这一类而 asynchronous I/O后一类 。有人可能会说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。各个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)的区别和联系:selectpollepoll都是IO多路复用的机制I/O多路复用就是通过一种机制可以监视多个描述符一旦某个描述符就绪(一般是读就绪或者写就绪)能够通知应用程序进行相应的读写操作。但selectpollepoll本质上都是同步I/O因为他们都需要在读写事件就绪后自己负责进行读写也就是说这个读写过程是阻塞的而异步I/O则无需自己负责进行读写异步I/O的实现会负责把数据从内核拷贝到用户空间。1.selectselect的原型int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);select的第一个参数nfds为fdset集合中最大描述符值加1fdset是一个位数组其大小限制为__FD_SETSIZE(1024)位数组的每一位代表其对应的描述符是否需要被检查。第二三四参数表示需要关注读、写、错误事件的文件描述符位数组这些参数既是输入参数也是输出参数可能会被内核修改用于标示哪些描述符上发生了关注的事件所以每次调用select前都需要重新初始化fdset。timeout参数为超时时间该结构会被内核修改其值为超时剩余的时间。select的调用步骤(1)使用copy_from_user从用户空间拷贝fdset到内核空间(2)注册回调函数__pollwait(3)遍历所有fd调用其对应的poll方法(对于socket这个poll方法是sock_pollsock_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支持的文件描述符数量太小了默认是10242.pollpoll的原型int poll(struct pollfd *fds, nfds_t nfds, int timeout); poll与select不同通过一个pollfd数组向内核传递需要关注的事件故没有描述符个数的限制pollfd中的events字段和revents分别用于标示关注的事件和发生的事件故pollfd数组只需要被初始化一次。 poll的实现机制与select类似其对应内核中的sys_poll只不过poll向内核传递pollfd数组然后对pollfd中的每个描述符进行poll相比处理fdset来说poll效率更高。poll返回后需要对pollfd中的每个元素检查其revents值来得指事件是否发生。3.epoll 直到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的原型int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); epoll既然是对select和poll的改进就应该能避免上述的三个缺点。那epoll都是怎么解决的呢在此之前我们先看一下epoll 和select和poll的调用接口上的不同select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函 数epoll_create,epoll_ctl和epoll_waitepoll_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察看,一般来说这个数目和系统内存关系很大。epoll实现代码示例(了解即可)#!/usr/bin/env pythonimport selectimport socketresponse bserversocket 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作为一个序列(filenoevent 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:# 一旦完整的响应数据发送完成就不再关注写eventepoll.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_sourcecopy总结(1)selectpoll实现需要自己不断轮询所有fd集合直到设备就绪期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用 epoll_wait不断轮询就绪链表期间也可能多次睡眠和唤醒交替但是它是设备就绪时调用回调函数把就绪fd放入就绪链表中并唤醒在 epoll_wait中进入睡眠的进程。虽然都要睡眠和交替但是select和poll在“醒着”的时候要遍历整个fd集合而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就行了这节省了大量的CPU时间这就是回调机制带来的性能提升。(2)selectpoll每次调用都要把fd集合从用户态往内核态拷贝一次并且要把current往设备等待队列中挂一次而epoll只要 一次拷贝而且把current往等待队列上挂也只挂一次(在epoll_wait的开始注意这里的等待队列并不是设备等待队列只是一个epoll内 部定义的等待队列)这也能节省不少的开销。4.selector 这三种IO多路复用模型在不同的平台有着不同的支持而epoll在windows下就不支持好在我们有selectors模块帮我们默认选择当前平台下最合适的我们只需要写监听谁然后怎么发送消息接收消息但是具体怎么监听的选择的是select还是poll还是epoll这是selector帮我们自动选择的selector代码示例服务端#服务端from socket import *import selectorsselselectors.DefaultSelector()def accept(server_fileobj,mask):conn,addrserver_fileobj.accept()sel.register(conn,selectors.EVENT_READ,read)def read(conn,mask):try:dataconn.recv(1024)if not data:print(closing,conn)sel.unregister(conn)conn.close()returnconn.send(data.upper()b_SB)except Exception:print(closing, conn)sel.unregister(conn)conn.close()server_fileobjsocket(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,并且绑定了一个回调函数acceptwhile True:eventssel.select() #检测所有的fileobj是否有完成wait data的for sel_obj,mask in events:callbacksel_obj.data #callbackaccpetcallback(sel_obj.fileobj,mask) #accpet(server_fileobj,1)客户端#客户端from socket import *csocket(AF_INET,SOCK_STREAM)c.connect((127.0.0.1,8088))while True:msginput(: )if not msg:continuec.send(msg.encode(utf-8))datac.recv(1024)print(data.decode(utf-8))小练习基于selectors模块实现并发的FTP