Netty剖析之零拷贝

什么是零拷贝?

  • 零拷贝是网络编程的关键,很多性能优化都离不开;
  • 零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据);
  • 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算;
  • 在Java程序中,常用的零拷贝有 mmap(内存映射)和sendFile。那么,他们在操作系统里,到底是怎么样的一个的设计?下面我们将来分析mmap和sendFile这两个零拷贝;

传统IO读写

public class Test {
    public static void main(String[] args) throws Exception {
        File file = new File("d:\\test.txt");
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");

        byte[] byteArray = new byte[(int)file.length()];
        randomAccessFile.read(byteArray);

        Socket socket = new ServerSocket(8899).accept();
        socket.getOutputStream().write(byteArray);
    }
}

示意图:
在这里插入图片描述

上图中,上半部分表示用户态和内核态的上下文切换,下半部分表示数据复制操作,步骤如下:

  1. 调用read方法会导致用户态到内核态的一次变化,同时,第一次数据拷贝开始(DMA(Direct Memory Access,直接内存存取,即不使用CPU拷贝数据到内存,而是DMA引擎传输数据到内存,用于解放 CPU),引擎从磁盘读取test.txt文件,并将数据放入到内核缓冲区;
  2. 第二次数据拷贝,即将内核缓冲区的数据拷贝到用户缓冲区,同时发生了一次内核态到用户态的上下文切换;
  3. 第三次数据拷贝,当我们调用write方法时,系统会将用户缓冲区的数据拷贝到socket缓冲区,此时又发生了一次用户态到内核态的上下文切换;
  4. 第四次数据拷贝,数据异步的从socket缓冲区使用DMA引擎拷贝到网路协议引擎,这一段不需要上下文切换;
  5. write方法执行完后,再次从内核态切换到用户态;

从上述步骤可见,复制拷贝的操作太多了,大大影响了操作效率,因此就有了mmap优化;

mmap优化

mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图所示:
在这里插入图片描述
上图中,user buffer和kernel buffer共享test.txt。如果你想把硬盘的test.txt传输到网络中,再也不用拷贝到用户空间,然后再从用户空间拷贝到 Socket 缓冲区。
现在,你只需要从内核缓冲区拷贝到Socket缓冲区即可,这将减少一次内存拷贝(从4次变成了3次),但不减少上下文切换次数。

还有没有更优的方案呢?接着往下看

sendFile

Linux 2.1版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换,如下图所示:
在这里插入图片描述
从上图可以看到,我们进行sendFile系统调用时,数据被DMA引擎从文件复制到内核缓冲区,然后调用,然后掉用write方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为在一个用户空间。
最后,数据从 Socket 缓冲区进入到协议栈。此时,数据经过了 3 次拷贝,3 次上下文切换。

那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?实际上,Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图:
在这里插入图片描述
现在,test.txt要从文件进入到网络协议栈,只需2次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到SocketBuffer,基本无消耗。
等一下,不是说零拷贝吗?为什么还是要2次拷贝?首先我们所说的零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。例如我们刚开始的例子,内核缓存区和 Socket 缓冲区的数据就是重复的。

mmap、sendFile比较

  1. mmap 适合小数据量读写,sendFile 适合大文件传输
  2. mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  3. sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

在这个选择上:rocketMQ在消费消息时,使用了mmap。kafka使用了sendFile

发布了107 篇原创文章 · 获赞 19 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/chen_changying/article/details/104137996