相关内容:操作系统 IO 概念 & 文件拷贝
概念
零拷⻉是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷⻉时间。它是一种 I /O 操作优化技术。
传统 IO 拷贝

而如果进行服务端发送文件,则需要进行 磁盘 -> 内核缓冲区 -> 程序内存 -> Socket 缓冲区 -> 网卡。
这个流程需要进行 4 次用户态内核态的转换、4 次数据拷贝过程(2 次 CPU+2 次 DMA)。关于 DMA 拷贝参考操作系统 IO 概念 & 文件拷贝介绍。
IO 拷贝优化
直接 I/O
fd1 = // 打开的文件描述符
fd2 = // 打开的 socket 描述符
buffer = // 应用程序内存
read(fd1, buffer...) // 先把数据从文件中读出来
write(fd2, buffer...) // 再网络发出整个过程会有 4 次数据拷贝,读进来 2 次,写回去又 2 次。
磁盘→内核缓冲区→应用程序内存→Socket 缓冲区→网络

内存映射文件 mmap
fd1 = // 打开的文件描述符
fd2 = // 打开的 socket 描述符
buffer = // 应用程序内存
mmap(fd1, buffer...) // 先把数据映射到 buffer 上
write(fd2, buffer...) // 再网络发出整个过程会有 3 次数据拷贝,不再经过应用程序内存,直接在内核空间中从内核缓冲区拷贝到 Socket 缓冲区。


sendfile 零拷贝
但如果用零拷贝,可能连内核缓冲区到 Socket 缓冲区的拷贝也省略了。如图 4-5 所示,在内核缓冲区和 Socket 缓冲区之间并没有做数据拷贝,只是一个地址的映射,底层的网卡驱动程序要读取数据并发送到网络的时候,看似读的是 Socket 缓冲区的数据,但实际上直接读的是内核缓冲区中的数据。

在这里,我们看到虽然叫零拷贝,实际是 2 次数据拷贝,1 次是从磁盘到内核缓冲区,1 次是从内核缓冲区到网络。之所以叫零拷贝,是从内存的角度来看的,数据在内存中没有发生过数据拷贝,只在内存和 I/O 之间传输。
Linux 系统中,零拷贝的系统 API 为:
sendfile(int out_fd, int in_fd, off_t *offset, size_t count)其中,out_fd 传入 socket 描述符,in_fd 传入是文件描述符。
Java 中,对应的是
FileChannel.transferTo(long position, long count, WritableByteChannel target)
sendfile+DMA scatter/gather 实现的零拷贝
linux 2.4 版本之后,对 sendfile 做了优化升级,引入 SG-DMA 技术,其实就是对 DMA 拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次 CPU 拷贝。
sendfile+DMA scatter/gather 实现的零拷贝流程如下:

- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区
- DMA 控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
- 上下文(切换 2)从内核态切换回用户态,sendfile 调用返回。
可以发现,sendfile+DMA scatter/gather 实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包 DMA 拷贝。这就是真正的零拷贝(Zero-copy) 技术,全程都没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
Java 提供的零拷贝方式
Java NIO 对 mmap 的支持
Java NIO 有一个 MappedByteBuffer 的类,可以用来实现内存映射。它的底层是调用了 Linux 内核的 mmap 的 API。
try {
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输 writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
} Java NIO 对 sendfile 的支持
FileChannel 的,底层就是 sendfile() 系统调用函数。Kafka 这个开源项目就用到它,平时面试的时候,回答面试官为什么这么快,就可以提到零拷贝这个点。
@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}sendfile 的小 demo 如下:
try {
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 数据传输
readChannel.transferTo(position, len, writeChannel); readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}