Skip to content

IO

1. IO与NIO

IO与NIO区别:

  1. IO面向流,NIO面向缓冲区;
  2. IO阻塞,NIO非阻塞

NIO三大核心内容:

  • selector(选择器,用于监听channel)
  • channel(通道)
  • buffer(缓冲区)

2. 阻塞IO模型

解释: 假设应用程序的进程发起IO调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次IO操作,称之为阻塞IO。

应用: Java BIO、阻塞Socket

缺点: 如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞IO优化。

3. 非阻塞IO模型(NIO)

解释: 如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞IO

缺点: 存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。可以考虑IO复用模型

非阻塞IO的流程如下:

  1. 应用进程向操作系统内核,发起recvfrom读取数据。
  2. 操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。
  3. 应用程序轮询调用,继续向操作系统内核发起recvfrom读取数据。
  4. 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
  5. 完成调用,返回成功提示。

4. IO多路复用模型

背景: 既然NIO无效的轮询会导致CPU资源消耗,我们等到内核数据准备好了,【主动】通知应用进程再去进行系统调用

核心思路: 系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。

IO多路复用模型之select

应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。

非阻塞IO模型(NIO)中,需要N(N>=1)次轮询系统调用,然而借助select的IO多路复用模型,只需要发起一次系统调用就够了,大大优化了性能。

缺点:

  1. 监听的IO最大连接数有限,在Linux系统上一般为1024。因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。所以催生了epoll
  2. select函数返回后,是通过遍历fdset,找到就绪的描述符fd。(仅知道有I/O事件发生,却不知是哪几个流,所以需要遍历所有流)

IO多路复用模型之poll

采用事件驱动来实现。epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的的机制。这就是epoll的亮点。

缺点: epoll明显优化了IO的执行效率,但在进程调用epoll_wait()时,仍然可能被阻塞的。所以很自然的就可以这样思考:不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动IO模型

5. IO模型之信号驱动模型

信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用sigaction的时候建立一个SIGIO的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过SIGIO信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom,去读取数据。

问题: 信号驱动IO模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,不管是BIO,还是NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。还有没有优化方案呢?AIO(真正的异步IO)!

6. IO 模型之异步IO(AIO)

背景: 前面讲的BIO,NIO和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不是真正的异步。

AIO实现了IO全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。简单理解就是将数据提前复制到用户进程缓冲区,复制好了之后再发送通知

异步IO的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似的业务场景:比如发起一笔批量转账,但是转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。