传统 IO 流程
- 操作系统的核心就是内核,它不同于普通应用程序,所以为了保护内核的存储空间,操作系统将虚拟空间分为了两个部分:用户空间和内核空间
- DMA:直接内存访问,是一种在数据块与存储器之间直接传输数据的技术,是主板上的一块独立芯片,允许外设设备和内存之间进行数据 IO 传输
- 四次上下文切换,四次拷贝
- 将磁盘文件,读取到操作系统内核缓冲区
- 将内核缓冲区的数据,拷贝到用户空间的缓冲区
- 数据从用户空间缓冲区拷贝到内核的socket网络发送缓冲区
- 数据从内核的socket网络发送缓冲区拷贝到网卡接口(硬件)的缓冲区,由网卡进行网络传输
什么是零拷贝
零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术
零拷贝常见的实现方式
mmap + write
mmap + write实现零拷贝的方式是一种利用虚拟内存技术来优化文件IO操作的方法。它通过将内核中的读缓冲区与用户空间的缓冲区进行映射,使得所有的IO操作都在内核空间中完成,从而避免了将数据从内核空间复制到用户空间的上下文切换和数据拷贝。
mmap将内核空间缓冲区的数据直接映射到用户空间,这样就不需要再拷贝一份数据了,但是还是会有上下文切换的,变成了三次拷贝+四次上下文切换
具体步骤:
- 用户进程通过mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
- CPU利用DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
- 上下文从内核态切换回用户态,mmap方法返回。
- 用户进程通过write方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
- CPU将内核缓冲区的数据拷贝到的socket缓冲区。
- CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回。
sendfile
sendfile 是一种在 Linux 系统中实现零拷贝的技术,它可以在网络 IO 操作中避免将数据从内核空间复制到用户空间,从而提高了 IO 操作的效率
具体步骤如下:
- 通过 DMA 拷贝技术将磁盘数据拷贝到内核缓冲区
- 缓冲区描述符符合数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓冲区的数据拷贝到网卡缓冲区,而不再需要借助 socket 缓冲区,多一次拷贝
Netty 中如何实现零拷贝
netty中的零拷贝和操作系统汇总零拷贝不一样,操作系统实现零拷贝主要是在内核态的优化,Netty 完全是在用户状态实现零拷贝
CompositeByteBuf 实现零拷贝
- 常规使用
ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
- 使用 CompositeByteBuf
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);
wrap 实现零拷贝
将 btye[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象进而避免了拷贝操作
- 常规使用
byte[] bytes = ...;
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);
- 使用 Wrap
byte[] bytes = ...;
ByteBuf byteBuf = Unpooled.wrappedBuffer(btyes);
slice 实现零拷贝
将 ByteBuf 拆解成多个 ByteBuf,但是共享同一存储空间不同分区,避免了内存拷贝
ByteBuf byteBuf = ...;
ByteBuf header = byteBuf.slice(0, 5);
ByteBuf body = ByteBuf.slice(5, 10);
FileRegion 实现零拷贝
通过FileRegion包装的FileChannel.tranferTo实现文件传输,可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
● 传统文件传输
public static void copyFile(String srcFile, String destFile) throws Exception {
byte[] temp = new byte[1024];
FileInputStream in = new FileInputStream(srcFile);
FileOutputStream out = new FileOutputStream(destFile);
int length;
while ((length = in.read(temp)) != -1) {
out.write(temp, 0, length);
}
in.close();
out.close();
}
● file Region
public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r");
FileChannel srcFileChannel = srcFile.getChannel();
RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw");
FileChannel destFileChannel = destFile.getChannel();
long position = 0;
long count = srcFileChannel.size();
srcFileChannel.transferTo(position, count, destFileChannel);
}