零拷贝
面试提问
"什么是零拷贝?Java NIO 如何实现零拷贝?"
传统 IO 的数据拷贝
传统读取并发送文件
// 传统方式
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr); // 读取到用户空间
socket.getOutputStream().write(arr); // 发送出去数据拷贝流程(4 次拷贝,4 次上下文切换)
┌──────────────────────────────────────────────────────┐
│ 传统 IO 数据流 │
├──────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ DMA拷贝 ┌─────────┐ │
│ │ 磁盘 │ ─────────────→ │ 内核缓冲 │ │
│ └─────────┘ (1) └────┬────┘ │
│ │ CPU拷贝(2) │
│ ↓ │
│ ┌─────────┐ │
│ │ 用户缓冲 │ │
│ └────┬────┘ │
│ │ CPU拷贝(3) │
│ ↓ │
│ ┌─────────┐ │
│ │Socket缓冲│ │
│ └────┬────┘ │
│ │ DMA拷贝(4) │
│ ↓ │
│ ┌─────────┐ │
│ │ 网卡 │ │
│ └─────────┘ │
│ │
│ 上下文切换:用户态→内核态→用户态→内核态→用户态 (4次) │
└──────────────────────────────────────────────────────┘性能问题
| 问题 | 说明 |
|---|---|
| 多次拷贝 | 4 次数据拷贝,2 次 CPU 参与 |
| 多次切换 | 4 次上下文切换,开销大 |
| 用户空间参与 | 数据在用户空间无意义的中转 |
零拷贝技术
零拷贝(Zero Copy)不是完全没有拷贝,而是减少 CPU 参与的拷贝次数。
Linux 零拷贝方式
| 方式 | 系统调用 | 说明 |
|---|---|---|
| mmap | mmap() | 内存映射文件 |
| sendfile | sendfile() | 内核态直接传输 |
| splice | splice() | 管道传输 |
| tee | tee() | 复制管道数据 |
mmap(内存映射)
原理
将文件映射到用户空间的虚拟内存,省去内核缓冲区到用户缓冲区的拷贝。
┌──────────────────────────────────────────────────────┐
│ mmap 数据流 │
├──────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ DMA拷贝 ┌─────────┐ │
│ │ 磁盘 │ ─────────────→ │ 内核缓冲 │ │
│ └─────────┘ (1) └────┬────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ 用户虚拟内存 │ ←映射 │
│ └──────┬──────┘ │
│ │ CPU拷贝(2) │
│ ↓ │
│ ┌─────────┐ │
│ │Socket缓冲│ │
│ └────┬────┘ │
│ │ DMA拷贝(3) │
│ ↓ │
│ ┌─────────┐ │
│ │ 网卡 │ │
│ └─────────┘ │
│ │
│ 拷贝次数:3 次(减少 1 次 CPU 拷贝) │
└──────────────────────────────────────────────────────┘Java 实现
FileChannel channel = new RandomAccessFile("test.txt", "r").getChannel();
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY,
0,
channel.size()
);
// buffer 直接映射文件内容,无需 read() 调用sendfile
原理
数据完全在内核态传输,不经过用户空间。
┌──────────────────────────────────────────────────────┐
│ sendfile 数据流 │
├──────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ DMA拷贝 ┌─────────┐ │
│ │ 磁盘 │ ─────────────→ │ 内核缓冲 │ │
│ └─────────┘ (1) └────┬────┘ │
│ │ CPU拷贝(2) │
│ ↓ │
│ ┌─────────┐ │
│ │Socket缓冲│ │
│ └────┬────┘ │
│ │ DMA拷贝(3) │
│ ↓ │
│ ┌─────────┐ │
│ │ 网卡 │ │
│ └─────────┘ │
│ │
│ 拷贝次数:3 次(上下文切换只有 2 次) │
└──────────────────────────────────────────────────────┘Java 实现
FileChannel fileChannel = new FileInputStream("test.txt").getChannel();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 零拷贝传输
fileChannel.transferTo(0, fileChannel.size(), socketChannel);sendfile + DMA Scatter/Gather
原理(真正的零拷贝)
Linux 2.4+ 支持 DMA Scatter/Gather,数据描述符直接传给 Socket,DMA 直接从内核缓冲读取。
┌──────────────────────────────────────────────────────┐
│ sendfile + DMA Scatter/Gather │
├──────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ DMA拷贝 ┌─────────┐ │
│ │ 磁盘 │ ─────────────→ │ 内核缓冲 │ │
│ └─────────┘ (1) └────┬────┘ │
│ │ │
│ 描述符(位置、长度)传递 │
│ ↓ │
│ ┌─────────┐ │
│ │Socket缓冲│ │
│ └────┬────┘ │
│ │ DMA拷贝(2) │
│ ↓ │
│ ┌─────────┐ │
│ │ 网卡 │ │
│ └─────────┘ │
│ │
│ 拷贝次数:2 次 DMA 拷贝,CPU 不参与! │
│ 上下文切换:2 次 │
└──────────────────────────────────────────────────────┘Java NIO 零拷贝 API
FileChannel.transferTo()
// 最简单的零拷贝
try (FileChannel from = new FileInputStream("source.txt").getChannel();
FileChannel to = new FileOutputStream("dest.txt").getChannel()) {
long transferred = from.transferTo(0, from.size(), to);
}FileChannel.transferFrom()
try (FileChannel from = new FileInputStream("source.txt").getChannel();
FileChannel to = new FileOutputStream("dest.txt").getChannel()) {
to.transferFrom(from, 0, from.size());
}MappedByteBuffer
try (FileChannel channel = FileChannel.open(
Paths.get("test.txt"),
StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE,
0,
channel.size()
);
// 直接修改映射内存
buffer.put(0, (byte) 'H');
// 修改会自动同步到文件
}零拷贝应用场景
| 场景 | 技术 | 说明 |
|---|---|---|
| Kafka | sendfile | 高效消息传输 |
| Netty | FileRegion | 文件传输 |
| Nginx | sendfile | 静态文件服务 |
| RocketMQ | mmap + sendfile | 消息存储 |
对比总结
| 方式 | CPU 拷贝 | DMA 拷贝 | 上下文切换 |
|---|---|---|---|
| 传统 IO | 2 | 2 | 4 |
| mmap | 1 | 2 | 4 |
| sendfile | 1 | 2 | 2 |
| sendfile+DMA | 0 | 2 | 2 |
面试要点总结
| 问题 | 答案要点 |
|---|---|
| 什么是零拷贝? | 减少 CPU 参与的数据拷贝,降低上下文切换 |
| 传统 IO 几次拷贝? | 4 次(2 DMA + 2 CPU),4 次上下文切换 |
| mmap 原理? | 文件映射到用户虚拟内存,省去一次 CPU 拷贝 |
| sendfile 原理? | 内核态直接传输,用户空间不参与 |
| transferTo 作用? | Java NIO 提供的零拷贝方法 |
参考资料
- Zero Copy (opens in a new tab)
- Linux sendfile 系统调用
- Kafka 零拷贝实现