一、为什么需要 Zero Copy技术?
看起来是很简单的,但是如果我们深入到操作系统的层面,就会发现实际的微观操作要更复杂。在这个场景中,至少出现 4 次数据拷贝和 3 次的内核态和用户态的切换。具体来说有以下步骤:
2、将数据从内核缓冲区拷贝到用户空间缓冲区,read() 系统调用返回,并从内核态切换回用户态。
4、数据最终经由 Socket 通过 DMA 传送到硬件(如网卡)缓冲区,write() 系统调用返回,并从内核态切换回用户态。
二、Zero Copy 原理
关于零拷贝提供了两种解决方式:mmap + write 方式、sendfile 方式
所有现代操作系统都使用虚拟内存,使用虚拟地址取代物理地址,这样做的好处就是:
2)虚拟内存空间可以远远大于物理内存空间
使用 mmap+write 方式替换原来的传统 IO 方式,就是利用了虚拟内存的特性。mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系;这样就可以省掉原来内核 Read Buffer copy 数据到用户缓冲区,但是还是需要内核 Read Buffer 将数据 copy 到内核 Socket Buffer,如下图:
这个流程就少了一个CPU Copy,提升了 IO 的速度。不过发现上下文的切换还是 4 次,没有减少,因为还是要应用程序发起 write 操作。那能不能减少上下文切换呢?
3、sendfile方式
1)首先(通过 DMA )将数据从磁盘读取到内核 Read Buffer 中;
3)最后将 Socket buffer 中的数据 copy 到网卡设备中发送;
Linux2.4内核进行了优化,提供了gather操作,这个操作可以把最后一次CPU Copy去除,什么原理呢?就是在内核空间 Read Buffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中,这样就不需要复制(其实本质就是和虚拟内存的解决方法思路一样,就是内存地址的记录)
Java 的 Zero Copy 是由 Java NIO 来提供的,NIO 三大核心要素 :Buffer(缓冲区)、Channel(通道)和 Selector(选择器),Buffer 和Channel 组合实现了Java 的 Zero Copy,主要是由 MappedByteBuffer、DirectByteBuffer 以及 FileChannel来完成的。
MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的一种实现,它继承自 ByteBuffer。FileChannel 定义了一个 map() 方法,它可以把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件。
map() 方法通过本地方法 map0() 为文件分配一块虚拟内存,作为它的内存映射区域,然后返回这块内存映射区域的起始地址。
2)通过 Util 的 newMappedByteBuffer方法或者 newMappedByteBufferR方法反射创建一个 DirectByteBuffer 实例,其中 DirectByteBuffer 是 MappedByteBuffer 的子类。
- DirectByteBuffer
DirectByteBuffer 内部的字节缓冲区位在于堆外的(用户态)直接内存,它是通过 Unsafe 的本地方法 allocateMemory() 进行内存分配,底层调用的是操作系统的 malloc() 函数。
DirectByteBuffer(int cap) { super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null;}- FileChannel
transferTo() 和 transferFrom() 方法的底层实现是由 FileChannelImpl 提供的,底层原理是基于 sendfile 实现数据传输的。
本文开篇详述了为什么需要 Zero Copy以及其底层原理。从源码着手分析了 Java NIO 对零拷贝的实现,主要包括基于内存映射(mmap)方式的 MappedByteBuffer 以及基于 sendfile 方式的 FileChannel。
PS:这个坑已经越挖越大了,在这里又引入了虚拟内存、mmap 以及 DMA (Direct Memory Access),甚至 Java 的 NIO 等概念。
挖坑序列文章
