IO模型
文章目录
1 到底什么是IO?
计算机角度的IO
比较直观的例子是 计算机的输入输出。
其中计算机组成原理中的冯-诺伊曼结构,就是将计算机分成5个部分:运算器、控制器、存储器、输入设备、输出设备。
涉及计算机核心与其他设备间数据迁移的过程,就是IO。
如磁盘IO,就是将磁盘读取数据到内存,这算一次输入,对应的,从内存中的数据写入到磁盘,就是一次输出。这就是IO的本质。
操作系统的IO
我们要将内存中的数据写入到磁盘中,主体会是什么呢?
主体可能是一个Java程序,假设网络传来的二进制流,一个Java进程可以把它写入磁盘。
操作系统负责计算机的资源管理和进程调度。
我们运行的应用程序,其实需要操作系统,才能做的一些特殊操作,如磁盘文件读写、内存的读写等等。
因为这些危险的操作不可以由应用程序乱来,只能交给底层操作系统来操作,我们直接调用操作系统开发的API就行了。
- 什么是用户空间?什么是内核空间?
- 以32位操作系统为例,它为每一个进程都分配了4G(2的32次方)的内存空间。这4G可访问的内存空间分为二部分,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。
我们的应用程序是跑在用户空间中的,不存在实质的IO过程,真正的IO是在操作系统内核执行的。
即应用程序IO操作分为:IO调用和IO执行。
2 操作系统中的一次IO过程
应用程序发起的一次IO操作包含两个阶段:
- IO调用:应用程序进程向操作系统内核发起调用。
- IO执行:操作系统内核完成相应的IO操作。
而内核完成一次IO执行操作还包括三个过程:
- 准备数据阶段:内核等待I/O设备准备好已经写入内核缓冲区中的数据
- 拷贝数据阶段:将数据从内核缓冲区拷贝到用户进程缓冲区
接下来是经典的5中IO模型梳理
3 阻塞IO模型
假设应用程序的进程发起IO调用,但是如果内核还没有准备好的话,那么应用程序进程就会一直 阻塞等待 。一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,应用程序才会解除 Block 状态,这次IO操作就是 阻塞IO 。
阻塞IO的经典应用是 阻塞Socket、Java BIO。
阻塞IO的缺点就是:如果内核数据一直没准备好,那么用户进程就会一直阻塞交出CPU,浪费性能,可以使用非阻塞IO优化。
4 非阻塞IO模型
如果内核数据没有准备好,那么可以先返回错误信息给用户进程,让它需要等待,而是通过轮询的方式再来请求,这就是非阻塞IO。
非阻塞IO流程:
- 应用程序向操作系统内核,发起 recvfrom 读取数据
- 此时OS内核还没有准备好数据,就会立即发送一个EWOULDBLOCK错误码
- 应用程序进程轮询调用,就会继续向内核发出recvfrom读取数据
- 此时内核的数据已经准备好了,就从内核缓冲区的数据拷贝到用户空间。
- 完成调用,返回成功提示。
缺点:看似是非阻塞了,其实本质上仍然是阻塞的,会一直占用CPU,浪费CPU资源。
5 IO多路复用模型
既然 NIO 无效的轮询会导致CPU资源消耗,我们等到内核数据准备好了,主动通知应用进程再去系统调用,那不就好了嘛?
此时,我们需要了解一下,文件描述符fd(File Descriptor),形式上是一个非负整数,当应用程序打开一个现有文件或者创建一个新的文件,内核就会向进程返回一个文件描述符。
多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当 socket 真正有读写事件发生才会占用资源来进行实际的读写操作。
IO复用模型的核心思路:系统会给我们一类函数(select、poll、epoll函数),它们可以监控多个 fd 操作,任何一个返回内核数据就绪,应用程序再发起recvfrom系统调用。
IO多路复用之select
应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回一个可读状态,这时应用进程再发起recvfrom请求来读取数据。
NIO中,需要N次轮询系统调用,然而借助了select的IO多路复用模型,只需要发起一次系统调用就可以了,大大优化了性能。
另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,
而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。
但是,select还是有几个缺点:
-
监听IO的最大连接数有限,在Linux系统上一般为1024。
-
select函数返回后,是通过遍历fdset,找到就绪的描述符fd。
(仅知道有I/O事件发生,却不知是哪个socket,因此需要遍历所有的socket)
-
一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
因为select有最大连接数的限制,所以之后提出了poll。
与select相比,poll解决了 连接数限制的问题。但是呢,select和poll一样都是要去遍历socket的fd来获取就绪的socket。
如果同时连接大量的客户端,在一时刻可能只有极少数的处于就绪状态,但伴随着监视的fd数量增长,效率就会大大下降。
之后,epoll 就诞生了。
6 IO多路复用之epoll
为了解决select/poll存在的问题,就出现了epoll,它是采用事件驱动来实现的。
epoll先通过epoll_ctl()来注册一个fd,一旦基于fd就绪时,内核就会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历fd的操作,而是监听事件回调机制,这就是epoll的亮点。
select、poll、epoll的区别
select | poll | epoll | |
---|---|---|---|
底层数据结构 | 数组 | 链表 | 红黑树和双链表 |
获取就绪的fd | 遍历 | 遍历 | 事件回调 |
事件复杂度 | O(n) | O(n) | O(1) |
最大连接数 | 1024 | 无限制 | 无限制 |
fd数据拷贝 | 每次调用select,需要将fd数据从用户空间拷贝到内核空间 | 每次调用poll,需要将fd数据从用户空间拷贝到内核空间 | 使用内存映射(mmap),不需要从用户空间频繁拷贝fd数据到内核空间 |
epoll明显优化了IO的执行效率,但在进程调用epoll_wait()时,仍然可能被阻塞。
因为我们可以假设进程无需时不时去询问数据是否准备就绪,等进程发出请求后,内核数据准备好了通知进程就行了,就诞生了信号驱动IO模型。
7 IO模型之信号驱动模型
信号驱动IO就不再主动询问内核数据是否准备就绪,而是向内核发出一个信号(调用 sigaction 时候建立一个 SIGIO 的信号),然后应用用户进程可以去做别的事,不用阻塞。
当内核数据就绪时,在通过SIGIO信号通知应用进程,数据准备好的可读状态。当应用用户进程收到信号之后,立即调用recvfrom,去读取数据。
信号驱动IO模型,在应用程序发出信号时,是立即返回的,不会阻塞用户进程的。
看似其中已经是异步操作了,但仔细一看的话,可以看出,数据复制到用户缓冲区的期间,应用进程还是阻塞的。
回头来看,不管是BIO,还是NIO,还是信号驱动,在数据从内核复制到应用缓冲区的时候,都是阻塞的。
因此就引出来真正的异步大BossIO——AIO!
8 IO模型终结者之异步IO(AIO)
前面提到的BIO、NIO和信号驱动,在数据从内核到用户缓冲的过程都是阻塞的,因此都不算是真正的异步。
AIO实现了IO全流程的非阻塞,应用程序发出系统调用后,立即返回不是处理的结果,而是提交成功的结果。
等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号后通知用户进程IO操作执行完毕,用户就可以直接使用数据了。
异步IO的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景:
比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。
阻塞、非阻塞、同步、异步IO汇总
IO模型 | |
---|---|
阻塞I/O模型 | 同步阻塞 |
非阻塞I/O模型 | 同步非阻塞 |
I/O多路复用模型 | 同步阻塞 |
信号驱动I/O模型 | 同步非阻塞 |
异步IO(AIO)模型 | 异步非阻塞 |