IO
事件驱动
1.要理解事件驱动和程序,就需要与非事件驱动的程序进行比较。实际上,现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的。早期则存在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时, 会不断地检查这个条件,直到条件满足,这是很浪费cpu时间的。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,当然程序也可自行决定不释放cpu),当事件触发时被操作系统唤醒,这样就能更加有效地使用cpu. 2.再说什么是事件驱动的程序。一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照一定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执 行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。 3.事件驱动的程序,必定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件。 4.事件驱动的程序的行为,完全受外部输入的事件控制,所以,事件驱动的系统中,存在大量这种程序,并以事件作为主要的通信方式。 5.事件驱动的程序,还有一个最大的好处,就是可以按照一定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性往往被用于保证某些过程的原子化。 6.目前windows,linux,nucleus,vxworks都是事件驱动的,只有一些单片机可能是非事件驱动的。
IO
IO: 同步、异步、阻塞、非阻塞
阻塞IO
阻塞IO:只发生一次系统调用, 阻塞的时候不能做其它事情,一直等待
默认情况下所有的socket都是blocking
非阻塞IO
非阻塞IO: 发生多次系统调用, 数据处理会滞后。
linux下,可以通过设置socket使其变为non-blocking。(socket_obj.setblocking(False))
在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
、
需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
缺点:
1.循环调用recv()将大幅度推高CPU占用率
2.任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
IO多路程复用
又称为事件驱动IO(event driven IO)
select,epoll,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
注意:
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。
select监听fd变化的过程分析: #用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到; #用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。 import socket import select tcp_server = socket.socket() tcp_server.bind(('127.0.0.1', 8080)) tcp_server.listen(10) rlist=[tcp_server,] wlist=[] wdata={} while True: # tcp_server.accept() rl, wl, e = select.select(rlist,wlist,[], 3) # 监听tcp_server # select.select(rlist,wlist,xlist[,timeout]) # rlist -- wait until ready for reading # wlist -- wait until ready for writing # xlist -- wait for an ``exceptional condition # timeout -- timeout print(rl) for sock in rl: # conn, addr = sock.accept() # print(conn) print('hello') print('>>>')
import select import socket import time # select 把socket放入 select中,然后每当有一个连接过来,把连接conn放入select模型里面去 port = 8080 ip = "127.0.0.1" ss = socket.socket() ss.bind((ip, port)) ss.listen(10) read_list = [ss] write_list = [] msg_list = dict() while 1: # print('listen again') rlist, wlist, xlist = select.select(read_list, write_list, [], 5) for i in rlist: if i is ss: # 如果ss准备就绪,那么说明ss就可以接受连接了,当ss接受到连接 # 那么把连接返回readlist conn, addr = i.accept() read_list.append(conn) # 如果不是socket对象,那么就是conn连接对象了,如果是conn连接对象,那么就代表有 # 读入数据的变化,对应recv方法 else: try: data = i.recv(1024) # 如果接受不到数据了 则说明连接已经关闭了 if not data: print('connecting close') read_list.remove(i) break # 我们去发送数据,但是我们要把conn准备好了再去发送 # 所以首先把数据存在一个dict中msg_list,然后再等他准备好的时候 # 再去发送 msg_list[i] = [data] if i not in write_list: write_list.append(i) except Exception: read_list.remove(i) for j in wlist: # 把对应各自的消息取出来 msg = msg_list[j].pop() try: j.send(msg) # 回复完成后,一定要将outputs中该socket对象移除 write_list.remove(j) except Exception: # 如果报错就所以连接或者已经断开了,那么我们就把他移出出去 write_list.remove(j)
import selectors import socket sel = selectors.DefaultSelector() 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) 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) sel.unregister(conn) conn.close() sock = socket.socket() sock.bind(('localhost', 1234)) sock.listen(100) sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, accept) while True: events = sel.select() for key, mask in events: callback = key.data callback(key.fileobj, mask)
异步IO