【零拷贝】深度解读

前言:

  很多应用会用到磁盘,磁盘是一种比较好的持久化工具,例如:MQ,MySql,Log等都会用到磁盘,所以磁盘性能的优化显得比较重要,这部分的知识的学习也是必要的;磁盘用的好的时候,其实速度也是很快的,下面开始介绍一种磁盘常用技术,零拷贝

概念:

  DMA:直存储器访问,其实很多和内存交互并不需要CPU的,可以通过硬件的支持直接从外设和内存交互,就是DMA技术;

  用户态:程序控制权在用户代码上,注意,切换到用户态是具有一定的代价的;

  内核态:程序控制权在操作系统代码上,注意,切换到内核态也是具有一定的代价的;

问题:

  假设我们需要读取磁盘文件到网络,我们可能会写以下代码:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

  这段代码,看着简单,实际上代价是很大的,其中就包括了4次内存的拷贝,如下图所示:

  1. 首先是read,触发了系统调用,程序从用户态进入内核态,发起DMA调用,从硬盘复制到内核缓冲区;
  2. 然后内核缓冲区完成后,内核触发一个CPU copy,从内核缓存复制到用户buffer,这是第二次拷贝,这个时候read函数完成,程序从内核态转换到用户态;
  3. 用户态开始调用write,系统发生CPU copy,把用户buffer复制到socket buffer,代码切换到内核态;
  4. 内核态开始继续用DMA,开始进行socket buffer到协议引擎;

 

【零拷贝】深度解读

 

  可以看见,这种方案还是比较有多余的,但这样设计也是因为硬件和软件能力和架构问题,那么能不能直接一点从内核直接发送数据呢?当然可以,但肯定需要系统的特殊方式支持,下面就介绍2种方式,第一种:

MMAP:

  该技术通过调用mmap代替调用read,如下面的例子所示:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

  看起来代码感觉和read一样,但其实里面做的事情是不同的,主要的是read是把缓存读取了出来,而mmap不然,他是通过映射文件到一个共享内存中,这块内存,由内核态和用户态共享,过程大概是下图所示这样的:

  1. 首先,调用第一个方法,程序从用户态转换到内核态,采用DMA技术把文件copy到内核buffer中;
  2. 然后返回用户态,用户态拿到了一个共享的mmap文件引用,接着调用write;
  3. 系统继续切换内核态,write方法,从mmap中CPU copy到了socket buffer中;
  4. 最后内核态的程序继续用DMA,发送数据;

【零拷贝】深度解读

 

  可以看出,其实mmap是减少了一半的copy量,只是因为mmap技术,可以使得用户态和内核态的隔离变得没那么死板了。至于为何要进行用户态和内核态的隔离,这就是设计和安全方面的考虑了吧;所以,涉及安全问题,证明这样做是有代价的,我们来讨论下,mmap的代价。

  陷阱:用了mmap,代表你这个文件是被用户和内核共享的,那么就有一个线程安全问题,如果你在进行write的时候,mmap文件被另一个用户进程删掉了那么会怎么样呢?你会因为进行一个错误的存储访问而被信号 SIGBUS 中断;默认情况下是会中断进程的,解决方案有两种:

  • 为该信号注册一个做简单返回处理的handler,但这样会使得write方法返回已经写的字节数,所以这方案是不可取的;
  • 第二种是从内核中获取 file leasing (文件租约),该方案通过从系统Leasing一个文件描述符,当文件被第三方删除的时候,你会提前收到一个实时的信号 RT_SIGNAL_LEASE ,这样你的程序也会被中断,但不至于会被SIGBUS一样被杀死;

SEND FILE

  Linux2.1内核之后,出现了一种新的方法,sendfile,该方法的牛皮之处不仅仅在于减少copy,而且还减少了用户态和内核态的切换,因为他真的只有一个方法调用,后面完全是内核的工作;

sendfile(socket, file, len);

  过程大概如下图这:

  1. 首先调用方法,用户态进入内核态执行sendfile;
  2. 内核态直接DMA,复制到内核buffer,然后直接从内核buffer CPU copy到socket buffer;
  3. 返回用户态(因为这部分是异步flush),最后DMA copy到协议引擎,一气呵成;

【零拷贝】深度解读

 

  看到这里,我们会有疑问,如果copy的时候,其他用户进程又来删掉了呢?其实这里也差不多,唯一不同的就是不会被 SIGBUS信号中断杀死,毕竟这是内核的事情,所以也是需要leasing RT_SIGNAL_LEASE才能正常工作的哦;

SEND FILE

   前面的介绍,怎么说都有一次CUP copy,那么能不能上面的唯一一次cpu copy也去掉呢?其实也是可以的,但需要socket方面的支持了;这就是2.4版本 kernel中了;其实就是改进版本的send file所以,用法也没变。

sendfile(socket, file, len)

  它的工作过程大概是:

  1. 首先调用方法,用户态进入内核态执行sendfile;
  2. 内核态直接DMA,复制到内核buffer,但这次不再cpu copy到 socket buffer中,而是把一些文件的描述符,长度等信息append到socket buffer中;
  3. 返回用户态,后面内核中,socket方法的内核调用DMA直接从在kernel buffer中 收集文件内容到协议引擎中;

【零拷贝】深度解读

 

 

总结:

  毕竟以上方法也是有一次copy的,有人说这也不算是 zero copy,其实零拷贝是对于内核而言的,内核中没有进行多余的copy,这也就够了,减少了cpu copy是节省了很多时间的;但是不是mmap就比sendfile差呢?其实不然,这两者是各有优点的,对比如下:

  1. 使用 mmap + write 方式 优点:即使频繁调用,使用小块文件传输,效率也很高 缺点:不能很好的利用 DMA 方式,会比 sendfile 多消耗 CPU,内存安全性控制复杂,需要避免 JVM Crash 问题。
  2. 使用 sendfile 方式 优点:可以利用 DMA 方式,消耗 CPU 较少,大块文件传输效率高,无内存安全新问题。 缺点:小块文件效率低于 mmap 方式,只能是 BIO 方式传输,不能使用 NIO。

  当然了,我本人也没有研究那么深的水平,这篇文章参考的国外一篇博文,尊重作者我把链接放下,读者可以帮我纠正一下我翻译不对的地方,一起学习,谢谢;

 

  参考文献:https://www.linuxjournal.com/article/6345

 

上一篇:nn.moduleList 和Sequential


下一篇:操作系统(十五)——mmap