IO模型有哪些?
IO 即输入输出,根据冯诺依曼体系结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备
站在我们程序员的角度来看,首先要明白一些概念:
IO就是计算机系统与外部设备之间通信的过程,比如说把读写文件,一行一行遍历。
站在计算机操作系统的角度:
其实IO是分为 用户空间(User space) 和 内核空间(Kernel space ) 的,我们在程序发起IO操作,其实操作系统是需要从用户空间(程序)进入到内核空间(操作系统指令),这样才能访问文件。
操作系统内核完成IO操作还包括两个过程:
- 准备数据阶段:内核等待I/O设备准备好数据
- 拷贝数据阶段:将数据从内核缓冲区拷贝到用户进程缓冲区
文件IO: 读取:需要将操作系统内核空间将数据准备好拷贝给应用程序的用户空间 写入:需要将应用程序的用户空间将数据准备好拷贝给操作系统内核空间
网络IO: 接收网络请求:网络--》网卡--》内核空间--》用户空间 发送网络请求:用户空间--》内核空间--》网卡--》网络
IO的模型一般分为四种:
- ①同步阻塞IO(Blocking IO):即BIO,传统的IO模型。
- ②同步非阻塞IO(Non-blocking IO), 即NIO,默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。
- ③多路复用IO(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型(Redis单线程为什么速度还那么快,就是因为用了多路复用IO和缓存操作的原因)
- ④异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。
# 1、BIO:
我们目前使用较多的也是这种BIO,比如说InputStream的wirte和read,都是同步阻塞的,只有等待文件读写完毕,我们才能进行下一步。
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
# 2、NIO
用户发出请求,内核数据如果没有准备好,先返回错误的信息给用户,让它不需要等待,用户通过轮询的方式来请求。
频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源,但相对BIO性能有很大的提升
著名的 Netty 就是使用NIO。
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上;多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
# 3、多路复用IO
既然NIO是每次都轮询,那么是不是可以等内核数据准备好了,主动通知用户进程再去调用,这就完美解决了。这种就是多路复用IO的思想。
服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理(复用同一个线程)
Redis就是这种多路复用IO
⏬
拓展:
所谓的I/O复用,就是多个I/O可以复用一个进程。I/O多路复用允许进程同时检查多个fd,以找出其中可执行I/O操作的fd。 系统调用select()和poll()来执行I/O多路复用。在Linux2.6中引入的epoll()是select()的升级版,提供了更高的性能。通过I/O复用,我们可以在一个进程处理大量的并发I/O。
文件描述符fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起
recvfrom
系统调用。
# 初级版I/O复用
比如一个进程接受了10000个连接,这个进程每次从头到尾的问一遍这10000个连接:“有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍。”然后进程就一直从头到尾问这10000个连接,如果这10000个连接都没有I/O事件,就会造成CPU的空转,并且效率也很低,不好不好。
那么,如果发明一个代理,每次能够知道哪个连接有了I/O流事件,不就可以避免无意义的空转了吗?为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。
# 升级版I/O复用
# select()
select可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。
while true {
select(streams[])
for i in streams[] {
if i has data
read until unavailable
}
}
但select有几个缺点:
select()采用轮询的方式来检查fd是否就绪,当fd数量较多时,性能欠佳。因为从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。
监听的IO最大连接数有限,在Linux系统上一般为1024。
因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的
socket
。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。
# 生活实例
小明家楼下有一个收发室,每次有快递到了就先代收,但收发室也不知道那个是小明的快递;但小明去取的时候,要查询所有代收的快递。
# 高级版I/O复用
# epoll()
epoll能更高效的检查大量fd,UNIX中提供了类似功能的kqueue调用,它采用事件驱动来实现。
epoll可以理解为event poll,不同于忙轮询和无差别轮询,当连接有I/O流事件产生的时候,epoll就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个事件。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1)的)
epoll先通过epoll_ctl()
来注册一个fd
(文件描述符),一旦基于某个fd
就绪时,内核会采用回调机制,迅速激活这个fd
,当进程调用epoll_wait()
时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是epoll的亮点。
# 生活实例
小明家楼下有一个收发室,每次有快递到了,就先代收并做了标记;然后通知小明去取送给小明的快递。(精准打击)
select | poll | epoll | |
---|---|---|---|
底层数据结构 | 数组 | 链表 | 红黑树和双链表 |
获取就绪的fd | 遍历 | 遍历 | 事件回调 |
事件复杂度 | O(n) | O(n) | O(1) |
最大连接数 | 1024 | 无限制 | 无限制 |
fd数据拷贝 | 每次调用select,需要将fd数据从用户空间拷贝到内核空间 | 每次调用poll,需要将fd数据从用户空间拷贝到内核空间 | 使用内存映射(mmap),不需要从用户空间频繁拷贝fd数据到内核空间 |
epoll明显优化了IO的执行效率,但在进程调用epoll_wait()
时,仍然可能被阻塞。能不能酱紫:不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动IO模型。
三者数据结构:
select是数组 poll链表 epoll红黑树+链表
# 信号驱动IO模型
信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用sigaction
的时候建立一个SIGIO
的信号),然后应用用户进程可以去做别的事,不用阻塞。
当内核数据准备好后,再通过SIGIO
信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom
,去读取数据。
数据从内核区复制到缓冲区的时候,应用进程还是阻塞的(BIO、NIO、IO多路复用 都是如此),它要读取数据,要经过两个阶段的请求:
- 第一次发送select请求,询问数据状态是否准备好(是立即返回的,不会阻塞进程)
- 第二次发送recevform请求读取数据(数据从内核区复制到缓冲区的时候会阻塞)
那这两个操作能不能一次完成呢?答案是 AIO 可以做到。
# 4、AIO
又叫异步IO,我只要发送一个请求我告诉内核我要读取数据,然后我就什么都不管了,然后内核去帮我去完成剩下的所有事情。
Nginx采用了异步非阻塞的方式工作。
信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核通知我们操作什么时候完成。
异步IO的优化思路是解决了应用程序需要先后发送询问请求、发送接收数据请求两个阶段的模式,在异步IO的模式下,只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作。
# 活实例
阻塞取快递:小明收到快递即将送达的信息后,什么事都不做,一直专门等快递。
非阻塞取快递:小明收到快递即将送达的信息后,等快递的时候,还一边敲代码、一边刷微信。
1. 同步阻塞:小明收到信息后,啥都不能干,下楼干等快递;(BIO)
2. 同步非阻塞:小明收到信息后,边刷微博,刷着刷着需要不定时主动打开看看短信快递到了没;(NIO)
3. 异步阻塞:小明收到信息后,啥都不干,一直等着快递员通知他取快递;
4. 异步非阻塞:小明收到信息后,边刷着微博,边等快递员通知他取快递。(AIO)
# 总结
同步与异步,重点在于消息通知的方式;阻塞与非阻塞,重点在于等消息时候的行为。
阻塞非阻塞:
简单来说就是 是否需要等待
阻塞调用是 指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用 指在不能立刻得到结果之前,该调用不会阻塞当前线程。
同步异步:
我个人觉得区别是是否需要请求方是否都需要自己全程参与
同步:用户请求到结束,都要自己参与,比如用户进程触发IO操作并等待或者轮询去看IO操作是否就绪
异步:用户请求后,就去做其他事情,内核处理好数据再通知我就行了
阻塞 | 非阻塞 | |
---|---|---|
同步 | BIO | NIO |
同步 | IO多路复用 | 信号驱动IO模型 |
异步 | AIO |
参考:
- 公众号《田螺的小男孩》
- 知乎,忘记链接了~
- 小明例子:https://blog.csdn.net/yournevermore/article/details/101029630 (opens new window)
- 100%弄明白5种IO模型:https://zhuanlan.zhihu.com/p/115912936