NIO笔记(一)之IO模型

NIO 简介

  1. 传统I/O库与NIO最重要的区别是数据的打包和传输的方式,传统的I/O以流的方式处理数据,一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。NIO以块的方式处理数据,每一个操作都在一步中产生或消费一个数据块,按块处理数据比按字节流处理数据要快的多。BIO是面向流的,每次从流中读取一个或多个字节,直到读取完所有的字节,没有缓存在任何地方,流不能前后移动流中的数据,如需前后移动处理,需要先将其缓存至一个缓冲区。Java NIO面向缓冲,数据会被读取到一个缓冲区,需要时可以在缓冲区中前后移动处理,这增加了处理过程的灵活性。但与此同时在处理缓冲区前需要检查该缓冲区中是否包含有所需要处理的数据,并需要确保更多数据读入缓冲区时,不会覆盖缓冲区内尚未处理的数据。
  2. NIO采用内存映射文件的方式来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了

I/O内存缓冲区

  1. 用户空间:常规进程所在区域,JVM就是常规进程,该区域执行的代码不能直接访问硬件设备
  2. 内核空间:操作系统所在区域。内核代码它能与设备控制器通讯,控制着用户区域进程的运行状态等等。最重要的是,所有的I/O都直接或间接通过内核空间
  3. 数据交互:当用户(java)进程进行I/O操作的时候,它会执行一个系统调用将控制权移交给内核,内核代码负责找到请求的数据,并将数据传送到用户空间内的指定缓冲区
  4. 内核空间缓冲区:内核代码读写数据要通过磁盘的I/O操作,由于磁盘I/O操作的速度比直接访问内存慢了好几个数量级,所以内核代码会对数据进行高速缓存或预读取到内核空间的缓冲区(减少磁盘I/O,提高性能)
  5. 用户空间缓冲区:同上,java I/O进程通过系统调用读写数据的速度,要比直接访问虚拟机内存慢好几个数量级,所以可以执行一次系统调用时,预读取大量数据,缓存在虚拟机内存中。
  6. DMA(Direct Memory Access 直接内存访问):是一种内存访问技术,它允许某些计算机内部的硬件子系统(计算机外设),可以独立地直接读写系统内存,而不需CPU介入处理,在同等程度的处理器负担下,DMA是一种快速的数据传送方式。
  7. 磁盘控制器:硬盘控制器即磁盘驱动器适配器,是计算机与磁盘驱动器的接口设备,它接收并解释计算机来的命令,向磁盘驱动器发出各种控制信号。
  8. 关系图
    NIO笔记(一)之IO模型
  9. 表面上看,把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题
    • 首先,硬件通常不能直接访问用户空间
    • 其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色

IO模型

阻塞I/O

  1. 阻塞I/O(blocking I/O)模型是最流行的I/O模型,默认情况下,所有套接字和文件描述符就是阻塞的。阻塞I/O将使请求进程阻塞,直到请求完成或出错
  2. BIO的阶段
    • 等待数据就绪。例如套接字有数据到来,文件描述符可读。
    • 将数据从内核空间拷贝到用户空间。
      NIO笔记(一)之IO模型

非阻塞I/O

  1. 非阻塞I/O(nonblocking I/O):如果I/O操作会导致请求进程睡眠,则不要把它挂起,而是返回一个错误告诉它。相比于BIO,NIO的第一个阶段不会阻塞,相反是一直在对非阻塞描述符调用read 或 recvfrom 等操作。当一个应用进程像这样对一个非阻塞描述符调用 recvfrom 时,我们称之为轮询
  2. 应用进程持续轮询内核,以查看某个操作是否就绪。这样做往往消费大量CPU时间,不过这种模型偶尔也会遇到。在嵌入式开发中,非阻塞I/O模型比较常见。比如在编写设备驱动的时候,常常会短暂地轮询设备状态寄存器,以等待设备就绪。
    NIO笔记(一)之IO模型

IO复用

  1. I/O 多路复用(I/O multiplexing)会用到 select 或者 poll 函数,这两个函数也会使进程阻塞,但是和阻塞 I/O 所不同的的,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。Java NIO就是这种模型
    NIO笔记(一)之IO模型

