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

本文探讨了零拷贝技术在Kafka、Netty等技术中的应用,对比了传统数据传输与零拷贝(包括sendFile和内存映射)的性能差异,揭示了零拷贝在减少上下文切换和提升数据处理速度方面的优势,以及内存映射如何在Linux和Windows中优化数据传输过程。
摘要由CSDN通过智能技术生成

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

51cf373dd3156e387af376018c014a10.png

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

5c48764e70801f4c28081e37b19253b9.png

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

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

过程解释

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

46b20a338f88ddae250d6180ec42ca0b.png

内部流程如下图所示:

31b753b25737fd520d6092bf8871b7f6.png

用户进程如 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多大,发送到服务端。

2d6b0bb00d8a96d68e528eae716d17d0.png

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

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 过程如下图:

fa0c01b1c5e7973443be1b7678816e20.png

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

0ea0a5635e375b9b72bf538ae36634b5.png

不管是 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 等的实现上发挥着巨大作用。

7ca2fd72c985710581da117e0f93883c.png

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

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

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

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

31ce6d9d238719c58ad43645d9a63f2e.png

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值