在 讲文件读写策略前,我们先讲一下普通文件的传输机制
普通文件传输机制
设 用户进程A 需要发送一份文件给 主机B
第一步: 用户进程发起read 调用 ,进程由用户态转为内核态,并阻塞自身
第二步: 内核 查看该文件是否在缓存中
第三步: (假设文件不在缓存中)内核去读取磁盘文件并驱动DMA 加载到内核的页缓存
第四步: 将页面缓存的文件拷贝到用户进程空间,并唤醒进程
第五步: 用户进程发起write 调用,进程又由用户态切换为内核态,并阻塞自身
第六步: 内核将用户进程的文件拷贝到用户进程对应的socket 缓冲区中
第六步: 内核将缓冲区的文件经过DMA 拷贝发送到网络中
这里面经过了2次cpu 拷贝, 2次DMA 拷贝,2次系统调用,4次进程状态转换(上下文切换)
Zero-copy
观察普通的文件传输机制后,可以明显看到有多余的拷贝。为什么不直接共用相同的缓存呢? 或者为什么非要内核进程拷贝给用户进程,不能用户进程直接自己去获取吗?
针对这两种问题,Zero-copy 有多种技术方案。
1. 直接IO (无需内核参与拷贝)
直接IO的意思是 应用程序可以直接访问硬件设备的存储,操作系统内核除了进行必要的虚拟存储配置工作之外,不参与数据传输过程中的其它任何事情。直接 I/O 使得数据可以直接在应用程序和外围设备之间进行传输,完全不需要操作系统内核页缓存的支持。
2. 无需应用程序参与的mmap()、send_file()、slipce() 调用
mmap
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享
mmap的缺点:
1.文件如果很小,是小于4096字节的,比如10字节,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有10字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域的是4096个字节,11~4096的字节部分用零填充。因此如果连续mmap小文件,会浪费内存空间。
- 对变长文件不适合,文件无法完成拓展,因为mmap到内存的时候,你所能够操作的范围就确定了。
3.如果更新文件的操作很多,会触发大量的脏页回写及由此引发的随机IO上。所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快。
经过mmap技术,用户空间缓存和内核缓存之间的拷贝过程就省略掉了。
send_file
如果用户进程只是请求将文件进行转发等,自身并不操作文件。那么其实可以不用拷贝给用户进程。
Zero-copy 针对这个问题,把原先的cpu 拷贝整个文件的过程改为只传输文件描述符。 这里需要支持DMA 收集拷贝功能。使用该功能可以驱动DMA 自行读取文件数据并直接拷贝到网络协议引擎中。
slipce
splice() 可以被看成是类似于基于流的管道的实现,可以使得两个文件描述符相互连接,splice 的调用者则可以控制两个设备(或者协议栈)在操作系统内核中的相互连接。
适用场景: splice() 可以在操作系统地址空间中整块地移动数据,从而减少大多数数据拷贝操作。splice() 适用于可以确定数据传输路径的用户应用程序,它不需要利用用户地址空间的缓冲区进行显式的数据传输操作。那么,当数据只是从一个地方传送到另一个地方,过程中所传输的数据不需要经过用户应用程序的处理的时候,spice() 就成为了一种比较好的选择。
联系:用户应用进程必须拥有两个已经打开的文件描述符,一个用于表示输入设备,一个用于表示输出设备。
区别:splice() 允许任意两个文件之间互相连接,sendfile()只适用于文件到 socket 进行数据传输。
3. 写时复制(减少内核到用户进程间的拷贝)
以上我们讲的都是传输文件,且只有一个进程参与。在文件被多个进程读写时,操作系统使用写时复制策略来减少大量的文件复制。(避免给每个进程的用户空间都复制一份)
写入时复制(CopyOnWrite,简称COW)核心思想是,如果有多个调用者(Callers)同时访问相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
通俗易懂的讲,写入时复制技术就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。