什么是零拷贝技术
传统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 次(read 和 write 都需要用户态切换) |
最低效,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)。DirectByteBuffer
是MappedByteBuffer
的具体实现类。
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);