NIO 看破也说破(三)—— 不同的IO模型

586 阅读4分钟

上两节我们提到了select 和 poll函数,查看man手册:


SELECT(2)                   Linux Programmer's Manual                     SELECT(2)

NAME
       select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
POLL(2)                  Linux Programmer's Manual                         POLL(2)

NAME
       poll, ppoll - wait for some event on a file descriptor

SYNOPSIS
       #include <poll.h>

synchronous I/O multiplexing中文解释是同步的多路复用,因此select 是一个同步的I/O多路复用模式。Unix共五种I/O模型:

  • 阻塞I/O
  • 非阻塞I/O
  • I/O多路复用
  • 信号驱动
  • 异步I/O

信号驱动和真正的异步I/O并不常用,我们重点说一下前三个。

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成

阻塞I/O

ServerSocket server = new ServerSocket(8080);
while (true) {
  Socket socket = server.accept();
  System.out.println("链接端口:" + socket.getPort());
  InputStream inputStream = socket.getInputStream();
  BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
  String str = null;
  while ((str = reader.readLine()) != null) {
    System.out.println("接受:" + str);
    socket.getOutputStream().write("ok\n".getBytes());
    socket.getOutputStream().flush();
    if ("over".equals(str)) {
      System.out.println("要关闭了");
      socket.close();
      break;
    }
  }
  System.out.println("===========");
}

当有数据获取时,用户线程要释放cpu,直到数据由内核处理完成,整个过程用户线程是阻塞的。

用户线程调用内核IO操作,需要等IO彻底完成后才返回到用户空间,因此是阻塞IO

非阻塞I/O

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
serverSocketChannel.configureBlocking(false);
while (true) {
  SocketChannel socketChannel = serverSocketChannel.accept();
  if (socketChannel == null) {
    System.out.println("没有链接 ");
    continue;
  }
  System.out.printf("新链接,端口是 %s", ((InetSocketAddress) socketChannel.
                                   getRemoteAddress()).getPort());
  ByteBuffer ds = ByteBuffer.allocate(10);
  socketChannel.read(ds);
  System.out.println("接受数据");
}

IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成,因此是非阻塞的。(用户线程不因为I/O还未到达,一直傻傻的等待,而是需要不断去问内核是否有可用的数据。在准备就绪的情况下获取数据。

补充一个gif(刷新查看):

I/O多路复用

非阻塞IO中需要用户线程在每个IO通路上,各自不断轮询IO状态,来判断是否有可处理的数据。如果把一个连接的可读可写事件剥离出来,使用单独的线程来对其进行管理。多个IO通路,都复用这个管理器来管理socket状态,这个叫I/O多路复用。

多路复用在内核中提供了select,poll,epoll三种方式:

select

原理示意

特点

只能处理有限(不同系统参数:1024/2048)个socket

select监控socket时不能准确告诉用户是哪个socket有可用数据,需要轮询判断

poll

原理示意

特点

与select没区别

采用链表实现,取消了文件个数的限制

epoll

原理示意

epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了

fd 上的事件发生时,与它对应的回调函数就会被调用把fd 加入链表,其他处于“空闲的”状态的则不会被加入

epoll从上面链表中获取有事件发生的fd

epoll准确的表述应该是I/O事件通知器:

EPOLL(7)               Linux Programmer's Manual                EPOLL(7)

NAME
       epoll - I/O event notification facility

SYNOPSIS
       #include <sys/epoll.h>

特点

没有最大连接限制

可以直接告诉用户程序哪一个,哪个连接有数据了

epoll详细的介绍可以参考我的专辑 mp.weixin.qq.com/mp/homepage… 中的第二篇文章《读懂才会用:Redis源码系列》

同步异步的概念

同步和异步的概念描述的是用户线程与内核的交互方式:

同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;异步是指用户线程发起IO请求 后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

因此 阻塞I/O,非阻塞I/O,I/O多路复用,都属于同步调用。只有实现了特殊API的AIO才是异步调用,之后单开一篇讲解。

AIO(7)                         Linux Programmer's Manual               AIO(7)

NAME
       aio - POSIX asynchronous I/O overview

DESCRIPTION
       The  POSIX  asynchronous  I/O  (AIO)  interface  allows applications to initiate one or more I/O operations that are performed asyn‐
       chronously (i.e., in the background).  The application can elect to be notified of completion of the I/O operation in a  variety  of
       ways: by delivery of a signal, by instantiation of a thread, or no notification at all.

系列

NIO 看破也说破(一)—— Linux/IO 基础

NIO 看破也说破(二)—— Java 中的两种BIO

NIO 看破也说破(三)—— 不同的IO模型

NIO看破也说破(四)—— Java的NIO

NIO 看破也说破(五): 搞,今天就搞,搞懂Buffer

关注我

如果您在微信阅读,请您点击链接 关注我 ,如果您在 PC 上阅读请扫码关注我,欢迎与我交流随时指出错误,公众号【小眼睛聊技术】

版权声明: 本文为作者【小眼睛】的原创文章,文章转载请联系作者,发表于 InfoQ 和公众号。