01 引言
零拷贝(Zero-copy
)是高性能IO绕不开的话题,通过减少拷贝次数(IO次数)来提高IO的性能。
要了解零拷贝就要先了解一下几个基本概念:用户态(User Mode
)、内核态(Kernal Mode
)、虚拟内存以及DMA
。
- 用户态:也叫用户空间,该区域主要用来运行程序代码,为了保证系统内核的安全,它不能直接访问内存等硬件设备,必须通过系统调用进入到内核态来访问那些受限的资源。
- 内核态:也可以成为内核空间,该区域是运行内核代码的地方,就是计算底层的代码,如80中断等。可以执行任意的指令访问系统资源,既可以访问内核空间也可以访问用户空间。
- 虚拟内存:操作系统为每个进程分配了独立的虚拟地址空间,也就是虚拟内存,虚拟地址空间又分为用户空间和内核空间。
DMA
:内存直接读取(Direct Memory Access),又被人成为协处理器。是一种允许外围设备(硬件子系统)直接访问系统主内存的机制。
02 基本概念
我们先了解一下传统的IO,对于用户进程发出读写指令时,计算机是如何运行的。
- 当用户进程调用
read()
,用户态无法调用内核态的设备,只能触发系统调用(IO)。这时计算机需要从用户态切换为内核态。 - 到达内核态之后,计算机通过
DMA
控制器将数据从磁盘读取出来,放到内核的缓冲区。完成第一次拷贝。 - CPU需要将缓冲区的数据拷贝到用户态的缓冲区,完成第二次拷贝,也是read()函数的返回。这时计算器需要从内核态切换为用户态。
- 因为最终的数据需要通过网卡输出,所以用户进程就需要调用
write()
函数,CPU将用户缓冲区的数据拷贝到Socket
缓冲区,完成第三次拷贝。同时需要再次触发系统调用。这时计算机又需要从用户态切换为内核态。 DMA
控制器把数据从Socket
缓冲区,拷贝到网卡设备输出,至此完成第四不拷贝。同时需要将内核态切换为用户态,write()
函数返回。
从流程来看,传统的IO读写操作需要四次的拷贝,以及用户态和内核态的来回切换。而这些操作都是比较消耗资源的。所以传统的IO有它的瓶颈。
那这样的问题能不能解决呢?
当然可以,零拷贝的技术正是为此而来。
03 零拷贝
零拷贝(Zero-Copy
)一般指的是从磁盘读取文件发送到网络或者从网络接收数据写入到磁盘文件的过程中,最大程度的减少数据的拷贝次数。零拷贝并不是真正上将拷贝的次数变成0,只是减少。
在Java
中也可以使用零拷贝技术, 主要是java.nio.channels.FileChannel
中的方法:
transferTo(long position, long count, WritableByteChannel target)
transferFrom(ReadableByteChannel src,long position, long count)
用来实现字节数据从一个channel
转换到另一个channel
中,这里就是Page Cache
到Socket
缓冲区的拷贝。
从示例图中,可以看到将两次拷贝变成了一次拷贝,总共三次拷贝。
- 用户进程调用了
FileChannel.transferTo()
后,触发系统调用,系统从用户态切换到内核态。 DMA
协处理器将文件数据拷贝到内核缓冲区Page Cahe
。这是第一次拷贝。- CPU将内核缓冲区的数据拷贝到
Socket
缓冲区,完成第二次拷贝。 DMA
又将Socket
缓冲区的数据拷贝到网卡进行数据传输,完成第三次拷贝。
内核缓冲区到Socket
缓冲区都属于内核态,能不能共享或者取消这次拷贝呢?这个属于系统层面的设计。而linux 2.4
版本中已经做了优化。
“Then by using the DMA scatter/gather operation, the network interface card can gather all the data from different memory locations and store the assembled packet in the network card buffer.”
避免了从内核缓冲区拷贝到 Socket
缓冲区的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
3.1 Java的场景
@Test
void test06() throws Exception {
StopWatch watch = new StopWatch();
watch.start();
try (FileInputStream fileInputStream = new FileInputStream(Path.of("src/main/file/upload/10最大图片.jpg").toFile());
FileOutputStream outputStream = new FileOutputStream(Path.of("src/main/file/temp/part.jpg").toFile())) {
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
}
watch.stop();
System.out.println(watch.getTotalTimeMillis());
System.out.println("----------------------------------");
StopWatch watch2 = new StopWatch();
watch2.start();
try (RandomAccessFile sraf = new RandomAccessFile("src/main/file/upload/10最大图片.jpg", "r");
RandomAccessFile raf = new RandomAccessFile("src/main/file/temp/part1.jpg", "rw")) {
long length = sraf.length();
System.out.println("length:" + length);
// 调用transferTo方法
sraf.getChannel().transferTo(0, length, raf.getChannel());
}
watch2.stop();
System.out.println(watch2.getTotalTimeMillis());
}
执行结果:
我们可以看到使用零拷贝,是传统的IO性能的10倍左右。意不意外,惊喜不惊喜!
3.2 Socket场景
在 Linux
中系统调用 sendfile()
可以实现将数据从一个文件描述符传输到另一个文件描述符,从而实现了零拷贝技术。
try (ServerSocketChannel server = ServerSocketChannel.open();
FileChannel fileChannel = FileChannel.open(Paths.get("data.big"), READ)) {
server.bind(new InetSocketAddress(9000));
SocketChannel client = server.accept();
fileChannel.transferTo(0, fileChannel.size(), client);
}
04 零拷贝技术栈扩展
框架 | 零拷贝实现类 | 应用场景 |
---|---|---|
Netty | FileRegion | 大文件网络传输 |
RocketMQ | MappedFile | 消息存储 |
Tomcat | SendfileFeature | 静态资源传输 |
gRPC | ByteBufferInputStream | 流式数据传输 |
05 小结
零拷贝和传统IO的性能产生了相当大的性能差异,赶快用起来吧。零拷贝的实现方式有很多如mmap
、sendfile
,dma
,directI/O
等。
你了解几个呢?评论区留言。