一、零拷贝方式
针对以上三个方向进行优化分别产生了不同的零拷贝方式
1. mmap(RocketMQ)
基于 mmap + write 系统调用的零拷贝方式,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝
2. sendFile(kafka)
基于 sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝
3. DMA Gather Copy 概念引入
依赖与 sendFile,相比sendFile 减少了 CPU拷贝,需要硬件支持整个拷贝过程 2 次 DMA 拷贝
4. splice引入
借助Linux世界中用于进程间通信的管道,pipe整个拷贝过程 2 次 DMA 拷贝
二、针对不同方式讲解
1. mmap概念引入
mmap仅仅将文件内容映射到了进程地址空间中,并没有真正的拷贝到进程地址空间,这节省了一次从内核态到用户态的数据拷贝。
尽管mmap可以节省数据拷贝,但维护文件与地址空间的映射关系也是有代价的,除非CPU拷贝数据的时间超过维系映射关系的代价,否则基于mmap的程序性能可能不及传统的read/write。
此外,如果映射的文件被其它进程截断,在Linux系统下你的进程将立即接收到SIGBUS信号,因此这种异常情况也需要正确处理。
mmap+write拷贝流程
基于 mmap + write 系统调用的零拷贝方式,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝
缺陷:mmap 主要的用处是提高 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费,因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存。
2. sendFile概念引入
你没有看错,在Linux系统下为了解决数据拷贝问题专门设计了这一系统调用。sendfile 系统调用的引入,不仅减少了 CPU 拷贝的次数,还减少了上下文切换的次数
基于 sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝
调用sendfile后,首先DMA机制会把数据从磁盘拷贝到内核buf中,接下来把数据从内核buf拷贝到相应的socket buf中,最后利用DMA机制将数据从socket buf拷贝到网卡中。
我们可以看到,同使用传统的read/write相比少了一次数据拷贝,而且内核态和用户态的切换只有两次。
有的同学可能已经看出了,这好像不是零拷贝吧,在内核中这不是还有一次从内核态buf到socket buf的数据拷贝吗?这次拷贝看上去也是没有必要的。
的确如此,为解决这一问题,单纯的软件机制已经不够用了,我们需要硬件来帮一点忙,这就是DMA Gather Copy。
3. DMA Gather Copy 概念引入
有了这一特性,无需再将内核文件buf中的数据拷贝到socket buf,而是网卡利用DMA Gather Copy机制将消息头以及需要传输的数据等直接组装在一起发送出去。
在这一机制的加持下,CPU甚至完全不需要接触到需要传输的数据,而且程序利用sendfile编写的代码也无需任何改动,这进一步提升了程序性能。
实际上sendfile的使用场景是比较受限的,大前提是用户态无需看到操作的数据,并且只能从文件描述符往socket中传输数据,而且DMA Gather Copy也需要硬件支持,那么有没有一种不依赖硬件特性同时又能在任意两个文件描述符之间以零拷贝方式高效传递数据的方法呢?
答案是肯定的!这就要说到Linux下的另一个系统调用了:splice。
4. splice引入
在这一方法下必须将数据从内核态拷贝的用户态,然后在从用户态拷贝回内核态,既然用户态无需对该数据有任何操作,那么为什么不让数据传输直接在内核态中进行呢?
现在目标有了,实现方法呢?
答案是借助Linux世界中用于进程间通信的管道,pipe。
零拷贝解决的问题是服务器中数据的读写性能问题。
参考: