文章目录
一、IO模型的简要介绍
下面以network的read、recv举例来介绍IO模型的四种模型。一般当read/recv读数据的操作发生时,该操作会经历两个阶段:
- 等待数据 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
这些IO模型的区别就是在两个阶段上各有不同的情况。
阻塞 I/O(blocking IO)
在linux中,默认情况下所有的socket都是blocking。即一直等待,直到获得数据。blocking IO的特点就是在IO执行的两个阶段都被block了。
缺点
调用recv的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
解决方法
- 多线程或者多进程【无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。】
- “线程池”或“连接池”【线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执
行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,但“池”始终有其上限】。
注:受为收,打错字了。
非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。即执行下面任务不断询问数据是否准备好。【用户进程其实是需要不断的主动询问kernel数据好了没有。】每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是可以做其他事情的。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。尽管非阻塞IO解决了阻塞IO的一些问题但非阻塞IO模型绝不被推荐。
缺陷:
- 推高CPU占用率浪费资源;
- 数据有延迟。
I/O 多路复用( IO multiplexing)【事件驱动IO(event driven IO)】
IO多路复用其实就是我们说的select,poll,epoll,。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这几个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。即select【以select举例】会轮询检测所有的连接,或者 IO。哪个有数据,就返回.我们就可以拿到了。【其理论事件驱动模型】
当用户进程调用了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。
select函数
import select
fd_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()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息。
select的优势在于可以处理多个连接,不适用于单个连接 。
示例
#服务端
from socket import *
import select
server = 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,xl=select.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,addr=sock.accept()
#把新的客户端连接加入到监听列表中,当客户端的连接有接收消息的时候,select将被触发,会知道这个连接有动静,有消息,那么返回给rl这个返回值列表里面。
rlist.append(conn)
else:
# 由于客户端连接进来时socket接收客户端连接请求,将客户端连接加入到了监听列表中(rlist),客户端发送消息的时候这个连接将触发
# 所以判断是否是客户端连接对象触发
try:
data=sock.recv(1024)
#没有数据的时候,我们将这个连接关闭掉,并从监听列表中移除
if not data:
sock.close()
rlist.remove(sock)
continue
print("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 *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8093))
while True:
msg=input('>>: ').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
data=client.recv(1024)
print(data.decode('utf-8'))
client.close()
异步 I/O(asynchronous IO)
异步 I/O(asynchronous IO)用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从系统内核kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
python在copy数据这个阶段没有提供操纵操作系统的接口,所以用python没法实现这套异步IO机制,其他几个IO模型都没有解决第二阶段的阻塞(用户态和内核态之间copy数据),但是C语言是可以实现的。但是python仍然有异步的模块和框架(tornado、twstied,高并发需求的时候用)。
可能会需要知道的知识补充
(有些补充原稿中可能需要用到的知识,但现稿用不到)
IO
I/O是input/output的意思,就是输入输出操作。比较常见的IO,disk IO, net IO, std IO,等,甚至打印机这类的通过串、并、USB外联都算作IO。常用的处理方式往往是轮询。对于一般异步编程问题就是合理地利用了这个轮询之间的间隔去处理其他主要任务,等待IO搞定之后再处理后续任务。
文件描述符fd
一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数,是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
缓存 I/O
数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
用户空间与内核空间
为了保证用户进程不能直接操作内核(kernel)【操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限】,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。供内核使用,称为内核空间;供各个进程使用,称为用户空间。
进程切换
进程切换【很耗资源】 为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。
进程的阻塞
进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
事件驱动
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。传统的编程是线性的,它的流程大致如下:开始—>代码块A—>代码块B—>代码块C—>代码块D—>…—>结束。对于事件驱动型程序模型,它的流程大致如下:开始—>初始化—>等待
同步:提交一个任务之后要等待这个任务执行完毕
异步:只管提交任务,不等待这个任务执行完毕就可以去做其他的事情
阻塞:recv、recvfrom、accept,线程阶段 运行状态–>阻塞状态–>就绪
非阻塞:没有阻塞状态
参考博客:python之IO多路复用 - cls超 - 博客园 (cnblogs.com)