IO模式详解

IO预备知识介绍

IO的两个阶段

  • 数据被拷贝到操作系统内核的缓冲区
  • 数据从操作系统内核中拷贝的用户内存

理解阻塞和非阻塞

  • 阻塞是一直block住对应的线程直至操作完成
  • 非阻塞是在数据准备时就会立即返回

理解同步和异步

  • 先来看看两者在POSIX上的概念:
    • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
    • An asynchronous I/O operation does not cause the requesting process to be blocked;
  • 这里需要关注的是IO operation,也就是真正的IO过程,也就是我们上述讲到的第二阶段。而我们下述前方法中的第二阶段都是阻塞的,所以都是同步的方法。只有最后一种是真正的异步IO

操作系统层面IO类别

  • 五种IO模型对比如下图所示:
  • IO模式详解

阻塞IO

  • 在IO的两个阶段都是阻塞的
  • IO模式详解

非阻塞IO

  • 在数据准备过程中不断地询问kernel数据是否准备完成
  • IO模式详解

IO多路复用

  • 有些地方也称其为(event driven IO)
  • IO模式详解
  • 多路复用转接只多了一个select函数。
  • 当用户进程调用select时,整个线程会被blocking
  • 他的基本原理就是poll()这个函数会不断的轮询所监控的所有socket是否准备完成,当某个socket有数据返回时,便会通知用户进程。
  • 在多路复用中,实际上,对于每一个socket来说,都是设置成non-blocking的。但是,如上图所示,整个用户进程是一直被block的。只不过进程是被select这个函数blocking,而不是socket IO

多路复用之select、poll、epoll详解(本质上都是同步IO)

select

  • int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
    • 其监视的描述符分为三类,readfds,writefds,exceptfds
    • 调用select后进程会阻塞,直至有描述符就绪,或者超时(timeout设置超时时间,如果立即返回设为null即可)
    • 函数返回后,可以通过遍历fds,来找到就绪的描述符
      • 事实上,同时连接大量的客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符增长,其效率也会下降。
  • 优点
    • 良好的跨平台支持(几乎支持所有的平台)
  • 缺点
    • 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024

poll

  • int poll (struct pollfd *fds, unsigned int nfds, int timeout)
    • pollfd包含了要监视的event和发生的event
    • 函数返回后,可以通过遍历fds,来找到就绪的描述符
      • 事实上,同时连接大量的客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符增长,其效率也会下降。
    • 没有最大数量限制,事实上,数量过大后性能也会下降

epoll

  • 是在2.6内核中提出的,是之前select和poll的增强版本。相对于这两者来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
  • 优点:
    • 监视的描述符数量不受限制
  • int epoll_create(int size);
    • 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
    • size只是对内核初始分配数据结构的一个建议
    • 当创建好epoll句柄时,他就会占用一个fd值,所以在使用完epoll时,必须调用close()关闭,否则有可能导致fd被耗尽
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    • 对指定fd进行op操作
  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    • 等待epfd上的IO事件,最多返回maxevents个事件
    • 参数events用来从内核得到事件的集合,maxevents告之内核这个集合有多大,这个值不能大于epoll_create时的size
工作模式
  • LT(level trigger)(默认模式)
    • 检测到事件描述符,将事件通知给应用程序,应用程序可以不立即处理该事件。当下次调用epoll_wait时,会再次响应应用程序并通知此事件。
    • 同时支持block socket和non-block socket
  • ET(edge trigger)(高速工作方式)
    • 这种情况下,当操作符从就绪变成了未就绪,内核通过epoll告诉你,应用程序必须立即处理该事件。如果不处理,当下次调用epoll_wait时,不会再次响应应用程序。
    • 很大程度上减少了epoll事件被重复触发的次数,因此效率比LT模式要高。
    • 在ET模式下,必须使用非阻塞套接字,以避免因为一个文件句柄的阻塞读/写操作把处理多个文件描述符的任务饿死。

信号驱动IO

  • signal driven IO
  • 并不常用
  • IO模式详解

异步IO

  • asynchronous IO,异步IO,无需自己负责读写,异步IO的实现会负责把数据从内核拷贝到用户空间。
  • 其实用的也很少
  • IO模式详解

Java层IO类别介绍

  • Java层IO为对操作系统层的封装
  • 使用者无需对操作系统

BIO

  • java下的io对应的是Blocking IO
    • socket.read()和socket.write()是同步阻塞的
  • 是一种同步阻塞IO

传统BIO

  • 同步阻塞IO
  • 线程的读取必须阻塞在一个线程内等待其完成
  • 一请求一应答模式
    • 但可以通过多线程来同时响应多个应答
    • 线程的创建和销毁会带来很多开销
    • 但在不同线程之间切换的成本也很高

伪异步IO

  • 为了处理传统BIO带来的线程阻塞问题,后来有人对它的线程模型进行了优化。后端使用线程池来维护线程。
  • 因此,资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

总结

  • 在活动连接数不是特别高的情况下,这种模型是很不错的。可以让每一个链接专注于自己的IO而且模型简单。

NIO

  • 是一种同步非阻塞的IO,是多路复用的基础
  • java下的nio对应的是Not-blocking IO
  • 在操作系统层面通过epoll来实现

NIO与IO的区别

  • IO流是阻塞的,NIO流是非阻塞的
  • IO面向流,NIO面向缓冲区。
    • 虽然IO流也有(xxxBuffer),但是只是流的包装类,还是从流读到缓冲区的
  • NIO通过channel进行读写
    • 通道只能和buffer进行交互,因为buffer,通道可以进行异步读写
  • NIO有选择器,而IO没有
    • 使用单个线程处理多个信道,线程之间的切换在操作系统之间是昂贵的。因此,为了提升效率,选择器是十分有用的
  • JDK原生的NIO让人诟病的地方:
    • JDK的NIO底层通过epoll实现,epoll让人诟病的空轮询会让CPU空转,利用率达到100%
    • 代码复杂,编程模型难。项目庞大之后,自行实现的NIO很容易出现各类bug,维护成本较高

AIO(Asynchronous I/O)

  • 其实就是NIO 2,在Java 7中引入的
  • 基于事件和回调机制实现的
  • 是一种异步非阻塞IO
  • 在操作系统层面通过epoll来实现
  • 目前使用还不是特别广泛,Netty之前也尝试过AIO,不过后来放弃了

参考资料