码迷,mamicode.com
首页 > 编程语言 > 详细

Java NIO

时间:2020-08-18 13:31:22      阅读:54      评论:0      收藏:0      [点我收藏+]

标签:方法   不能   使用   开始   sock   情况下   code   注意   inf   

1.1 基础知识

首先我们知道进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。
技术图片
这里数据的两次拷贝都需要时间,而这两端时间中进程和内核的状态不一样就产生了下面五种i/o模型:

  • 阻塞I/O
    对于内核而言,网络中的数据并不一定马上可读,所以内核要等到数据准备好了往内核读。然后再将读取完成的内核数据拷贝给用户。而这期间用户线程也是一直在等待读取数据,不能够做其他事。
    技术图片

  • 非阻塞I/O
    所以我们就想,能不能让用户线程不要那么傻,一直在哪里等。当用户发送时,内核缓存数据准备好没有给个准话,当时就直接返回信息0或者1.要是0的用户就做其他事,然后再来问。要是1的话就直接开始将数据拷贝到用户缓存区。
    技术图片

  • I/O复用
    在上面的模型中,我们针对的是一个process对应一个i/o。那如果用户进程调用了多个i/o呢?当然我们可以采用前面两种方式一个接一个的完成i/o操作。那能不能有一种方式对这多个i/o进行选择呢?比如谁准备好了就先调用谁?这里就是我们的i/o复用了。用户通过select这个函数不断轮询所有它负责的socket连接。这里需要注意的是,虽然第一阶段都是阻塞,但是阻塞式I/O如果要接收更多的连接,就必须创建更多的线程。I/O复用模式下在第一个阶段大量的连接统统都可以过来直接注册到Selector复用器上面,同时只要单个或者少量的线程来循环处理这些连接事件就可以了,一旦达到“就绪”的条件,就可以立即执行真正的I/O操作。而且并不是说性能方面复用就一定优于阻塞,前者只是更加适合于需要大量连接的场景。
    技术图片

  • 信号驱动的I/O
    首先用户进程建立SIGIO信号处理程序,并通过系统调用sigaction执行一个信号处理函数,这时用户进程便可以做其他的事了,一旦数据准备好,系统便为该进程生成一个SIGIO信号,去通知它数据已经准备好了,于是用户进程便调用recvfrom把数据从内核拷贝出来,并返回结果。
    技术图片

  • 异步I/O
    前面四种模型在最后一步真正的I/O操作是同步阻塞(拷贝数据)。但是异步I/O中当用户进程向内核发起某个操作后,会立刻得到返回,并把所有的任务都交给内核去完成(包括将数据从内核拷贝到用户自己的缓冲区),内核完成之后,只需返回一个信号告诉用户进程已经完成就可以了。
    技术图片

换种方式理解,最初的io模型中,关注的信息是->我要读写了。这种情况下,等待数据准备好和拷贝数据都阻塞,然后进一步的nio模型中,关注的信息是->我可以读写了。这种情况下socket主要的读、写、注册和接收函数在等待的时候都是非阻塞的,只是在最后的拷贝阶段是同步阻塞的。但性能非常高。然后最新的aio中,最后一步操作都是非阻塞的。完成后直接给线程一条数据准备完成的信号就可以了。

1.2零拷贝技术

CPU不执行数据从一个存储区域到另一个存储区域的拷贝任务,这通常用于在网络上传输文件时节省CPU周期和内存带宽。
首先我们需要知道的大多数文件系统的默认 IO 操作都是缓存 IO。也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。也就是说在一次网络传输过程中,数据大概会经历四次copy:
技术图片
其中还发生了多次上下文切换,这样确实加大了cpu的负担,我们可以怎样优化呢?

  • 如果说数据在这个过程并没有做任何修改->上述来回拷贝数据就是一种浪费
    采用mmap()代替read(),就是将内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用 write(),操作系统直接将内核缓冲区的内容拷贝到 socket缓冲区中,这一切都发生在内核态,最后, socket缓冲区再把数据发到网卡去:
  1. 首先(通过DMA)将数据从磁盘读取到kernel buffer中;
  2. 然后将kernel buffer拷贝到socket buffer中;
  3. 最后将socket buffer中的数据copy到网卡设备(protocol engine)中发送;
    技术图片
    很显然这样减少了一次拷贝
    当然还可以继续优化。我们还可以直接将数据直接由内存写入socket buffer(或者说将kernel buffer和socket buffer合并)这样就形成了真正的零拷贝。
    具体实现:
  4. 将文件拷贝到kernel buffer中;
  5. 向socket buffer中追加当前要发生的数据在kernel buffer中的位置和偏移量;
  6. 根据socket buffer中的位置和偏移量直接将kernel buffer的数据copy到网卡设备(protocol engine)中;

1.3NIO 直接缓冲区和非直接缓冲区

非直接缓冲区是直接通过拷贝的形式传递的,如从磁盘读取文件到物流空间,然后拷贝到jvm,再读取数据。直接缓冲区是通过物流内存映射文件直接传递的
正常情况下,JVM创建一个缓冲区的时候,实际上做了如下几件事:

  1. JVM确保Heap区域内的空间足够,如果不够则使用触发GC在内的方法获得空间;
  2. 获得空间之后会找一组堆内的连续地址分配数组, 这里需要注意的是,在物理内存上,这些字节是不一定连续的;
  3. 对于不涉及到IO的操作,这样的处理没有任何问题,但是当进行IO操作的时候就会出现一点性能问题.

所有的IO操作都需要操作系统进入内核态才行,而JVM进程属于用户态进程, 当JVM需要把一个缓冲区写到某个Channel或Socket的时候,需要切换到内核态.
而内核态由于并不知道JVM里面这个缓冲区存储在物理内存的什么地址,并且这些物理地址并不一定是连续的(或者说不一定是IO操作需要的块结构),所以在切换之前JVM需要把缓冲区复制到物理内存一块连续的内存上, 然后由内核去读取这块物理内存,整合成连续的、分块的内存.
也就是说如果我们这个时候用的是非直接缓存的话,我们还要进行“复制”这么一个操作,而当我们申请了一个直接缓存的话,因为他本是就是一大块连续地址,我们就可以直接在它上面进行IO操作,省去了“复制”这个步骤
当然缺点也是有的,他的分配和释放都比较昂贵,而且容易发生内存泄露。

Java NIO

标签:方法   不能   使用   开始   sock   情况下   code   注意   inf   

原文地址:https://www.cnblogs.com/jiezao/p/13511501.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!