零拷贝(Zero-Copy)

  • 传统I/O : 硬盘—>内核缓冲区—>用户缓冲区—>内核 Socket 缓冲区—>协议引擎
  • sendfile :硬盘—>内核缓冲区—>内核 Socket 缓冲区—>协议引擎
  • sendfile(DMA 收集拷贝):硬盘—>内核缓冲区—>协议引擎   

零拷贝(Zero-Copy):一种高效的数据传输机制

  • mmap + write
  • sendfile

1、传统的数据传输方式(四次上下文切换,四次拷贝)

从某台机器将一份数据通过网络传输到另一台机器,通过Java语言简单描述就是:

public static void transfer() throws IOException {

    Socket socket = new Socket(HOST, PORT);
    InputStream in = new FileInputStream(FILE_PATH);
    OutputStream out = new DataOutputStream(socket.getOutputStream());

    byte[] buffer = new byte[1024];
    while (in.read(buffer) != -1) {
        // 将数据写到 Socket
        out.write(buffer);
    }

    out.close();
    socket.close();
    in.close();
}

虽然代码操作看起来很简单,但是深入到操作系统层面,就会发现实际的微观操作相当复杂;具体步骤:

  • JVM OS 发出 read() 系统调用,触发上下文切换,从用户态切换到内核态
  • 从外部存储(如:磁盘)读取文件内容,通过直接内存访问(DMA --- Direct Memory Access存入内核地址空间的缓冲区
  • 将数据从内核缓冲区拷贝到用户空间缓冲区,read() 系统调用返回,并从内核态切换回用户态
  • JVM OS 发出 write() 系统调用,触发上下文切换,从用户态切换到内核态
  • 将数据从用户缓冲区拷贝到内核中与目的地 Socket 关联的缓冲区
  • 数据最终经由 Socket 通过 DMA 传送到硬件(如:网卡)缓冲区,write() 系统调用返回,并从内核态切换回用户态

   

   

这个过程进行了四次上下文切换(模式切换),并且数据被来回拷贝了四次;但是真正消耗资源和浪费时间的是第2、3次;因为这两次都需要经过 CPU Copy 而且还需要内核态和用户态之间的来回切换。如果忽略系统的调用细节,整个过程可以通过下图表示:

上下文切换是CPU密集型的工作,数据拷贝是 I/O 密集型的工作

如果一次传输工作就像上面那样复杂的话,效率是相当低下的;零拷贝机制的目标就是消除冗余的上下文切换和数据拷贝,提高效率

   

2、零拷贝的数据传输方式

2.1mmap + write (内存映射)(四次上下文切换,三次数据拷贝)

替代原来的 read + write 方式,mmap 是一种内存映射文件的方式;mmap 通过内存映射,将文件映射到内核缓冲区;

同时,用户空间可以共享内核空间的数据mmap 允许程序直接在用户态中访问内核空间中的数据,这样能避免一次无意义的 Copy);建立共享映射后,就不需要从内核缓冲区拷贝到用户缓冲区了,这就避免了一次拷贝了

  • 进行映射拷贝,触发上下文切换,从用户态切换到内核态
  • 建立用户缓冲区和内核缓冲区的映射,从内核态切换回用户态
  • 进行数据发送,把数据通过 Socket 发送出去,从用户态切换到内核态
  • 直接把内核缓冲区的数据拷贝到 Socket 缓冲区中,然后拷贝到网络协议引擎里发送出去,系统调用返回,并从内核态切换回用户态

       

 

 

   

2.2sendfile(两次上下文切换,最少两次数据拷贝)

sendfile() 系统调用在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了数据在内核缓冲区和用户缓冲区之间的拷贝,操作效率很高

Linux 2.1 版本提供了 sendFile() 函数:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer 中;同时由于完全和用户态无关,就减少了一次上下文切换

  • sendfile() 系统调用,利用DMA 引擎将数据拷贝到内核缓冲区,从用户态切换到内核态
  • 数据被拷贝到 Socket 缓冲区
  • DMA 引擎将数据从内核 Socket 缓冲区中拷贝到协议引擎中
  • 系统调用返回,并从内核态切换回用户态

 

 

   

Linux 2.4 后,Socket 缓冲区做了调整,DMA 带收集功能,DMA 可以直接将内核缓冲区数据直接传输到协议引擎,消灭最后一次拷贝

  • sendfile() 系统调用,利用 DMA 引擎将数据拷贝到内核缓冲区,从用户态切换到内核态
  • 将带有文件位置和长度信息的缓冲区描述符添加到 Socket 缓冲区,此过程不需要将数据从内核缓冲区拷贝到 Socket 缓冲区中
  • DMA 引擎直接将数据从内核缓冲区中拷贝到协议引擎中,这样避免了最后一次数据拷贝
  • 系统调用返回,并从内核态切换回用户态

   

2.3mmap sendfile 的区别

  • 都是 Linux 内核提供,实现零拷贝的 API
  • mmap 适合小数据量读写,sendFile() 适合大文件传输
  • mmap 需要 4 次上下文切换,3 次数据拷贝
  • sendFile() 需要 3 次上下文切换,最少 2 次数据拷贝
  • sendFile() 可以利用DMA 方式,减少CPU 拷贝;mmap 则不能(必须从内存缓冲区拷贝到Socket 缓冲区)

基于此基础,RocketMQ 使用了 mmapKafka 使用了 sendFile()

posted @ 2020-08-29 10:22  暴脾气大大  阅读(995)  评论(0编辑  收藏  举报