事件,IO,select
事件驱动模型
对于普通编程来说,代码遵循线性流程:开始--》代码A--》代码B--》代码C--》。。。--》结束,编程者知道代码的运行顺序,由编程者控制
事件驱动模型,流程则是:开始--》初始化--》等待,这个等待不同于常规编程的等待,如input(),强制需要用户输入某种数据。
事件驱动模型的等待,是不知道的,不需要强制用户输入,而是当某个事件发生,程序就会做出反应,这些事件包括鼠标点击,信息输入,键盘固定设置的键等
对于编写服务器处理模型的程序,有3种方式
1.每接到一个请求,创建一个线程处理
2.每接到一个请求,创建一个进程处理
3.每接到一个请求,放到一个事件列表中,让主进程通过IO阻塞的方式处理
第三种就是协程、事件驱动模型方式,大多网络服务器都是采用这种方式
例:
对于UI编程来说,常常对鼠标点击要做出反应,如果采用创建一个线程循环检测是否有鼠标点击,则会有很多弊端
如:CPU资源的浪费,阻塞情况下,其他的情况的检测,将无法进行,监测多个设备时,会造成响应时间延迟
所以对UI编程来说,都会采用事件驱动模型,如很多平台都会提供onClick()事件
事件驱动模型的大致流程如下:
1.有一个事件队列
2.当发生一个事件时,向列表中增加这个事件
3.有一个循环,不断从队列中取出事件,根据事件的不同,调用不同的函数
4.事件都各自保存自己的函数指针,这样,每个事件都有自己的处理函数
事件驱动模型编程是一种编程范式,这里的执行流由外部的事件决定,它的特点是有一个事件循环,当外部事件发生时通过回调机制触发相应处理
另外两种编程范式是单线程同步,多线程编程
综述:
1.事件驱动模型可以节省CPU资源,它有机会释放CPU进入睡眠状态(注意这是有机会,也可以自主选择不释放),当时间触发时再被唤醒
2.典型的事件驱动的程序就是一个死循环,并以一个线程的方式存在,包含两部分,按照一定条件接收并选择要处理的事件和处理事件的过程,
且当事件没有被触发时,程序会因为查询事件队列失败而进入睡眠状态,从而释放CPU
3.事件驱动的程序必定直接或间接拥有一个事件队列,用于存储未处理的事件
4.事件驱动程序完全由外部输入的事件控制,
5.事件驱动程序,可以按照一定的顺序处理队列中的事件,而这个顺序是由事件触发的顺序决定,这一特性往往用于保证某个过程的原子化
6.注意:事件驱动的监听事件是由操作系统调用CPU完成的
IO多路复用
用户空间和内核空间
操作系统采用虚拟存储器,对于32位操作系统而言,它的寻址空间为4G(2的32次方)
操作系统的核心是内核,独立于普通应用程序,可以访问被保护的内存空间,也拥有访问底层硬件设备的所有权
为了保证用户程序不能直接访问内核(kernel),保护内核的安全,操作系统将虚拟内存分为两部分,一部分为内核空间,一部分为用户空间
针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,
而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起进程的执行,这种行为称为进程切换,由操作系统完成
从一个进程运行转到另一个进程运行,发生的变化:
保存处理机上下文,包括程序计数器和其他寄存器
更新PCB信息,把进程的PCB移入相应的队列,如:就绪,在某事件阻塞队列等
选择另一进程执行,并更新其PCB
更新内存管理的数据结构,恢复处理机上下文
进程阻塞
正在执行的程序,由于期待的某个事件未发生,如请求系统资源失败,等待某种操作,新数据尚未到达等等,
则由系统自动执行阻塞原语(block),当进程进入阻塞状态,是不占用CPU资源的
文件描述符(file descriptor),计算机术语,是一个用于指向文件引用的抽象概念,在形式上是一个非负整数
它是一个索引值,当打开或创建一个文件时,内核向进程返回一个描述符
一些涉及底层程序编写时多围绕文件描述符编写,只适用于Linux和Unix
缓存I/O
缓存I/O又称标准I/O,大多文件系统的默认I/O操作都是缓存I/O,Linux的缓存I/O机制中,
操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)
即数据会先拷贝到操作系统内核的缓存区,然后再从内核的缓存区拷贝到用户的地址空间
缺点:数据在传输时的内核和用户空间的拷贝操作,对CPU和内存带来的开销是非常大的
blocking IO(阻塞IO)
在Linux下,所有的socket默认情况下都是blocking,其大致流程如下
当用户调用recvfrom时,内核(kernel)开始准备数据,对于network IO来说,很多时候数据在开始时并未到达,于是kernel就会等待,
用户这边,进程就会阻塞,直到kernel拿到所有数据,并copy到用户内存中,kernel会返回一个结果,用户进程才会解除block状态
所以,blocking IO的特点就是IO执行的两个过程都被block了
non-blocking IO(非阻塞IO)
Linux下,可以设置socket使其变为non-blocking IO,流程如下
与blocking IO不同,non-blocking IO在发出询问时,如果数据没有准备好,kernel会立刻回一个消息,用户不用等待
用户进程通过的轮询,直到kernel检测到数据准备好,然后将数据copy到用户内存
它将一个长时间的阻塞分成多个小阻塞,在此期间,进程不断被CPU访问,CPU权限还在自己手中,这段期间是可以再操作其他的事情,
也因此导致任务响应延迟增大
注意当kernel检测到数据准备好了之后的拷贝操作,依然是阻塞状态
#server端
import time import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) sk.setsockopt sk.bind(('127.0.0.1',6667)) sk.listen(5) sk.setblocking(False) #设置为非阻塞IO while True: try: print ('waiting client connection .......') 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)
#client端 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 multilexing(IO多路复用)
利用select或epoll轮询所负责的socket,当数据到达时,就通知用户进程,select/epoll的好处在于单个process可以处理多个网络连接的IO
当用户调用select后,整个进程都会被block,同时kernel会监测所有select负责的socket,当任何一个socket数据准备好后,select就会返回
此时,用户在read操作,将数据从kernel中copy到用户进程
想比较blocking IO ,IO multilexing 优势在于可以同时处理多个connection(如果处理的链接数不多的话,不一定比multi-threading+blocking IO快)
#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) #通过select监测socket连接,返回3个结果 #4个参数,第1个为可读集合,第2个位可写,第3个为异常信息,第4个是等待的最大时间 # 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)
#client.py 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'))
asynchronous I/o(异步IO)
全程不存在阻塞,用户发起read后,kernel会立刻返回结果,然后用户就会去做其他的事情,然后kernel等待数据准备好,然后copy到用户内存
全部做好之后,kernel会发送一个signal,告诉用户已read完成
各个IO model过程比较
select,poll,epoll
select:通过select()系统调用来监测多个系统描述符的数组,当select()返回时,该数组中的文件描述符就会被修改标志位,
使得进程可以获得这些标志位,从而进行后续的读写操作,select全平台支持,但单个进程监测文件描述符存在最大限制,Linux一般最大1024
poll:与select差不多,但它没有最大限制
epoll:无最大限制,和select不同,当某个 链接活跃时,能直接提供具体的链接,而不用像select将整个链接轮询一遍才能得到这个链接
IO多路复用的触发方式
水平触发:如果文件描述符已经就绪,可以非阻塞的执行IO时,此时触发通知,允许任意时刻重复检测IO状态,而不必尽可能多的执行IO
边缘触发:如果文件描述符在上一次状态改变后又有新的IO操作来了,触发通知,此时就要尽可能多的执行IO
select属于水平触发,epoll既属于水平触发,又属于边缘触发
两种触发的区别:当水平触发时,select/epoll会立即返回,不会有阻塞,因为已经有数据了
当边缘触发时,不会返回,此时是阻塞状态,等到新的数据到来时,epoll才会返回,此时新数据,老数据都可以读到
从电子角度来看:水平触发是只在高电平和低电平情况下才会触发。边缘触发是在电平发生改变(高->低,低->高)时触发