netty源码分析:1.java.nio与零拷贝

Posted mask哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了netty源码分析:1.java.nio与零拷贝相关的知识,希望对你有一定的参考价值。

1.异步 同步 阻塞 非阻塞 区别:

阻塞和非阻塞指的是执行一个操作是等操作结束再返回,还是马上返回。

比如餐馆的服务员为用户点菜,当有用户点完菜后,服务员将菜单给后台厨师,此时有两种方式:

  • 第一种:就在出菜窗口等待,直到厨师炒完菜后将菜送到窗口,然后服务员再将菜送到用户手中;
  • 第二种:等一会再到窗口来问厨师,某个菜好了没?如果没有先处理其他事情,等会再去问一次;

第一种就是阻塞方式,第二种则是非阻塞的。

  同步和异步是事件本身的一个属性。还拿前面点菜为例,服务员直接跟厨师打交道,菜出来没出来,服务员直接指导,但只有当厨师将菜送到服务员手上,这个过程才算正常完成,这就是同步的事件。同样是点菜,有些餐馆有专门的传菜人员,当厨师炒好菜后,传菜员将菜送到传菜窗口,并通知服务员,这就变成异步的了。其实异步还可以分为两种:带通知的和不带通知的。前面说的那种属于带通知的。有些传菜员干活可能主动性不是很够,不会主动通知你,你就需要时不时的去关注一下状态。这种就是不带通知的异步。

对于同步的事件,你只能以阻塞的方式去做。而对于异步的事件,阻塞和非阻塞都是可以的。非阻塞又有两种方式:主动查询和被动接收消息。被动不意味着一定不好,在这里它恰恰是效率更高的,因为在主动查询里绝大部分的查询是在做无用功。对于带通知的异步事件,两者皆可。而对于不带通知的,则只能用主动查询。

  非阻塞和异步的概念:非阻塞只是意味着方法调用不阻塞,就是说作为服务员的你不用一直在窗口等,非阻塞的逻辑是"等可以读(写)了告诉你",但是完成读(写)工作的还是调用者(线程)服务员的你等菜到窗口了还是要你亲自去拿。而异步意味这你可以不用亲自去做读(写)这件事,你的工作让别人(别的线程)来做,你只需要发起调用,别人把工作做完以后,或许再通知你,它的逻辑是我做完了 告诉/不告诉 ,他和非阻塞的区别在于一个是"已经做完"另一个是"可以去做"

2. NIO
 

IO :操作数据是字节操作,流处理,每次创建一个线程

bio:基于字节和字符操作,流处理,提前创建一定的线程

nio:基于channel/buffer草,以块德的方式处理数据。selector(选择器)监听多个通道事件(连接请求、数据到达)

NOI中selector/channel/buffer关系:

1) 每个 channel 都会对应一个 Buffer

2) Selector 对应一个线程, 一个线程对应多个 channel(连接)

3) 该图反应了有三个 channel 注册到 该 selector //程序

4) 程序切换到哪个 channel 是有事件决定的, Event 就是一个重要的概念

5) Selector 会根据不同的事件,在各个通道上切换

6) Buffer 就是一个内存块 , 底层是有一个数组

7) 数据的读取写入是通过 Buffer, 这个和 BIO , BIO 中要么是输入流,或者是 输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写, 需要 flip 方法切换 channel 是双向的, 可以返回底层操作系统的情况, 比如 Linux , 底层的操作系统通道就是双向的.

单线程版

多线程版

3.零拷贝

java中零copy有 mmap(内存文件映射) 和 sendFile

传统文件读写过程:

       File file = new File("index.html");
        RandomAccessFile raf = new RandomAccessFile(file, "rw");

        byte[] arr = new byte[(int) file.length()];
        raf.read(arr);

    Socket socket = new ServerSocket(8080).accept();  
                  socket.getOutputStream().write(arr);

上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操。步骤:

  1. read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区
  2. 发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
  3. 发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
  4. 第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
  5. write 方法返回,再次从内核态切换到用户态。
     

制拷贝操作太多了。如何优化这些流程?

1) mmap优化:通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。如下图:

如上图,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 Socket 缓冲区。

2).sendFile优化: Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。

如上图,进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用,然后调一次write 方法时,从内核缓冲区进入到 Socket,是没有上下文切换的,因为在一个用户空间。

最后,数据从 Socket 缓冲区进入到协议栈。

此时,数据经过了 3 次拷贝,3 次上下文切换。

还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈

实际上,Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。如下图:

现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝第一次使用 DMA 引擎从文件拷贝到内核缓冲区第二次从内核缓冲区将数据拷贝到网络协议栈内核缓存区只会拷贝一些 offset 和 length 信息到 SocketBuffer,基本无消耗。

总结:

 1. java nio中文件传输的方式

 1).Memory-mapped files 内存映射文件的方式,通过缓存区访问文件
 2).Direct buffers直接缓冲区的方式,在合适的情况下可以使用零拷贝传输,但同时这会带来初始化与内存释放的问题(需要池化与主动释放);

2.零拷贝:
         
是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(
只有 kernel buffer 有一份数据,sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。

       零拷贝优势:1.)带来更少的数据复制;2.) 性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

3.mmap 和 sendFile 的区别:

  1. ) mmap 适合小数据量读写,sendFile 适合大文件传输。
  2. ) mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  3. )sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

   

  

以上是关于netty源码分析:1.java.nio与零拷贝的主要内容,如果未能解决你的问题,请参考以下文章

Netty基础系列 --堆外内存与零拷贝详解

Day467&468&469.JavaBIO&NIO编程&AIO&NIO与零拷贝&对比 -netty

netty里的ByteBuf扩容源码分析

Netty源码分析(七) PoolChunk

源码分析Netty4专栏

源码分析Netty4专栏