前言
经常听到到“零拷贝”,望文生义认为就是文件传输时候没有发生拷贝行为,全靠意念传输…
其实零拷贝含义是:不使用 CPU 来进行数据复制,不在用户态进行数据移动,使用 DMA 芯片代替 CPU 进行数据复制和移动。
知识预热
在了解零拷贝之前我们需要先了解用户态和内核态、中断上下文、DMA等知识点。
用户态和内核态
通常 CPU 会分为几个权限等级。用常用的 inter x86 架构举例,一共会分为 0~3共4个界别(数字越小权限越大)。不过在 unix/Linux 系统中只会使用到 0 和 3 两个级别。
所以通常进程运行在 0 级别称之为运行在内核态,运行在 3 级别称之为运行在用户态。Linux 中进程一般有两个栈,分别使用在内核态和用户态。进程中有页表分别指向内核态和用户态空间,故俩着使用的内存空间隔离的,所以有了上下文切换(见下一下节)。
在进程的生命周期中,大多数时间是运行在用户态中。处于内核态可直接切换到用户态,但用户切换到内核态通常是使用系统调用或者是shell。
例如:程序需要读写文件,则需要调用内核提供的 read()、write() 接口。
通常内核态和用户态状态切换有以下几种:
- 系统调用 如 read()、write() 、fork()…
- 异常 如常见的缺页异常
- 中断 这里指硬中断。(中断是一种电信号,当设备有某种事件发生时,它就会产生中断,通过总线把电信号发送给中断控制器。)
补充知识点:
软中断是执行中断指令产生的,而硬中断是由外设引发的。
硬中断的中断号是由中断控制器提供的,软中断的中断号由指令直接指出,无需使用中断控制器。
硬中断是可屏蔽的,软中断不可屏蔽。
硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部。
软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部。
上下文
上高中时候,语文课中有道大题是阅读理解。里面有几个小题是摘取一段话,让你分析表达作者什么心理活动巴拉巴拉这些。如果只有这段话,想必只有神才知道作者在想啥。所以要结合这句话的上下文一起来分析,不过通常只用到上文。在这里上下文可以理解为一种语境。
在程序开发中对上下文有不同的解释,这里的上下文指线程在切换中需要保存的和恢复所需的信息。一个线程被抢占剥夺 CPU 的使用权从而被挂起,这种情况叫做“切出”。 一个线程被选中占用 CPU 从而继续运行,叫作“切入”。所以保存切出、切入的信息就叫上下文了。
DMA (直接存储器访问)
DMA 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。
在实现DMA传输时,是由DMA控制器直接掌管总线,因此,存在着一个总线控制权转移问题。即DMA传输前,CPU要把总线控制权交给DMA控制器,而在结束DMA传输后,DMA控制器应立即把总线控制权再交回给CPU。一个完整的DMA传输过程必须经过DMA请求、DMA响应、DMA传输、DMA结束4个步骤。
零拷贝
传统 I/O 模型
在 DMA 技术出现之前,一个读操作的过程大致如下图:
- 进程使用系统调用 read() ,CPU 向磁盘控制器发起指令,然后返回。
- 磁盘准备好数据以后,将数据存储在控制缓冲区内并 CPU 发起一个硬中断。
- CPU 收到中断信号,将磁盘控制缓冲区数据拷贝到页缓冲区(page cache), 再把数据拷贝到用户态缓冲区。
整个过程中存在: 多次拷贝同一份数据、cpu 参与数据搬移、多次内核态和用户态的切换等现象。当数据量大的时候这种模型显然是有缺陷的,所以 DMA 技术出现了。
DMA I/O 模型
如下图所示:
- 进程使用 read() 系统调用,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态。
- CPU 向 DMA 发起 I/O 请求后返回,由 DMA 继续向磁盘发起 I/O 请求。
- 磁盘准备好数据以后,将数据存储在控制缓冲区内并 DMA 发起一个硬中断。
- DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中。
- 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU。
- CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回。
上图中加多了 DMA 后 CPU 减少了俩次数据拷贝工作,但是还是发生了拷贝且同一份数据多次拷贝和用户态与内核态的上下文切换。
如果想加速机器的性能,那必须在这两点上需要做更多的优化,我们接着往下看。
实现零拷贝
我们再来看一下传统的 I/O 模型实现一个简单的文件传输过程:首先将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
如下图:
如上图所示:一共发生了4 次用户态与内核态的上下文切换、4 次数据重复拷贝。所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
读优化 mmap + write
read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,可以用 mmap() 替换 read() 系统调用函数。
mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
如下图:
- 进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区。
- 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据。
- 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。
总结:减少一次数据拷贝的过程。但是仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里且需要 4 次上下文切换,因为系统调用还是 2 次。
写优化 sendfile
在 Linux 内核版本 2.1 中开始,提供了一个专门发送文件的系统调用函数 sendfile()。
它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。
如下图:
当你的网卡支持 G-DMA(The Scatter-Gather Direct Memory Access)技术,可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。
如下图:
- 通过 DMA 将磁盘上的数据拷贝到内核缓冲区里。
- 缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝。
总结
上面两个优化其实就是 零拷贝(Zero-copy)技术,因为没有在内存层面去拷贝数据,全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
但是还是发生了数据的拷贝,并没用一次数据都没拷贝。对应这本文的题目!
感谢
本文信息参考百度百科和知乎用户(小林图解计算机基础)。