信号驱动式I/O

  1. 信号驱动 I/O(signal-driver I/O)使用信号,让内核在描述符就绪时发送 SIGIO 信号通知我们进行处理。实际上这个处理是自动的,只不过我们可以指定这个处理函数
  2. 这种方式要求进程执行以下3个步骤
    • 建立 SIGIO 信号的信号处理函数。
    • 设置该套接字的属主,通常使用 fcntl 的 F_SETOWN 命令设置。
    • 开启套接字的信号驱动 I/O 功能,通常通过使用 fcntl 的 F_SETFL 命令打开 O_ASYNC 标志完成。也可以改用 ioctl 的 FIOASYNC 请求。
  3. 信号驱动 I/O 模型的优势在于等待数据到达期间进程不被阻塞,进程可以继续执行。相比于非阻塞 I/O,信号驱动 I/O 没有询轮带来的昂贵的 CPU 代价。
    NIO笔记(一)之IO模型

异步I/O

  1. 异步 I/O(asynchronous I/O)由 POSIX 规范定义,包含一系列以 aio 开头的接口。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核空间拷贝到用户空间)完成后通知我们。这种模型与信号驱动模型的主要区别是:信号驱动 I/O 是由内核通知我们何时可以启动一个 I/O 操作,而异步 I/O 模型是由内核通知我们 I/O 操作何时完成。
    NIO笔记(一)之IO模型

总结

NIO笔记(一)之IO模型

Scalable IO in Java

  1. Scalable IO in Java下载
  2. 所有的网络处理程序都有以下的处理过程
    • Read request
    • Decode request
    • Process service
    • Encode reply
    • Send reply

Classic Service Designs

  1. 接到一个新的连接建立请求后,会马上放到一个新的线程去处理,在input.readLine()处阻塞,线程调度器会充当select的工作(即发现哪个Socket有事件)
    NIO笔记(一)之IO模型

  2. 缺点

    • 连接数太多的时候,线程也会增多,系统压力增长太快,特别是长连接的情况下或者客户端网络环境很差,每次不能全部发送数据,而是部分发送,这时候一个连接(线程)会被占用较长的时间。
    • java线程机制本身占用内存(在linux 64位系统上每个线程占1M内存)
    • 太多线程造成上下文切换的开销
  3. 案例代码

        public class Server {
           public static void main(String[] args) {
               new Server().start();
           }
           public void start() {
               try {
                   ServerSocket serverSocket = new ServerSocket(8888);
                   while (true) {
                       Socket socket = serverSocket.accept();
                       //一个线程、多个线程或者线程池
                       new Thread(new Handler(socket)).start();
                   }
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
       
       
           static class Handler implements Runnable {
               final Socket socket;
       
               private static final Logger logger = LoggerFactory.getLogger(Handler.class);
       
               Handler(Socket s) {
                   socket = s;
               }
       
               public void run() {
                   try {
                       BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                       PrintWriter out = new PrintWriter(socket.getOutputStream());
                       String input = null;
                       /**
                        * readLine()
                        * 1. 只有在数据流发生异常或者另一端被close()掉时,才会返回null值。
                        */
                       while ((input = br.readLine()) != null && input.length() > 0) {
                           logger.info(input);
                           if ("exit".equals(input)) {
                               socket.close();
                               return;
                           } else {
                               out.println(input);
                               out.flush();
                           }
                       }
                   } catch (IOException e) {
                       e.printStackTrace();
                   }
               }
           }
       }
    
    

NIO

  1. 随着并发数量的提高,传统nio框架采用一个Selector来支撑大量连接,管理和触发连接已经遇到瓶颈
  2. 在处理大量连接的情况下,多个Selector比单个Selector
  3. 多个Selector的情况下,处理OP_READOP_WRITESelector要与处理OP_ACCEPTSelector分离,也就是说处理接入应该要一个单独的Selector对象来处理,避免IO读写事件影响接入速度。
  4. Selector的数目问题,mina默认是cpu+2,而grizzly总共就2个,如果CPU个数超过8个,那么更多的Selector线程可能带来比较大的线程切换的开销。
    NIO笔记(一)之IO模型