参考:https://www.cnblogs.com/yuanchenqi/articles/5722574.html
https://blog.csdn.net/zhuangzi123456/article/details/84400108
一.事件驱动模型(一种编程范式)
协程:遇到IO切换
但何时切回去?如何确定IO操作结束?—>通过回调函数
传统的编程是如下线性模式的:
开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束
每一个代码块里是完成各种各样事情的代码,但编程者知道代码块A,B,C,D…的执行顺序;唯一能改变这个流程的是数据,输入不同的数据,根据条件语句判断,流程或许就改为A—>C—>E…—>结束;每次程序运行顺序或许都不同,但流程是由输入数据和程序决定的;如果知道这个程序当前的运行状态(包括输入数据和程序本身),那就知道接下来甚至一直到结束的运行流程
对于事件驱动型程序模型,流程大致如下:
开始--->初始化--->等待
与上面传统编程模式不同,事件驱动程序在启动之后,就等待被事件触发;传统编程下也有“等待”的时候,比如在代码块D中定义了一个input(),需要用户输入数据;但这与下面的等待不同:传统编程的“等待”,作为程序编写者是知道或强制用户输入某个东西的,如果用户输入错误,还需要提醒他,并要求重新输入;事件驱动程序的等待则是完全不知道,也不强制用户输入或者干什么;只要某一事件发生,程序就会做出相应的“反应”;这些事件包括:输入信息,鼠标点击,敲击键盘上某个键,系统内部定时器触发
1.事件驱动模型介绍
通常写服务器处理模型的程序时,有以下几种模型:
每收到一个请求,创建一个新的进程,来处理该请求
每收到一个请求,创建一个新的线程,来处理该请求
每收到一个请求,放入一个事件列表,让主进程通过非阻塞IO方式来处理请求
第3种即协程/事件驱动的方式,是大多数网络服务器采用的方式
2.论事件驱动模型
#鼠标点击事件注册:
#1个HTML文件,用浏览器解析
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p onclick="fun()">点我呀</p> #发生onclick事件时执行fun()函数
<script type="text/javascript">
function fun() {
alert('约吗?')
}
</script>
</body>
</html>
在UI编程中,常常要对鼠标点击进行相应,两种方式获得:
- 创建一个线程循环检测是否有鼠标点击
CPU资源浪费:可能鼠标点击的频率非常小,但扫描线程还是会一直循环检测
如果扫描鼠标点击的接口是阻塞的:如果该事件不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,可能永远不会扫描键盘
如果一个循环要扫描的设备非常多,又会引来响应时间的问题
- 事件驱动模型
目前UI编程大多是事件驱动模型,如很多UI平台提供onClick()事件(鼠标按下事件)
大体思路如下:
- 有一个事件(消息)队列(先进先出)
- 鼠标按下时,往队列中增加一个点击事件(消息)
- 有个循环,不断从队列取出事件;根据不同的事件,调用不同的函数,如onClick()/onKeyDown()
- 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数
事件驱动编程是一种编程范式,程序的执行流由外部事件决定,包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理
另外两种常见的编程范式是(单线程)同步,多线程编程
下图展示了随着时间的推移,这三种模式下程序所做的工作
这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身;阻塞在I/O操作上所花费的时间用灰色框标示
事件驱动注解:
1.要理解事件驱动和程序,就需要与非事件驱动的程序进行比较:
现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的
早期则存在许多非事件驱动的程序,在需要等待条件触发时,会不断地检查这个条件,直到满足,浪费cpu时间
而事件驱动的程序则有机会释放cpu从而进入睡眠态(注意是有机会,程序也可自行决定不释放cpu)
当事件触发时被操作系统唤醒,更加有效地使用cpu
2.再说什么是事件驱动的程序:
一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在
该死循环包括两部分:第一个部分按一定的条件接收并选择一个要处理的事件
第二个部分就是事件的处理过程
程序的执行过程就是选择事件和处理事件
而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,释放cpu
3.事件驱动的程序,必定会直接或间接拥有一个事件队列,用于存储未能及时处理的事件
4.事件驱动的程序的行为,完全受外部输入的事件控制
所以事件驱动的系统中存在大量这种程序,并以事件作为主要的通信方式。
5.事件驱动的程序可以按一定的顺序处理队列中的事件
而这个顺序由事件的触发顺序决定,这一特性常用于保证某些过程的原子化
6.目前windows,linux,nucleus,vxworks都是事件驱动的
只有一些单片机可能是非事件驱动的。
事件驱动的监听事件是由操作系统调用的cpu来完成的
二.IO模型准备
1.用户空间和内核空间:
现在操作系统都是采用虚拟存储器
对32位操作系统而言,寻址空间(虚拟存储空间)为4G(2的32次方)
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限
为了保证用户进程不能直接操作内核(kernel),保证内核安全,操作系统将虚拟空间划分为两部分:内核空间和用户空间
对linuxOS来说,最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称内核空间;较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各进程使用,称用户空间
实际上是CPU中寄存器的状态位不同(0为内核态,1为用户态),状态位不同时,依靠CPU指令集的不同进行操作上的限制(内存本身并没有进行分割)
2.进程切换:
为控制进程的执行,内核需能挂起在CPU上运行的进程;并恢复以前挂起的某进程的执行(即进程切换),这种切换由操作系统完成
因此任何进程都是在OS内核的支持下运行的,与内核紧密相关
从一进程的运行转到另一进程的运行经过以下变化:
保存处理机上下文,包括程序计数器和其他寄存器。
更新PCB信息
把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
选择另一个进程执行,并更新其PCB
更新内存管理的数据结构
恢复处理机上下文
注:进程切换很耗资源
3.进程的阻塞:
正在执行的进程期待的某些事件未发生,如请求系统资源失败/等待某操作完成/新数据未到达/无新任务,由系统自动执行阻塞原语(Block),使进程由运行状态变为阻塞状态
可见进程的阻塞是进程自身的主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态
当进程进入阻塞状态,不占用CPU资源
4.文件描述符File descriptor:
Fd是一个用于表述指向文件的引用的抽象化概念
在形式上是一个非负整数
实际上,Fd是一个索引值,指向内核为每个进程维护的该进程打开文件的记录表;当程序打开一个现有文件/创建一个新文件时,内核向进程返回一个Fd
在程序设计中,一些涉及底层的程序编写往往围绕着Fd展开;但这一概念往往只适用于UNIX/Linux这样的操作系统
三.缓存IO:又称标准I/O
大多数文件系统的默认IO操作都是缓存IO
1.如何运行:
#在Linux的缓存IO机制中:
OS会将IO的数据缓存在文件系统的页缓存(page cache)中
也就是说:数据先被拷贝到OS内核的缓冲区,然后再拷贝到应用程序的地址空间
用户空间无法直接访问内核空间---内核态到用户态的数据拷贝
- 为什么数据要先到内核区,直接到用户内存不是更直接吗?
只有操作系统可以操作硬件
2.缺点:数据在传输过程中需在应用程序地址空间和内核进行多次拷贝,这些拷贝操作带来的CPU以及内存开销极大
3.五种IO Model
- 同步(synchronous)IO和异步(asynchronous)IO,阻塞(blocking)IO和非阻塞(non-blocking)IO分别是什么,有什么区别?
- 这个问题不同的人给出的答案可能不同,比如wiki认为asynchronous IO和non-blocking IO是一个东西
- 这其实是因为不同人的知识背景不同,并且在讨论这个问题时上下文(context)也不同
为了更好的回答这个问题,先限定一下本文的上下文:Linux环境下的network IO
Stevens在文章中一共比较了五种IO Model:
blocking IO:阻塞IO |
nonblocking IO:非阻塞IO |synchronous:同步IO
IO multiplexing:IO多路复用 |
#signal driven IO:信号驱动IO
asynchronous IO:异步IO
具体介绍见后文
signal driven IO在实际中不常用,这里只提及剩下的四种IO Model
一个network IO(这里以read举例)会涉及两个系统对象:调用这个IO的process (or thread)和系统内核(kernel);当一个read操作发生时,会经历两个阶段:
- 等待数据准备(Waiting for the data to be ready)
- 将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
这些IO Model的区别就是在这两个阶段上有不同情况
四.五种IO Model详解
1.blocking IO(阻塞IO):
在linux中,默认情况下所有的socket都是blocking
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen(5)
while True:
conn,addr=sk.accept() #发送系统调用,见下图
while True:
conn.send(b'`W!2d8h;o12e“21d@$Efw/e2‘') #发送系统调用
data=conn.recv(1024) #发送系统调用
- 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据;对network IO来说,很多时候数据在开始还没有到达(比如,还没有收到一个完整的数据包),这时kernel就要等待足够的数据到来;而在用户进程这边,整个进程会被阻塞;当kernel等到数据准备好了,就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
缺点:效率低下
特点:在IO执行的两个阶段都被block;只发生一次系统调用;同步
2.non-blocking IO(非阻塞IO)
linux下可以通过.seblocking设置socket使其变为non-blocking
import socket,time
sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen(5)
sk.setblocking(False)
while True:
try:
conn,addr=sk.accept() #进行主动轮询,见下图
pass
conn.close()
except Exception as e: #如果系统调用返回error,执行其中代码
print(e)
time.sleep(5) #每次返回error,暂停5s
#返回error时打印:'[WinError 10035] 无法立即完成一个非阻止性套接字操作。'
- 当用户进程发出read操作时,如果kernel中的数据未准备好,kernel不会block用户进程,而是立刻返回一个error;从用户进程角度讲 ,发起一个read操作后,不需要等待,而是马上得到一个结果;用户进程判断结果是error时,就知道数据还未准备好,于是可以再次发送read操作;一旦kernel中数据准备好了,并再次收到用户进程的system call,那么kernel就马上将数据拷贝到用户内存然后返回
特点:用户进程需要不断主动询问kernel
- 在网络IO时,非阻塞IO也会进行recvform系统调用;与阻塞IO不同,非阻塞将整片时间的阻塞分成许多小的阻塞,所以进程不断有机会"被CPU光顾";即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间可以做其他事情
- 也就是说非阻塞的recvform系统调用后,进程没有被阻塞,内核马上返回给进程;如果数据还没准备好,返回一个error;进程在返回后,可以做别的事,然后再发起recvform系统调用
- 重复上面的过程,循环往复进行recvform系统调用,这个过程通常被称之为轮询;轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理
- 需要注意,拷贝数据的过程中,进程仍然是属于阻塞的状态
缺点:系统调用多;如果在做其他事时收到数据,数据无法及时处理
3.IO multiplexing(IO多路复用):也称event driven IO
利用select模块,该模块提供select/poll/epoll三个方法;Windows只支持select;Linux均支持;MacOS支持slect/poll
(1)基本介绍:
- 好处:单个process就可以同时处理多个网络连接的IO
- 基本原理:select()/epoll()函数会不断轮询负责的所有socket;当某个socket有数据到达,就通知用户进程
- 在IO multiplexing Model中,对每个socket,一般都设成non-blocking;但整个用户的process是一直被block的,不过是被select给block,而不是被socket IO给block
(2)使用select(效率最低):进程指定内核监听哪些文件描述符(最多1024个)的哪些事件,当没有文件描述符事件发生时,进程被阻塞;当一/多个文件描述符事件发生时,进程被唤醒;为水平触发
原理:监听一些端口,如其中某一个有新任务则发送一个信号,epoll接收到信号后得知有新任务(但不知道是谁的任务),开始轮询每一个端口(可能有多个端口有任务)
参见:https://www.cnblogs.com/zhiyong-ITNote/p/7553694.html
fd_r_list,fd_w_list,fd_e_list=select.select(rlist, wlist, xlist,timeout)
#参数说明:
rlist:wait until ready for reading(需要监听可读的socket对象)
wlist:wait until ready for writing(需要监听可写的socket对象)
xlist:wait for an "exceptional condition"(需要监听异常的socket对象)
timeout:最常监听秒数;默认为无限长
#如果超时,继续执行之后代码(不包括本进程之后的代码)
fd_r_list:满足可读条件的socket对象的列表(有任务要处理的socket对象)
#对conn而言,有新数据在内核态(recv前)时为高电平,入本列表
#对sk而言,有新连接请求在内核态时(accept前)为高电平,入本列表;初始为高电平(本身是一个新连接)
#获得连接后Fd才被从fd_r_list中移除
fd_w_list:满足可写条件的socket对象的列表
fd_e_list:发生错误的socket对象的列表
import socket,select
sk=socket.socket()
sk.bind(('127.0.0.1',8050))
sk.listen(5)
inp=[sk,]
while True:
r,w,e=select.select(inp,[],[],5)#监听是否有给sk的连接#最多监听5s
for i in r:#r为监听对象中状态发生变化的对象构成的列表
#每次对方发送数据时conn都发生变化(往socket中放入数据)
#出现新的连接请求时sk发生变化
#conn,add=i.accept()
#应该有分支结构,此处省略
#print(conn) #注释掉这两行后,sk不被接入用户态,发生死循环print('hello')
print('hello')
#inp.append(conn)
print('>>>')#超时则从此次处开始继续执行
- 当用户进程调用了select,那么整个进程会被block;而同时kernel会"监视"所有select负责的socket;当任何一个socket中的数据准备好了,select就会返回;这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程
- 这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些:这里使用了两个system call(select和recvfrom),而blocking IO只调用了一个system call(recvfrom);用select的优势在于可以在一个线程中同时处理多个connection
#server端并发聊天:
import socket,select
sk=socket.socket()
sk.bind(('127.0.0.1',8050))
sk.listen(5)
inputs=[sk,]
while True:
r,w,e=select.select(inp,[],[],5)
for obj in r:
if obj == sk:
conn,add=onj.accept()
inputs.append(conn)
else:
data=obj.recv(1024).decode('utf-8')
inp=input('回答%s号用户>>>'%inputs.index(obj))
obj.sendall(inp.encode('utf-8'))
print('>>>')
- 所以,如果处理的连接数不多,使用select/epoll的web server不一定比使用multi-threading+blocking IO的web server性能好,可能延迟还更大;select/epoll的优势不是对于单个连接处理得更快,而在于能处理更多的连接
- select函数返回结果中如果有文件可读了,进程就可以通过调用accept()/recv()来让kernel将位于内核中准备到的数据copy到用户区
(3)使用poll
(4)epoll(效率最高):要求提交请求的同时提交身份,只去有任务的端口获取数据;既可采用水平触发,也可采用边缘触发
市面上上见到的所谓的异步IO,如nginx/Tornado,就是利用epoll来实现的;我们叫它异步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:
# 这次再有人按按钮,我这不光灯会亮,上面还会显示要交卷学生的名字.这样我就可以直接去对应学生那收卷就
# 好了.当然,同时可以有多人交卷.
4.Asynchronous I/O(异步IO)—用户效率最高,但实现机制复杂
在linux下asynchronous IO用得很少
- 用户进程发起read操作之后,立刻就可以开始去做其它的事;而从kernel的角度来看,受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block;然后kernel会等待数据准备完成,然后将数据拷贝到用户内存;当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
特点:全程无阻塞
5.触发方式:水平触发&边缘触发
# 在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也不会立即返回.
6.IO Model比较
在non-blocking IO中,虽然进程大部分时间都不会被block,但仍要求进程主动check,且当数据准备完成后,也需要进程再次主动调用recvfrom拷贝数据到用户内存;而asynchronous IO就像是用户进程将整个IO操作交给了kernel来完成,做完后发信号通知,用户进程无需检查IO操作的状态,也无需主动拷贝数据
五.selectors模块:用于IO多路复用
1.该模块定义了1个BaseSelector抽象基类及其子类:SelectSelector/PollSelector/EpollSelector/DevpollSelector/KqueueSelector,分别使用对应的select方法
2.另外还有一个DefaultSelector类,是以上其中某个子类的别名,自动选择当前环境中最有效的Selector,平时用该类即可
3.模块定义了两个常量,用于描述event Mask(注册一个文件对象时,等待的events)
EVENT_READ:表示可读的,值其实是1
EVENT_WRITE:表示可写的,值其实是2
4.块定义了一个 SelectorKey类,一般用这个类的实例来描述一个已经注册的文件对象的状态,这个类的常用属性包括:
fileobj:表示已经注册的文件对象
fd:表示文件对象的描述符;是个整数;是文件对象的 fileno()方法的返回值
events:表示注册一个文件对象时,等待的events,即上面的event Mask
data:表示注册一个文件对象是绑定的data
5.抽象基类中的方法:
.register(fileobj, events, data):注册一个文件对象,返回1个SelectorKey类的实例
#将fileobj放入监听对象列表;将fileobj与data绑定
#fileobj:即可以是fd也可以是一个拥有fileno()方法的对象,如socket对象
#events:上面的event Mask常量
#data:fileobj有活动时调用的函数;默认为None
.unregister(fileobj):注销一个已注册过的文件对象,返回1个SelectorKey类的实例
#将fileobj移出监听对象列表
.modify(fileobj,events,data):修改1个注册过的文件对象,如监听可读->监听可写
#其实就是register()+unregister(),但更高效
#参数说明同register()
.select(timeout=None):选择满足监听event的对象,返回1个(key,events)的元组
#开始监听
#key是一个SelectorKey类的实例,key.data为与fileobj绑定的函数
#events是event Mask(EVENT_READ/EVENT_WRITE/二者的组合)
.close():关闭selector
#最后一定要记得调用它,要确保所有的资源被释放;
.get_key(fileobj):返回注册文件对象的key(返回1个SelectorKey类的实例)
6.应用:FTP等
import selectors,socket
sel=selectors.DefaultSelector()
#创建1个DefaultSelectors对象sel,所有监听相关操作都围绕sel展开
#会根据操作系统自动选择1个最好的IO多路复用的方式(Windows下只有select)
def accept(sock,mask):
conn,addr=sock.accept()#接收请求
conn.setblocking(False)
sel.register(conn,selectors.EVENT_READ,read)
#将conn和read()绑定,开始监听conn(conn是某个用户的连接)
'''适用于Linux
def read(conn,mask):
data=conn.recv(1024)
if data:
conn.send(data)
else:#如果client断开了连接,则data为空
sel.unregister(conn)#将conn移出监听对象列表
conn.close()
'''
def read(conn,mask):
try:
data=conn.recv(1024)
conn.send(data)
except Exception as e:#在Windows下,如果client断开了连接,则发生异常
sel.unregister(conn)#将conn移出监听对象列表
conn.close()
sock=socket.socket()
sock.bind(('127.0.0.1',8080))
sock.listen(5)
sock.setblocking(False)
sel.register(sock,selectors.EVENT_READ,accept)
#将sock与accept()绑定(sock有活动就调用accept),开始监听sock
while True:
events=sel.select()
#events是1个list,包含所有有活动的socket对象的相关信息
#events的元素是1个个tuple,每个tuple包括key和mask这2部分
for key,mask in events:
#key为SelectorKey对象;mask为event Mask,是1个掩码,值为0或1
callback=key.data#callback为与监听对象绑定的函数,此处就是accept()
callback(key.fileobj,mask)#执行callback
#key.fileobj为有活动的socket对象