今日看rocketMQ的设计,读到如何提高broker的发送性能,看到了使用mmp(文件内存映射)的技术,零拷贝,提高了数据从文件到内存再到网络的传输效率。这里有点迷糊,所以详细google了一把。特写下来留作记录。
内容都是来自于已有的东西,都放到后文的参考文档里面,参考文档比我详细多了,有图有解释,有理论,甚至有代码,如果能看参考文档的原文的话,建议看原文。
概念澄清
- “零拷贝”:在整个发送数据过程中,数据的复制是必不可少的,这里数据复制分两种类型,一种是CPU参与的一个字节一个字节处理的数据复制,一个是CPU不用参与,通过专有硬件DMA参与的,批量数据复制。自然,不用CPU参与的数据复制性能高。而“零拷贝”所说的拷贝,其实指的是,减少CPU参与的数据拷贝,最好减少到零次,但是各种实现方式里,很多种都只是减少一次两次,并没有直接让CPU参与的数据复制数归零。
为什么要多次拷贝
首先,操作系统是分用户内存空间和内核内存空间的,区分的原因是因为,很多操作系统的指令,安全级别很高,不是随随便便就能调用的,对应的,一部分归操作系统使用的内存空间安全级别也很高,不是随随便便就能访问的,二者都是操作系统才能操作的“内核态”。“内核态”的指令,叫做系统调用,“内核态”的内存空间,成为内核空间。
从硬盘读文件的过程,然后通过网络发送出去,一般的操作,需要经过:
拷贝流程
用户空间->(1. 读文件,系统调用,硬盘文件从硬件缓存复制到内核空间,使用DMA)
->内核空间->(2. 用户程序读取文件数据,使用CPU)
->用户空间->(3. 通过socket发送文件到网络,系统调用,数据复制回内核空间socket关联的内存,使用DMA)
->内核空间->(4. 数据复制到网卡驱动硬件的缓存,系统调用,使用DMA,执行完成)
.
这样三次切换。更具体和形象的流程,请看参考文档的第一个,里面对这个过程,以及零拷贝优化后的过程,做了详细的图片说明。
每一次切换,数据都需要在内核空间和用户空间之间拷贝。在这个流程中,上述的2,3两步是不经过DMA,需要CPU参与的。
减少拷贝的方式
这里把理论思路和实现方式放到一起说。
mmp 文件内存映射
这是Java NIO实现的一个鼎鼎大名的做法,MappedByteBuffer。思路是将内核空间的数据和用户空间共享,进而减少上述流程中的第2步,内核空间到用户空间的数据拷贝,但是第3步,复制到socket关联的内核空间还是需要的,也就是内核空间之间还要CPU参与,拷贝一次。但是在内核角度看,它不再需要向用户控件拷贝数据了。
sendFile方式
这种方式在Java NIO中也有实现,是FileChannel的transferTo,transferFrom方法。思路是将内核空间内的数据直接拷贝,没用户空间啥事儿,,还是省了上述流程的第2步,但依旧没解决内核空间之间CPU参与的数据拷贝。
优化的sendFile方式
内核空间保留两份数据,之间要相互拷贝是违反直觉的。这个原因在下面的前两个参考文档内都有解答,是因为DMA操作内存要求是整块的内存。内核空间的数据(包括数据和数据对应的描述符)已经不是整块内存存储了,所以不行。
但是这个问题随着内核和硬件的升级也有改善,在新内核,参考文档说是linux kernel 2.4之后,可以支持“零存整取”的DMA,这里“零存整取”是我找的词儿,对应英语是“scatter/gather”,一般被称为“收集拷贝功能”,意为从多个内存块上取数据,然后汇集到另外一块内存块里去。
通过这个特性,在内核空间的一次数据拷贝(上述流程的第3步),也不需要了,内核空间只用保存一份数据,数据的描述符拷贝到网络协议的内核控件去,然后直接通过DMA将二者灌到网卡驱动的缓存里。
参考文档
- Zero Copy I: User-Mode Perspective 图文并茂
- It’s all about buffers: zero-copy, mmap and Java NIO 结合代码
- 浅谈 Linux下的零拷贝机制 中文,且补充了上述两个文档的一些内容