零拷贝zero-copy

 一、名词介绍

  内核:操作系统的核心

  用户空间:指的是用户进程的运行空间

  内核空间:指的是内核的运行空间,是受保护的内存空间

  用户态:如果进程运行在用户空间就是用户态

  内核态:如果进程运行在内核空间就是内核态

  DMA拷贝

    对一个IO操作而言,是通过CPU发出对应的指令来完成的,但是相比CPU来说,IO的速度太慢了,CPU有大量的时间处于等待IO状态。

    因此就产生了DMA(Direct Memory Access):直接内存访问技术。是一种无需CPU的参与就可以让IO设备与系统内存之间进行双向数据传输机制。

    使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率。DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发的执行其他任务。当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,由CPU执行相应的中断服务程序进行后续处理。

    本质上说DMA控制器就是主板上一块独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少CPU的等待时间。

   

  上下文切换:内核在CPU上对进程或者线程进行切换。基本原理是,当发生任务切换时,保存当前任务的寄存器到进程控制块(PCB,Process Control Block)中,将下一个即将要切换过来的任务的寄存器状态恢复到当前CPU寄存器中,使其继续执行,同一时刻只允许一个任务独享寄存器。

    上下文的切换流程如下 :

    (1)挂起一个进程,将这个进程在CPU中的状态(上下文信息)存储于内存的PCB中。 
    (2)在PCB中检索下一个进程的上下文并将其在CPU的寄存器中恢复。 
    (3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行)并恢复该进程

 

  从Linux系统上看,除了引导系统的BIN区,整个内存空间主要分为两个部分:内核空间、用户空间

    内核空间是Linux自身使用的内存空间,主要提供给程序调度、内存分配、连接硬件等程序逻辑使用;

    用户空间则是提供给各个进程的主要空间。用户空间不具有访问内核空间资源的权限,因此如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成(也就是调用操作系统内核提供的API):进行上下文切换,从用户空间切换到内核空间,然后在完成相关操作后再从内核空间切换回用户空间。

 

  当程序请求网络数据的时候,需要经历两次拷贝:

    ①:程序需要等待数据从网卡拷贝到内核空间

    ②:因为用户程序无法访问内核空间,所以内核又得把数据拷贝到用户空间,这样处于用户空间的程序才能访问这个数据

 

 

 二、传统IO和虚拟内存技术

  1、传统IO方式

    传统的IO方式,底层实际上通过调用read()和write()来实现。

      ①、read:通过read() 把数据从硬盘读取到内核缓冲区,再拷贝到用户缓冲区;

      ②、write:然后再通过write()写入到Socket缓冲区,最后写入网卡设备。

    

      

 

    DMA Copy不需要CPU全程参与,CPU Copy需要CPU全程参与。

    读操作:

      ①:用户进程通过read()方法向操作系统发起调用;此时上下文从用户态切换为内核态

      ②:DMA控制器把数据从硬盘中拷贝到内核的读缓存区,发生一次DMA Copy

      ③:CPU把内核读缓存区数据拷贝到应用缓冲区,发生了一次CPU Copy

      ④:read调用返回后,上下文再次从内核态切换为用户态

      发生两次上下文切换,两次Copy

 

    写操作:

      ①:用户进程通过write()方法发起调用,上下文从用户态切换为内核态 

      ②:CPU将应用缓冲区的数据Copy到内核的Socket缓冲区

      ③:DMA控制器把数据从Socket缓冲区Copy到网卡(或其他存储设备)

      ④:写入结束后返回,上下文从内核态切换回用户态

      发生两次上下文切换,两次Copy

 

    传统的IO读写操作,总共进行了4次上下文切换,4次Copy动作。数据在内核空间和应用空间之间来回复制,严重消耗资源。

 

    4次Copy如下:

      ①:读取磁盘文件到操作系统内核缓冲区(DMA Copy)

      ②:将内核缓冲区的数据Copy到应用程序缓冲区(CPU Copy)

      ③:将应用程序缓冲区的数据,Copy到内核Socket网络发送缓冲区(CPU Copy)

      ④:将内核Socket缓冲区的数据Copy到网卡(DMA Copy)

 

    注意:为了程序和系统安全考虑,操作系统为每个应用程序都设计了用户内存,且用户内存和内核内存是隔离的。这也是需要从应用程序缓冲区到内核缓冲区来回复制的原因了。

  

 

  2、虚拟内存技术

    虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行交换。

    背景:电脑中所运行的程序均需由经过内存执行,若执行的程序占用内存很大或很多,则会导致内存消耗殆尽。为解决该问题,window中运用了虚拟内存技术(linux中成为交换空间),即匀出一部分硬盘空间来充当内存使用。当内存耗尽时,电脑就会自动调用硬盘来充当内存,以缓解内存的紧张。

    所有现代操作系统都使用虚拟内存,使用虚拟地址取代物理地址,这样做的好处就是:

      1、多个虚拟内存可以指向同一个物理地址

      2、虚拟内存空间可以远远大于物理内存空间

   使用虚拟内存技术将内核空间和用户空间的虚拟地址映射到同一个物理地址,这样就不用在程序和内核之间来回复制。传统的IO操作经过虚拟地址优化后就省去了上面的CPU copy:

   

 

 

 三、零拷贝

  零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。

  简单来说就是减少和避免不必要的CPU数据拷贝过程,从而减少拷贝的CPU的开销及用户态和内核态之间的切换次数,从而优化数据传输的性能。

  这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽

 

  零拷贝实现思路:

    1)直接IO:数据直接跨过内核,在用户空间和IO设备之间传递,内核只是进行必要的虚拟存储配置等辅助工作

    2)避免内核和用户空间之间的数据拷贝:当用户空间不需要对数据进行访问时,则可以避免将数据从内核空间拷贝到用户空间(如sendfile方式)

    3)写时复制:数据不需要提前拷贝,而是当需要修改的时候再进行部分拷贝

 

  零拷贝实现方式

    零拷贝并不是没有拷贝数据,而是减少用户态-内核态的切换次数以及CPU拷贝的次数。有以下实现方式

    ①:mmap+write

    ②:sendfile

    ③:带有DMA收集功能的sendfile

 

  1、 mmap+write方式

    mmap:一种内存映射文件的方法,将一个或者其他对象映射进内存

    mmap函数原型:

      void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

        start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。
        length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理
        prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

    使用mmap+write方式替换原来的传统IO方式,就是利用了虚拟内存的特性,它将内核中的读缓冲区与用户空间的缓冲区进行映射,所有的IO都在内核完成

    mmap+write的IO读写过程:

    

      

    ①:用户进程通过mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
    ②:CPU利用DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
    ③:上下文从内核态切换回用户态,mmap方法返回。
    ④:用户进程通过write方法向操作系统内核发起IO调用,上下文从用户态切换为内核态
    ⑤:CPU将内核缓冲区的数据拷贝到的socket缓冲区。
    ⑥:CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回

      整体流程的核心区别就是,把数据读取到内核缓冲区后,应用程序进行写入操作时,直接把内核的Read Buffer的数据复制到Socket Buffer以便进行写入,这次内核之间的复制也是需要CPU参与的。

    与传统的IO流程相比,少了一次CPU Copy,不过上下文切换还是4次(4次上下文切换,3次Copy

    mmap+write方式减少了CPU的使用,适用于大文件的传输。

 

    

  2、sendfile方式

    sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝。

    相比mmap来说,sendfile同样减少了一次CPU Copy,而且减少了2次上下文切换。

    sendfile流程如下:

    

    整个过程发生了2次用户态和内核态的上下文切换和3次拷贝,具体流程如下:

     1)用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
     2)DMA控制器把数据从硬盘中拷贝到读缓冲区
     3)CPU将读缓冲区中数据拷贝到socket缓冲区
     4)DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile调用返回


    sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器

 

  3、sendfile+DMA scatter/gather实现的零拷贝

    linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。

    使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝

    sendfile+DMA scatter/gather实现的零拷贝流程如下:     

      ①:用户进程发起sendfile系统调用,上下文从用户态切换到核心态

      ②:DMA控制器,把数据从硬盘中拷贝到内核缓冲区

      ③:CPU把内核缓冲区的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区

      ④:DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡

      ⑤:上下文从内核态切换到用户态,sendfile调用返回

    可以发现,sendfile+DMA scatter/gather实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的

      

 

 

  应用:

   Java NIO

    ①:Java NIO有一个MappedByteBuffer的类,可以用来实现内存映射。它的底层是调用了Linux内核的mmap的API

    ②:FileChannel的transferTo()/transferFrom(),底层就是sendfile() 系统调用函数

   RocketMQ和Kafka都使用了零拷贝的技术

    对于MQ,整个流程为:生产者发送数据到MQ,然后持久化到磁盘,之后消费者从磁盘读取数据。

    对于RocketMQ来说这两个步骤使用的是mmap+write,而Kafka则是使用mmap+write持久化数据,发送数据使用sendfile

    

 

  总结:

    由于CPU和IO速度的差异问题,产生了DMA技术,通过DMA搬运来减少CPU的等待时间。

    传统的IO read+write方式会产生2次DMA拷贝+2次CPU拷贝,同时有4次上下文切换。

    而通过mmap+write方式则产生2次DMA拷贝+1次CPU拷贝,4次上下文切换,通过内存映射减少了一次CPU拷贝,可以减少内存使用,适合大文件的传输。

    sendfile方式是新增的一个系统调用函数,产生2次DMA拷贝+1次CPU拷贝,但是只有2次上下文切换。因为只有一次调用,减少了上下文的切换,但是用户空间对IO数据不可见,适用于静态文件服务器

  

 

  参考:

  https://mp.weixin.qq.com/s/yvenw3P2JvvSxWodBNNcMw

  https://www.jianshu.com/p/e76e3580e356

  https://blog.51cto.com/12182612/2424692?source=dra

 

END.

posted @ 2021-03-25 10:12  杨岂  阅读(642)  评论(0编辑  收藏  举报