netty的零拷贝,和操作系统的零拷贝有什么区别
Netty零拷贝
零拷贝:所谓的零拷贝,并不是不需要经过数据拷贝,而是减少内存拷贝的次数
无零拷贝时,数据的发送流程
DMA(Direct Memory Access,直接内存存取)是现代大部分硬盘都支持的特性,DMA 接管了数据读写的工作,不需要 CPU 再参与 I/O 中断的处理,从而减轻了 CPU 的负担。
应用程序把磁盘数据发送到网络的过程中会发生4 次用户态和内核态之间的切换
,同时会有4 次数据拷贝
。过程如下:
- 应用进程向系统申请读磁盘的数据,这时候程序从用户态切换成内核态。
- 系统也就是 linux 系统得知要读数据会通知 DMA 模块要读数据,这时 DMA 从磁盘拉取数据写到系统内存中。
- 系统收到 DMA 拷贝的数据后把数据拷贝到应用内存中,同时把程序从内核态变为用户态。
- 应用内存拿到数据后,会把数据拷贝到系统的 Socket 缓存,然后程序从用户态切换为内核态。
- 系统再次调用 DMA 模块,DMA 模块把 Socket 缓存的数据拷贝到网卡,从而完成数据的发送,最后程序从内核态切换为用户态。
如何提升文件传输的效率?
我们程序的目的是把磁盘数据发送到网络中,所以数据在用户内存和系统内存直接的拷贝根本没有意义,与数据拷贝同时进行的用户态和内核态之间的切换也没有意义。而上述常规方法出现了 4 次用户态和内核态之间的切换,以及 4 次数据拷贝。我们优化的方向无非就是减少用户态和内核态之间的切换次数
,以及减少数据拷贝的次数
。
为什么要在用户态和内核态之间做切换?
因为用户态的进程没有访问磁盘上数据的权限,也没有把数据从网卡发送到网络的权限。只有内核态也就是操作系统才有操作硬件的权限,所以需要系统向用户进程提供相应的接口函数来实现数据的读写。
这里涉及了两个系统接口调用分别是:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
于是,零拷贝技术应运而生,系统为我们上层应用提供的零拷贝方法有下列两种:
- mmap + write
- sendfile
MMAP + write
这个方法主要是用 MMAP 替换了 read。
对应的系统方法为:
- buf = mmap(file,length)
- write(socket,buf,length)
所谓的 MMAP,其实就是系统内存某段空间和用户内存某段空间保持一致,也就是说应用程序能通过访问用户内存访问系统内存。所以,读取数据的时候,不用通过把系统内存的数据拷贝到用户内存中再读取,而是直接从用户内存读出,这样就减少了一次拷贝。
步骤:
- 应用进程通过接口调用系统接口 MMAP,并且进程从用户态切换为内核态。
- 系统收到 MMAP 的调用后用 DMA 把数据从磁盘拷贝到系统内存,这时是第 1 次数据拷贝。由于这段数据在系统内存和应用内存是共享的,数据自然就到了应用内存中,这时程序从内核态切换为用户态。
- 程序从应用内存得到数据后,会调用 write 系统接口,这时第 2 次拷贝开始,具体是把数据拷贝到 Socket 缓存,而且用户态切换为内核态。
- 系统通过 DMA 把数据从 Socket 缓存拷贝到网卡。
- 最后,进程从内核态切换为用户态。
这样做到收益是减少了一次拷贝
,但是用户态和内核态仍然是 4 次切换
。
sendfile
这个系统方法可以实现系统内部不同设备之间的拷贝。具体逻辑我们还是先上图:
使用 sendfile 主要的收益是避免了数据在应用内存和系统内存或 socket 缓存直接的拷贝,同时这样会避免用户态和内核态之间的切换。
基本原理分为下面几步:
- 应用进程调用系统接口 sendfile,进程从用户态切换完内核态。
- 系统接收到 sendfile 指令后,通过 DMA 从磁盘把数据拷贝到系统内存。
- 数据到了系统内存后,CPU 会把数据从系统内存拷贝到 socket 缓存中。
- 通过 DMA 拷贝到网卡中。
- 最后,进程从内核态切换为用户态。
但是,这还不是零拷贝,所谓的零拷贝不会在内存层面去拷贝数据,也就是系统内存拷贝到 socket 缓存,下面给大家介绍一下真正的零拷贝。
真正的零拷贝
真正的零拷贝是基于 sendfile,当网卡支持 SG-DMA 时,系统内存的数据可以直接拷贝到网卡。如果这样实现的话,执行流程就会更简单,如下图所示:
基本原理分为下面几步:
- 应用进程调用系统接口 sendfile,进程从用户态切换完内核态。
- 系统接收到 sendfile 指令后,通过 DMA 从磁盘把数据拷贝到系统内存。
- 数据到了系统内存后,CPU 会把文件描述符和数据长度返回到 socket 缓存中(注意这里没有拷贝数据)。
- 通过 SG-DMA 把数据从系统内存拷贝到网卡中。
- 最后,进程从内核态切换为用户态。
零拷贝在用户态和内核态之间的切换是 2 次,拷贝是 2 次
,大大减少了切换次数和拷贝次数,而且全程没有 CPU 参与数据的拷贝。
Netty中的零拷贝
Netty 中的零拷贝和传统 Linux 的零拷贝不太一样。Netty 中的零拷贝技术除了操作系统级别的功能封装,更多的是面向用户态的数据操作优化,主要体现在以下 5 个方面:
- 堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。
- CompositeByteBuf 类,可以组合多个 Buffer 对象合并成一个逻辑上的对象,避免通过传统内存拷贝的方式将几个 Buffer 合并成一个大的 Buffer。
- 通过 Unpooled.wrappedBuffer 可以将 byte 数组包装成 ByteBuf 对象,包装过程中不会产生内存拷贝。
- ByteBuf.slice 操作与 Unpooled.wrappedBuffer 相反,slice 操作可以将一个 ByteBuf 对象切分成多个 ByteBuf 对象,切分过程中不会产生内存拷贝,底层共享一个 byte 数组的存储空间。新的 ByteBuf 对象进行数据操作也会对原始 ByteBuf 对象生效。
- Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝,这属于操作系统级别的零拷贝。
堆外内存
如果在 JVM 内部执行 I/O 操作时,必须将数据拷贝到堆外内存,才能执行系统调用。这是所有 VM 语言都会存在的问题。那么为什么操作系统不能直接使用 JVM 堆内存进行 I/O 的读写呢?主要有两点原因:第一,操作系统并不感知 JVM 的堆内存,而且 JVM 的内存布局与操作系统所分配的是不一样的,操作系统并不会按照 JVM 的行为来读写数据。第二,同一个对象的内存地址随着 JVM GC 的执行可能会随时发生变化,例如 JVM GC 的过程中会通过压缩来减少内存碎片,这就涉及对象移动的问题了。
Netty 在进行 I/O 操作时都是使用的堆外内存,可以避免数据从 JVM 堆内存到堆外内存的拷贝。
更多推荐
所有评论(0)