零拷贝技术

零拷贝技术以及Java中的实践

什么是零拷贝技术

传统I/O方式中,数据从磁盘到发送到网卡需要被拷贝4次(2次DMA拷贝,2次CPU拷贝)

DMA(Direct Memory Access,直接内存访问) 是一种 硬件机制,允许 外设(如磁盘、网卡)直接与内存进行数据交换,而无需 CPU 参与数据搬运。

  • 传统方式IO拷贝过程
    磁盘  --(DMA)→  内核缓冲区  --(CPU拷贝)→  用户缓冲区  --(CPU拷贝)→  Socket 缓冲区  --(DMA)→  网卡
    

零拷贝是一种I/O优化技术。两次DMA拷贝是依赖硬件完成的,是必不可少的。零拷贝主要是减少了 CPU 拷贝及上下文的切换。

目前来看,零拷贝技术的几个实现手段包括:mmap+write、sendfile、sendfile+DMA收集、splice等。

零拷贝技术原理

不同文件传输方式的数据拷贝对比

方式 CPU 拷贝次数 DMA 拷贝次数 使用的系统 API 上下文切换 特点
传统 I/O (read + write) 2 次read:内核 → 用户,write:用户 → 内核) 2 次(磁盘 → 内核,Socket → 网卡) read(), write() 2 次readwrite 都需要用户态切换) 最低效,CPU 负担最大
mmap + write 1 次write:用户 → 内核) 2 次(磁盘 → 内核,Socket → 网卡) mmap(), write() 1 次write 触发内核态切换) read + write 少一次 CPU 拷贝
sendfile 1 次sendfile 直接从内核页缓存拷贝到 Socket) 2 次(磁盘 → 内核,Socket → 网卡) sendfile() 1 次sendfile 触发内核态切换) 仅在内核中拷贝数据,无需用户态干预
sendfile + DMA 0 次(完全零拷贝) 2 次(磁盘 → 内核,Socket → 网卡) sendfile() + TCP_CORK(Linux 2.4+) 1 次sendfile 触发内核态切换) 最高效,CPU 不参与拷贝

mmap,内存映射,减少了一次将数据从内核缓冲区copy到用户缓冲区的过程。

sendfile在mmap的基础上,进一步简化,将数据直接从内核缓冲区copy到Socket发送缓冲区。

mmap方式

mmap(memory map)是一种内存映射文件的方法,应用程序可以直接操作文件数据,无需显式复制。

mmap + write 的优点

  • 适用于大文件:mmap 允许部分文件映射,可以在大文件操作时减少磁盘 I/O 。
  • 适用于进程间共享:多个进程可以共享 mmap 映射的内存,提高访问效率。
  • 降低 read() 负担:避免 read() 的 CPU 拷贝开销,仅依赖 write() 。

mmap + write 的缺点

  • write() 仍然需要 CPU 参与拷贝,不能完全避免 CPU 负担。
  • 受页缓存影响:如果数据访问模式导致频繁缺页,性能可能会下降。
  • 可能占用较大内存:映射大文件时,需要分配用户态地址空间,内存使用较高。
磁盘文件  ───►  内核页缓存(mmap映射,无拷贝)
                      │
                      ▼
             用户空间映射区
                      │
                      ▼
      write  触发数据拷贝(用户空间 → 内核态 Socket 发送缓冲区)
                      │
                      ▼
           Socket 发送缓冲区
                      │
                      ▼
         DMA 直接传输到网卡(无拷贝)

sendfile

sendfile 的优点

  • 更适合网络传输:sendfile 直接在内核态完成数据传输,避免用户空间拷贝,特别适合 Web 服务器、文件下载服务。
  • 减少 CPU 负担:相比 mmap + write,sendfile 直接从页缓存拷贝数据到 Socket,CPU 负担更低。
  • 减少系统调用:sendfile 只需一次系统调用,而 mmap + write 需要 mmap() + write()。

sendfile 的缺点

  • 仍然有一次 CPU 拷贝:虽然比 mmap + write 少,但 sendfile 仍然需要一次 页缓存 → Socket 缓冲区 的 CPU 拷贝。
  • 不支持用户空间数据:只能传输 文件描述符之间的数据,无法处理 malloc() 申请的用户空间数据。
  • 不适用于进程间共享:sendfile 仅用于 文件传输,不能用于 进程间数据共享。
磁盘文件  ───►  内核页缓存(无需 mmap,直接读取)
                      │
                      ▼
       sendfile 触发数据拷贝(内核页缓存 → Socket 发送缓冲区)
                      │
                      ▼
           Socket 发送缓冲区
                      │
                      ▼
         DMA 直接传输到网卡(无拷贝)

Java中零拷贝技术的实践

在 Java 中,零拷贝(Zero-Copy)技术主要用于高效的数据传输,避免不必要的 CPU 拷贝和用户态/内核态切换。常见的零拷贝技术有:

MappedByteBuffer(基于 mmap)

适用于: 大文件读写、日志存储、进程间通信(IPC)。DirectByteBufferMappedByteBuffer的具体实现类。

FileChannel fileChannel = FileChannel.open(Paths.get("data.log"),
    StandardOpenOption.READ, StandardOpenOption.WRITE);
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
buffer.put("Hello Zero-Copy!".getBytes()); // 直接操作内存,无需拷贝

FileChannel.transferTo() / transferFrom()(基于 sendfile)

适用于: 大文件传输(如 Web 服务器、日志存储)

try (FileChannel sourceChannel = new FileInputStream("source.txt").getChannel();
     FileChannel destChannel = new FileOutputStream("dest.txt").getChannel()) {
    sourceChannel.transferTo(0, sourceChannel.size(), destChannel);
}

Netty的零拷贝

1)基于 sendfile 的 FileRegion 适用于: 高并发网络通信(如 RPC、WebSocket、TCP 服务器)

FileChannel fileChannel = new FileInputStream("largeFile.txt").getChannel();
DefaultFileRegion fileRegion = new DefaultFileRegion(fileChannel, 0, fileChannel.size());

ChannelFuture future = channel.writeAndFlush(fileRegion);
future.addListener(f -> fileChannel.close()); // 发送完成后关闭文件

2)CompositeByteBuf(避免数据拷贝)

CompositeByteBuf只是逻辑合并多个ByteBuf,但数据仍然存储在 JVM 内存(堆或堆外),不涉及磁盘文件。CompositeByteBuf是纯用户态的,mmap依赖操作系统内核。

ByteBuf header = Unpooled.wrappedBuffer("Header".getBytes());
ByteBuf body = Unpooled.wrappedBuffer("Body".getBytes());
CompositeByteBuf response = Unpooled.compositeBuffer();
response.addComponents(header, body);