一、什么是拷贝
之前有一篇文章说过拷贝:
- 广义上讲就是任何数据复制,但是在Linux系统中比较特殊,这里的概念比较狭义,物理内存之间的数据复制才叫拷贝
- IO设备、Input 、DMA(Direct Memory Access)与其他设备之间的数据复制不能称为拷贝
为什么要着重强调这个概念呢?
因为很多博客中讲sendfile、mmap,多次拷贝的时候出现了描述矛盾的问题。
二、BufferedOutputStream/BufferedInputStream 传统拷贝
使用缓存,默认大小为8K,主要可以减少内核态与用户态的切换导致的性能损耗
传统IO拷贝
整个过程经过2次CPU拷贝,DMA(Direct Memory Access) 的拷贝是输入输出IO,因此这里我们不能算作拷贝。
上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作。下面说说他们的步骤:
- read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。
- 发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
- 发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
- 第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
- write 方法返回,再次从内核态切换到用户态。
二、FileChannel 拷贝
- transformFrom
- transformTo
内部同时支持两种拷贝
Android 6.0包括6.0之前的的版本,如果是一般拷贝,通过的是mmap
Android 7.0 之后的版本,默认选择sendfile,sendfile无法支持时使用mmap
同样,这两个种实现在Android 6.0也是支持的
android.system.Os提供了native相关接口
mmap拷贝
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图:
上图CPU之间只出现了一次拷贝
如上图,user buffer 和 kernel buffer 共享内存。如果你想把硬盘的文件传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 Socket 缓冲区。
现在,你只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。
sendfile 零拷贝
那么,我们还能继续优化吗? Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
Linux 2.1 sendfile仍然是1次拷贝,因为Socket Buffer也在内存区域
如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为都在内核空间。
最后,数据从 Socket 缓冲区进入到协议栈。此时,数据经过了 3 次拷贝,3 次上下文切换。那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?
实际上,Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图:
现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 SocketBuffer,基本无消耗。
等一下,不是说零拷贝吗?为什么还是要 2 次拷贝?
首先我们说零拷贝,是从操作系统的角度来说的,只有内存间的拷贝叫做拷贝。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。例如我们刚开始的例子,内核缓存区和 Socket 缓冲区的数据就是重复的。
而零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
再稍微讲讲 mmap 和 sendFile 的区别。
- mmap 适合小数据量读写,sendFile 适合大文件传输。
- mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
- sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
四、rename拷贝
File rename可以实现最快的拷贝,因为他的实现方式是修改系统文件目录链接完成,但是在android中,此方法存在限制
限制:Cross Device link ,不允许跨设备link,所谓跨设备,更严格的是不准跨根目录拷贝