编程过程中经常会遇到 IO,而且各种技术框架底层也是各种IO的应用。这里简单整理下。
IO类型
参考《UNIX网络编程》,IO一共分五种类型
-
阻塞IO(bloking IO)
-
同步非阻塞IO(synchronous non-blocking IO)
-
信号驱动式IO(signal-driven IO)
-
多路复用IO(multiplexing IO)
-
异步IO(asynchronous IO)
前四种为同步IO,最后一种为异步IO。
概念
这里简单介绍几个概念,具体内容可以学习《深入理解计算机系统》一书。
用户空间
内核空间
进程切换
进程阻塞
缓存 IO
操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中。
整个过程:
一、硬盘上的数据会先被拷贝到操作系统内核的缓冲区中;
二、然后再从操作系统内核的缓冲区拷贝到应用程序的地址空间。
前者是数据准备,后者是数据复制。两个步骤不同的实现方式组合,就构成了上边说的五种IO类型。
(值得一说 kafka 中用的零拷贝就是省略了第二步,直接将OS内核缓冲区的数据发送给网卡)
IO类型
下边来一一介绍。
阻塞IO
即最简单的IO,从名字就可以知道它直接阻塞用户进程直到系统返回数据。
也就是说,两个步骤完成之前,用户进程都是阻塞的。
如图:
优点:数据可用立刻返回,没有延迟;调用简单;
缺点:整个过程,用户进程都处于阻塞等待状态。
同步IO
也就是同步非阻塞IO,是一种轮询(polling)方式。
在第一步,用户进程每次read 时,都是立即直接返回 EAGAIN 或 EWOULDBLOCK 错误,直到操作系统缓存中准备好。
在第二步,还是会阻塞,直到数据从操作系统内核缓冲区复制到用户程序内存空间。
如图:
这种方式下,用户进程定期轮询,每次轮询都相当于一个小的阻塞(尤其是网络请求)。而在轮询间隔期间可以做其他的事情。
信号驱动式IO
所谓信号驱动,其实就是提前向内核注册,然后收到通知后回调处理函数。
一、用户进程预先告知内核、并向内核注册一个信号处理函数,然后用户进程返回。这一步骤不阻塞。 二、当内核数据就绪时会发送一个信号给进程,用户进程便在信号处理函数中调用IO读取数据到用户空间。这一步还是阻塞的。
注意:信号驱动IO并不是真正的异步,因为在第二步还是需要用户进程进行IO操作。
缺点:需要一个注册回调机制,开发实现成本高。
如图:
多路复用
因为Java NIO、Ngnix、NodeJS、Redis 等大火的原因,多路复用可以说众所周知。平时所说的select,poll,epoll就是它。
简单来说就是利用 selector 实现一个进程即 select()
函数循环监听一个文件描述符集合,当某个文件描述符就绪,则对其进行处理。
如图:
在这两个过程中,select
只负责等待,recvfrom
负责从操作内核空间拷贝数据。
从这点来说多路复用跟阻塞IO很像,但因为selector
可以对多个文件描述符进行阻塞监听,所以效率比阻塞IO的高。
异步IO
顾名思义,IO过程是异步的
如图:
一、用户进程发起 aio_read
调用并给内核传递描述符、缓冲区指针、缓冲区大小三个参数,然后系统调用立刻返回。因此用户进程可以继续工作。 二、当系统收到一个aio_read调用后立刻返回,之后内核等待数据准备完成,并将数据拷贝到用户内存。 三、之后系统内核会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程。
异步IO有点类似信号驱动IO,区别在于
-
信号驱动IO是由内核通知我们何时可以启动一个IO操作
-
异步IO模型是由内核告知我们IO操作何时完
IO对比,图片来源于书
综合以上
阻塞程度:阻塞IO > 同步非阻塞IO > 多路复用IO > 信号驱动IO > 异步IO,效率是由低到高的。
发展过程:
更多扩展
Reactor模式
是一种处理一个或多个客户端并发交付服务请求的事件设计模式。当请求抵达后,服务处理程序使用I/O多路复用策略,然后同步地派发这些请求至相关的请求处理程序。
具体角色
-
初始事件分发器(Initialization Dispatcher)
-
同步(多路)事件分离器(Synchronous Event Demultiplexer)
-
系统处理程序(Handles)
-
事件处理器(Event Handler)
多路复用函数
在多路复用中,涉及到了 select,poll,epoll 函数。因此本质上都是同步I/O。
select
poll
epoll