零拷贝(Zero-copy)技术是一种计算机操作系统中用于提高数据传输效率的优化策略。在传统的数据传输过程中,需要将数据从一个缓冲区拷贝到另一个缓冲区,然后再传输给目标。这涉及到多次的 CPU 和内存之间的数据拷贝操作,会消耗 CPU 的时间和内存带宽。而零拷贝技术通过直接共享数据的内存地址,避免了中间的拷贝过程,从而提高了数据传输的效率。
零拷贝是如何实现的?
1、MMap
MMap(Memory Map)是 Linux 操作系统中提供的一种将文件映射到进程地址空间的一种机制,通过 MMap 进程可以像访问内存一样访问文件,而无需显式的复制操作。
传统的 IO 需要四次拷贝和四次上下文(用户态和内核态)切换,而 MMap 只需要三次拷贝和四次上下文切换,从而能够提升程序整体的执行效率,并且节省了程序的内存空间。
2、senFile 方法
3、带有 scatter/gather 的 sendfile方式
scatter/gather 的 sendfile 只有两次数据复制(都是 DMA COPY)及 2 次上下文切换。CUP COPY 已经完全没有。不过这一种收集复制功能是需要硬件及驱动程序支持的。
4、splice 方式
splice
调用和sendfile
非常相似,用户应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。与sendfile
不同的是,splice
允许任意两个文件互相连接,而并不只是文件与socket
进行数据传输。对于从一个文件描述符发送数据到socket
这种特例来说,一直都是使用sendfile
系统调用,而splice
一直以来就只是一种机制,它并不仅限于sendfile
的功能。也就是说 sendfile 是 splice 的一个子集。
在 Linux 2.6.17 版本引入了 splice,而在 Linux 2.6.23 版本中, sendfile 机制的实现已经没有了,但是其 API 及相应的功能还在,只不过 API 及相应的功能是利用了 splice 机制来实现的。
和 sendfile 不同的是,splice 不需要硬件支持。
哪些地方用到了零拷贝技术?
在 Java 中,以下几个地方使用了零拷贝技术:
- NIO(New I/O)通道:java.nio.channels.FileChannel 提供了 transferTo() 和 transferFrom() 方法,可以直接将数据从一个通道传输到另一个通道,例如从文件通道直接传输到 Socket 通道,整个过程无需将数据复制到用户空间缓冲区,从而实现了零拷贝。
- Socket Direct Buffer:在 JDK 1.4 及更高版本中,Java NIO 支持使用直接缓冲区(DirectBuffer),这类缓冲区是在系统堆外分配的,可以直接由网卡硬件进行 DMA 操作,减少数据在用户态与内核态之间复制次数 ,提高网络数据发送效率。
- Apache Kafka 或者 Netty 等高性能框架:这些框架在底层实现上通常会利用 Java NIO 的上述特性来优化数据传输,如 Kafka 生产者和消费者在传输消息时会用到零拷贝技术以提升性能。
为什么使用零拷贝?
无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA copy
是都少不了的。因为两次 DMA 都是依赖硬件完成的。所以,所谓的零拷贝,都是为了减少 CPU copy 及减少了上下文的切换。
CPU拷贝 | DMA拷贝 | 系统调用 | 上下文切换 | |
传统方法 | 2 | 2 | read/write | 4 |
内存映射 | 1 | 2 | mmap/write | 4 |
sendfile | 1 | 2 | sendfile | 2 |
scatter/gather copy | 0 | 2 | sendfile | |
splice | 0 | 2 | splice | 0 |
为什么零拷贝的两次DMA不能省略?
在计算机系统中,DMA(Direct Memory Access)是用于在外设(如硬盘、网卡)和内存之间高效传输数据的一种技术。即便是零拷贝技术,也无法绕过两次 DMA 拷贝。
1、从设备到内存的传输(第一次 DMA 拷贝)
外设(如硬盘)上的数据必须通过 DMA 控制器传输到内存中,CPU 才能处理或转发这些数据。这是硬件工作机制决定的,因为内存是 CPU 和外设之间共享的高速缓存区。
2、从内存到设备的传输(第二次 DMA 拷贝)
在数据被传递到网卡等设备发送出去时,数据需要从内存再次通过 DMA 控制器传输到网卡的缓冲区。这是因为网卡等外设通常没有直接访问主存的权限,需要通过 DMA 将数据搬运到其自身的发送缓冲区中。
零拷贝的其他实现
1、splice()
系统调用
splice()
是 Linux 提供的一种系统调用,用于在两个文件描述符之间高效传输数据,避免了用户态和内核态之间的切换。- 数据直接在内核缓冲区中传输,减少了用户态的参与。
- 常用于管道和文件之间的数据传输。
2、tee()
系统调用
tee()
用于在管道之间复制数据,而不实际消耗 CPU 或内存。- 数据不会真正拷贝,只是增加了引用计数。
3、IO Uring(Linux Kernel 5.1+)
- IO Uring 是一种现代异步 IO 接口,通过共享环形缓冲区,在用户态和内核态之间减少数据拷贝和上下文切换。
- 支持零拷贝操作,如网络数据发送等。
4、DMA-BUF
- DMA-BUF 是一种跨设备的共享缓冲区机制,允许多个硬件设备共享同一块内存区域,减少数据拷贝。
- 典型场景:图形处理中的 GPU 和显示设备共享数据。
5、RDMA(Remote Direct Memory Access)
- RDMA 是一种高性能网络技术,允许数据直接从一个主机的内存传输到另一个主机的内存,而无需经过 CPU。
- 常用于分布式系统和高性能计算领域。
零拷贝的关键点
零拷贝的目标是尽可能减少:
- 数据从用户态到内核态的拷贝
- 数据在内存中的多次搬运
但 DMA 传输过程无法绕过,零拷贝的主要优化点是避免 CPU 和用户态的额外参与,实现更高的性能。