刨根问底零拷贝 Zero Copy

181 阅读4分钟

最原始的文件发送流程

  • 用户发起read()请求,由用户态切换到内核态
  • cup收到请求,发出指令给磁盘控制器,你去帮我读个文件(说完就干别的去了)
  • 磁盘控制器收到指令,将数据放入磁盘控制器的缓冲区,并向cpu报告(中断)
  • cpu收到报告,停下手头的工作,将磁盘控制器缓冲区里的数据拷贝到内核缓冲区
  • 然后将内核缓冲区的数据又拷贝应用程序缓冲区
  • 数据返回结束,此时由内核态切换回用户态

在以上的read()过程中,发生了2次用户态和内核态的切换,产生了两次数据的拷贝,其中cpu参与了两次(从磁盘缓冲区到内核缓区,从内核缓冲区到应用程序缓冲区)

DMA 技术技术下的文件发送流程

什么是DMA技术,Direct Memory Access 直接内存访问,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情。

  • 依旧是用户发起read()请求,用户态切换到内核态
  • cup收到请求,发出指令给DMA驱动,你去帮我读个文件(说完就干别的去了)
  • DMA控制器收到指令,继续将指令发送给磁盘控制器
  • 磁盘控制器收到指令,将数据放入磁盘控制器的缓冲区,并向DMA报告(中断)
  • DMA收到报告,将磁盘控制器缓冲区里的数据拷贝到内核缓冲区,这个过程不需要cpu参与
  • 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU
  • CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回,此时由内核态切换回用户态

在以上的read()过程中,发生了2次用户态和内核态的切换,产生了两次数据的拷贝,一次DMA拷贝,一次CPU拷贝

读完之后就是写,写的过程如下:

  • 应用程序要向网卡写数据,调用write()函数实现用户态切换内核态,这是第1次切换;

  • CPU将用户缓冲区数据拷贝到内核缓冲区,这是第1次CPU拷贝;

  • DMA控制器将数据从内核缓冲区复制到socket缓冲区,这是第1次DMA拷贝;

  • 完成拷贝之后,write()函数返回实现内核态切换用户态,这是第2次切换; 综上所述:

  • 读过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝;

  • 写过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝;

image.png

零拷贝技术下的文件发送流程

经过分析,如果仅仅是发送文件,不需要对文件内容进行处理,文件可以不用经过应用缓冲区,也就是内核缓冲区到应用程序缓冲区的拷贝和应用程序缓冲区到套接字缓冲区的拷贝完全是没必要的,零拷贝技术闪亮登场。

目前来看,零拷贝技术的几个实现手段包括:mmap+write、sendfile、sendfile+DMA收集、splice等

mmap+write

mmap是Linux提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享。

这样就减少了一次用户态和内核态的CPU拷贝,但是在内核空间内仍然有一次CPU拷贝。

image.png

sendfile

sendfile系统调用是在 Linux 内核2.1版本中被引入,它建立了两个文件之间的传输通道。

sendfile方式只使用一个函数就可以完成之前的read+write 和 mmap+write的功能,这样就少了2次状态切换,由于数据不经过用户缓冲区,因此该数据无法被修改。

image.png

从图中可以看到,应用程序只需要调用sendfile函数即可完成,只有2次状态切换、1次CPU拷贝、2次DMA拷贝。

但是sendfile在内核缓冲区和socket缓冲区仍然存在一次CPU拷贝,或许这个还可以优化。

sendfile+DMA收集

Linux 2.4 内核对 sendfile 系统调用进行优化,但是需要硬件DMA控制器的配合。

升级后的sendfile将内核空间缓冲区中对应的数据描述信息(文件描述符、地址偏移量等信息)记录到socket缓冲区中。

DMA控制器根据socket缓冲区中的地址和偏移量将数据从内核缓冲区拷贝到网卡中,从而省去了内核空间中仅剩1次CPU拷贝。

image.png

零拷贝应用场景

mmap+write适用于小数据量读写场景,而sendFile适用于不需要对待发送文件进行修改,大文件传输的场景

RocketMq底层发送数据用的是mmap+write,而kafka用的是sendfile

参考资料

www.linuxjournal.com/article/634…

zhuanlan.zhihu.com/p/377237946