欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

深入浅出操作系统的零拷贝

时间:2023-07-03

在 kafka、netty 这些技术中,零拷贝都是一个重要的考点。但是零拷贝与这些具体的技术无关,关键点是数据传输。就像冰糖葫芦里的山楂:冰糖葫芦里重要的组成可以有山楂。但是山楂并不是冰糖葫芦特有的,羊羹里也可以有。

下面是一个 MQ 的基本流程。

如果采用传统方法进行数据传输,消息从存储系统到达消费者需要经过4次拷贝。如果使用零拷贝技术,情况会怎样呢?

传统模式下的数据拷贝过程

过程解释

传统模式下,上图红框中经过了从文件读数据和从 socket 进行数据发送两个过程。

内部流程如下图所示:

用户进程如 Java 程序想进行 File.read ,需要将数据进行 DMA 拷贝读取到到文件读取缓冲区。还记得《时刻掌握系统运行状态-深度理解top命令》里的 buffers/cached 空间吗?文件读取缓冲区占用的就是这个空间。

文件读取缓冲区仍然是内核空间,用户进程要使用还需要进行一次 CPU 拷贝将数据拷贝到应用进程缓冲区。这时候用户进程比如 Java 程序就可以对数据进行排序、过滤等操作了。这个过程就完成了 File.read。

如果数据想发送到网卡,也就是 Socket.send。还需要再进行一次 CPU 拷贝发送到套接字发送缓冲区进行中转,这个地方也是要占用 buffers/cached 空间的。中转这个过程很快,所以 buffers/cached 空间可以很快被释放。

数据从中转站还要进行一次 DMA 拷贝,将数据运送到网络设备缓冲区,最终发送到网络上。这个过程就完成了 Socket.send。

这个过程要进行几次上下文切换呢?File.read 这个函数需要先调用发起内核请求,进入到内核空间操作,这是一次内核切换。内核操作完成返回内核的结果,这是第二次内核切换。同理, Socket.send 也需要两次内核切换。这里的用户态到内核态的切换就是上下文切换。总共是4次。

性能测试

这种方式性能如何呢?咱们来测试一下。

写个程序从本地电脑中读取自己的一张照片,这张照片5M多大,发送到服务端。

服务端只要能接收客户端数据就可以,我随便写了一个:

public static void server() throws Exception { ServerSocket serverSocket = new ServerSocket(520); int i = 1; while (true) { Socket socket = serverSocket.accept(); int left = 0; while (left >= 0) { InputStream io = socket.getInputStream(); byte[] bytes = new byte[1024]; left = io.read(bytes); } } }

客户端读取数据并发送到网络:

@GetMapping(path = "hi")public String hi() throws Exception { client(); return "end";}public void client() throws Exception { Socket socket = new Socket("127.0.0.1", 520); //向服务器端第一次发送字符串 OutputStream netOut = socket.getOutputStream(); InputStream io = new FileInputStream("D:\photo\编程一生.JPG"); long begin = System.currentTimeMillis(); byte[] bytes = new byte[1024]; while (io.read(bytes) >= 0) { netOut.write(bytes); } System.out.println("耗时为" + (System.currentTimeMillis() - begin) + "ms"); netOut.close(); io.close(); socket.close();}

服务启动后:http://localhost:8080/hi 访问5次,结果如下:

耗时为450ms

耗时为437ms

耗时为424ms

耗时为423ms

耗时为420ms

结论:使用传统方式,5M多的数据读取到发送需要400多毫秒。

零拷贝过程

过程解释

linux操作系统中有个 sendFile 方法可以不通过用户进行,直接将数据从磁盘发送到网络设备缓冲区。在 linux2.1 版本的 sendFile 过程如下图:

到了 linux2.4 ,linux 的 sendFile 进行了优化,实现了完全没有 CPU 拷贝实现数据传输。

不管是 linux2.1 还是 linux2.4 ,都是 linux 自身实现的,函数都对应的是 sendFile 。上层比如 Java 可以使用 transferTo 和 transferFrom 使用 sendFile 方法,这两个方法是 netty 实现的重要工具,一个是发送数据用,一个是接收数据用。

性能测试

这种方式性能如何呢?咱们来测试一下。

服务端不变,客户端代码如下:

@GetMapping(path = "hi")public String hi() throws Exception { client(); return "end";} public void client() throws Exception { SocketChannel socket = SocketChannel.open(); socket.connect(new InetSocketAddress("127.0.0.1", 520)); FileChannel io = new FileInputStream("D:\photo\编程一生.JPG").getChannel(); long begin = System.currentTimeMillis(); io.transferTo(0, io.size(), socket); System.out.println("耗时为" + (System.currentTimeMillis() - begin) + "ms"); io.close(); socket.close(); }

服务启动后:http://localhost:8080/hi 访问5次,结果如下:

耗时为44ms

耗时为33ms

耗时为43ms

耗时为46ms

耗时为35ms

结论:使用零拷贝方式,5M多的数据读取到发送需要40多毫秒。与传统方式相比,性能提高10倍。

内存映射模式与零拷贝

linux 系统有零拷贝,windows 也希望减少拷贝和下上下切换,它依靠内存映射(MMAP)。当然,linux 也支持内存映射,并且在 RocketMQ 等的实现上发挥着巨大作用。

通过与上面传统方式比较,可看到由于内存映射发挥作用,在文件读取时减少了一次 CPU 拷贝。

在 Java 中可以通过下面方法进行内存映射:

RandomAccessFile raf = new RandomAccessFile(file, "rw"); MappedByteBuffer mmap = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 500);

在 MQ 的实现上,内存映射(MMAP)和 sendFile 零拷贝是提升性能的利器。下面做一个比较:

上面可以看到 RocketMQ 由于使用了内存映射吞吐量远高于 ActiveMQ 和 RabbitMQ ,Kafka 由于使用了零拷贝又比 RocketMQ 提高了一个数量级。

实际上 RabbitMQ 的实现大量借鉴了 Kafka ,那 RabbitMQ 为什么不直接使用 Kafka 的零拷贝提高性能呢?因为 RabbitMQ 不仅仅是将数据从磁盘发送出去,还需要在内存中做一些排序、过滤等高级操作。

最后大家再来思考一个问题:零拷贝和内存映射两种模式下,各需要几次上下文切换?

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。