一、事件驱动模型介绍
传统的编程是如下线性的:
开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束
事件驱动型程序模型:
开始--->初始化--->等待
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。
另外两种常见的编程范式是(单线程)同步以及多线程编程
二、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资源的。
文件描述符
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
缓存I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝
三、五种IO模型
五种IO模型:
blocking IO
nonblocking IO
IO multiplexing
signal driven IO 不常用
asychronous IO
阻塞IO
略
非阻塞IO
例子1
#--服务端 import time import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) sk.bind(('127.0.0.1',6667)) sk.listen(5) sk.setblocking(False) #设置非阻塞 print('waiting client connection .......') while True: try: connection,address = sk.accept() # 进程主动轮询 print("+++",address) client_messge = connection.recv(1024) print(str(client_messge,'utf8')) connection.close() except Exception as e: print (e) time.sleep(4) #--客户端 import time import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) while True: sk.connect(('127.0.0.1',6667)) print("hello") sk.sendall(bytes("hello","utf8")) time.sleep(2) break
这种方式的两个不足之处:发了太多的系统调用,数据处理不及时
IO多路复用
elect poll epoll IO多路复用区别
select: win Linux unix mac通用的 缺点是监听的最大连接数1024
poll:linux
epoll:linux
epoll是最好的一种实现方式。
没有最大文件描述符数量的限制。
比如100个连接,有两个活跃了,epoll会告诉用户这两个两个活跃了,直接取就ok了,而select是循环一遍。
例子1,使用select
import socket import select sk=socket.socket() sk.bind(("127.0.0.1",9904)) sk.listen(5) while True: r,w,e=select.select([sk,],[],[],5)#参数分别表示:input output errorput,5表示监听5秒钟。不设置就一直监听 #使用select来监听sk. 有连接了对应sk有变化(通过文件描述符), for i in r: conn,add=i.accept()#注释这两行执行结果:每隔5秒输出hello 和>>>>>> print(conn)#客服端连接后,执行完。在下一次循环r也有值。因为select是水平触发 #没注释时,通过accept接收到了用户态,循环时就监听到没数据。注释后,没接受,在内核态就一直有变化数据 print("hello") print('>>>>>>') #客户端 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"))
补充:触发方式
在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别如下:
水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态,
没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发.
边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能
多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述
符.信号驱动式IO就属于边缘触发.
epoll既可以采用水平触发,也可以采用边缘触发.
从电子的角度理解:
水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.(只要
有数据可读(描述符就绪)那么水平触发的epoll就立即返回)
边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.(即使有数据
可读,但是没有新的IO活动到来,epoll也不会立即返回.)
IO多路复用优势:
同时可以监听多个连接
实现并发,socketservice里就用到了select。
例子2,实现并发
#服务端 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) #[sk,conn]能监听两个socket,一是本机的,而是连接的 #有新的连接的时候,sk有变化,r里有sk。当收到客户端的消息时,r里有conn for obj in r: if obj==sk: conn,add=obj.accept() print(conn) inp.append(conn) else: data_byte=obj.recv(1024) print(str(data_byte,"utf8")) res = input('回答%s号客户>>>' %inp.index(obj)) obj.sendall(bytes(res, 'utf8')) print('>>>>>>') #客户端 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"))
例子3,使用selectors模块
#服务端 import selectors import socket sel = selectors.DefaultSelector()#根据操作系统判断支持的IO多路复用的方式,使用。 def accept(sock, mask): conn, addr = sock.accept() # Should be ready print('accepted', conn, 'from', addr) conn.setblocking(False) sel.register(conn, selectors.EVENT_READ, read)#conn与read进行绑定 def read(conn, mask): data = conn.recv(1000) # Should be ready if data: print('echoing', repr(data), 'to', conn) conn.send(data) # Hope it won't block else: print('closing', conn)#linux关闭客户端时data是空走else。win下要加异常处理。略 sel.unregister(conn) #解除 conn.close() sock = socket.socket() sock.bind(('localhost', 9904)) sock.listen(100) sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, accept) #绑定:文件描述符和accept绑定 #sock只要有活动,直接去调用accept方法 while True: events = sel.select()#监听: for key, mask in events: callback = key.data #函数名字,accept (read) callback(key.fileobj, mask)#key.fileobj 就是socket对象 (conn) #客户端 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"))
异步IO
异步最大的特点 全程无阻塞
blocking 阻塞,nonblocking 非阻塞,IO multiplexing IO多路复用 都是同步