相关内容:操作系统 IO 概念 & 文件拷贝

概念

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

传统 IO 拷贝

600|600

而如果进行服务端发送文件,则需要进行 磁盘 -> 内核缓冲区 -> 程序内存 -> 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 缓冲区→网络

600|600

内存映射文件 mmap

fd1 = // 打开的文件描述符
fd2 = // 打开的 socket 描述符
buffer = // 应用程序内存
mmap(fd1, buffer...) // 先把数据映射到 buffer 上
write(fd2, buffer...) // 再网络发出

整个过程会有 3 次数据拷贝,不再经过应用程序内存,直接在内核空间中从内核缓冲区拷贝到 Socket 缓冲区。

600|600

600|600

sendfile 零拷贝

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

600|600

在这里,我们看到虽然叫零拷贝,实际是 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)

600|600

sendfile+DMA scatter/gather 实现的零拷贝

linux 2.4 版本之后,对 sendfile 做了优化升级,引入 SG-DMA 技术,其实就是对 DMA 拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次 CPU 拷贝。

sendfile+DMA scatter/gather 实现的零拷贝流程如下:

600|600

  1. 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
  2. DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. CPU 把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区
  4. DMA 控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
  5. 上下文(切换 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());
}