程序员|为什么 P8 程序员的代码你写不出来?零拷贝了解一下( 三 )



Windows下也有一个作用类似的API:TransmitFile 。
这一系统调用的目的是在两个文件描述之间拷贝数据 , 但值得注意的是 , 数据拷贝的过程完全是在内核态完成 , 因此在网络服务器的这个例子中我们将把那两行代码简化为一行 , 也就是调用这里的sendfile 。
使用sendfile将节省两次数据拷贝 , 因为数据无需传输到用户态:

调用sendfile后 , 首先DMA机制会把数据从磁盘拷贝到内核buf中 , 接下来把数据从内核buf拷贝到相应的socket buf中 , 最后利用DMA机制将数据从socket buf拷贝到网卡中 。
我们可以看到 , 同使用传统的read/write相比少了一次数据拷贝 , 而且内核态和用户态的切换只有两次 。
有的同学可能已经看出了 , 这好像不是零拷贝吧 , 在内核中这不是还有一次从内核态buf到socket buf的数据拷贝吗?这次拷贝看上去也是没有必要的 。
的确如此 , 为解决这一问题 , 单纯的软件机制已经不够用了 , 我们需要硬件来帮一点忙 , 这就是DMA Gather Copy 。
sendfile 与DMA Gather Copy传统的DMA机制必须从一段连续的空间中传输数据 , 就像这样:

很显然 , 你需要在源头上把所有需要的数据都拷贝到一段连续的空间中:

现在肯定有同学会问 , 为什么不直接让DMA可以从多个源头收集数据呢?

这就是所谓的DMA Gather Copy 。
有了这一特性 , 无需再将内核文件buf中的数据拷贝到socket buf , 而是网卡利用DMA Gather Copy机制将消息头以及需要传输的数据等直接组装在一起发送出去 。
在这一机制的加持下 , CPU甚至完全不需要接触到需要传输的数据 , 而且程序利用sendfile编写的代码也无需任何改动 , 这进一步提升了程序性能 。

当前流行的消息中间件kafka就基于sendfile来高效传输文件 。
其实你应该已经看出来了 , 高效IO的秘诀其实很简单:尽量少让CPU参与进来 。
实际上sendfile的使用场景是比较受限的 , 大前提是用户态无需看到操作的数据 , 并且只能从文件描述符往socket中传输数据 , 而且DMA Gather Copy也需要硬件支持 , 那么有没有一种不依赖硬件特性同时又能在任意两个文件描述符之间以零拷贝方式高效传递数据的方法呢?
答案是肯定的!这就要说到Linux下的另一个系统调用了:splice 。
Splice这里还要再次强调一下不管是sendfile还是这里的splice系统调用 , 使用的大前提都是无需在用户态看到要传递的数据 。
让我们再来看一下传统的read/write方法 。
在这一方法下必须将数据从内核态拷贝的用户态 , 然后再从用户态拷贝回内核态 , 既然用户态无需对该数据有任何操作 , 那么为什么不让数据传输直接在内核态中进行呢?
现在目标有了 , 实现方法呢?
答案是借助Linux世界中用于进程间通信的管道 , pipe 。
还是以网络服务器为例 , DMA把数据从磁盘拷贝到文件buf , 然后将数据写入管道 , 当在再次调用splice后将数据从管道读入socket buf中 , 然后通过DMA发送出去 , 值得注意的是向管道写数据以及从管道读数据并没有真正的拷贝数据 , 而仅仅传递的是该数据相关的必要信息 。

你会看到 , splice和sendfile是很像的 , 实际上后来sendfile系统调用经过改造后就是基于splice实现的 , 既然有splice那么为什么还要保留sendfile呢?答案很简单 , 如果直接去掉sendfile , 那么之前依赖该系统调用的所有程序将无法正常运行 。
总结本文介绍了很多零拷贝的优化技巧 , 但是注意 , 一定要注意 , 如果你的程序对性能要没有到那种极度苛刻哪怕慢1ns都不行的时候 , 忘掉本文讲解的这些所谓优化技巧 , 老老实实用read/write , 相比这些所谓的技巧 , 内存拷贝没有那么糟糕 。