【前言】本文为个人学习零拷贝的记录,大多为拼凑而成(已经尽可能标注出处了)
什么是零拷贝
零拷贝(Zero-copy)技术,狭义上是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域;但实际中很难真正做到应用场景下的完全零复制,广义上讲可以说是减少冗余拷贝的I/O优化技术。Wikipedia 对于该概念的解释如下:
“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.
下面简单介绍一下为什么I/O操作时需要数据的拷贝?首先,操作系统的核心是被称为内核的一些程序的集合,其涵盖了操作系统的核心功能,包括但不限于硬件的抽象、文件系统控制、进程调度、作业调度等等,这一部分当然是不允许用户进程去直接操作的(安全问题嘛,这时候就会有人抬杠了,我自己用的系统想干哈就干哈,怎么限制这么多?要知道这里的用户是指用户程序,系统上的程序不是使用者能完全控制的吧(乛?乛))。为此,系统将虚拟内存(这个是啥?需要的建议自己再去搜一下)划分为内核空间和用户空间两部分,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态。
上图为传统IOread() 请求的简单示意图,主要流程简述如下:
-
用户进程向系统发起read() 请求,此时进程会发生从用户态到内核态的切换,也就是代码会从应用程序所在的用户线程切换到内核中的内核线程去执行(实际就是可运行的CPU指令集权限的切换,具体想了解的可以参照用户态和内核态) -
CPU接收到请求指令后便对DMA控制器发起调度指令 (什么是DMA?这里就简单说一下,DMA全称叫直接内存存取,即Direct Memory Access 的缩写,是一种允许外围设备直接访问系统主内存的机制,在没有DMA参与的情况下,需要由CPU全程参与I/O的调度;而有了DMA控制器,CPU就只需要在数据传输的开始和结束做必要的中断处理,其他时间爱干啥干啥去,可以类似一个数据管家的身份吧,想深入的话请另外去下点功夫,这里不做赘述) -
DMA控制器向设备发起I/O请求,由设备将数据放入其缓冲区(如果有的话 -
数据读取完毕之后DMA控制器会接受到通知,之后由DMA控制器将数据拷贝到内核缓冲区 -
DMA控制器通知CPU(其实也就是中断啦)数据已经读完,由CPU接手将数据从内核缓冲区拷贝到用户缓冲区 -
进程从内核态切换回内核态,同时解除阻塞状态
从上述的描述中可以知道,一次read() 请求操作下来会发生两次上下文的切换和两次数据拷贝(一次DMA copy和一次CPU copy),write() 请求操作其实也类似。零拷贝技术就是对上述的数据传输过程的优化
为什么需要零拷贝
这里引用某知乎回答(其实根据上面的分析也基本可以知道主要是提高数据传输的性能,好像是废话→_→)
传统的 Linux 系统的标准 I/O 接口(read、write)是基于数据拷贝的,也就是数据都是 copy_to_user 或者 copy_from_user,这样做的好处是,通过中间缓存的机制,减少磁盘 I/O 的操作,但是坏处也很明显,大量数据的拷贝,用户态和内核态的频繁切换,会消耗大量的 CPU 资源,严重影响数据传输的性能,有数据表明,在Linux内核协议栈中,这个拷贝的耗时甚至占到了数据包整个处理流程的57.1%。
如何实现零拷贝
在 Linux 中零拷贝技术主要有 3 个实现思路:用户态直接 I/O、减少数据拷贝次数以及写时复制技术
- 用户态直接I/O:顾名思义是使用户态下的库函数能够直接访问硬件设备,绕过内核的操作,该种实现的方案只能适用于不需要内核缓冲区处理的应用程序,通常这些应用程序会在进程地址空间有属于自己的数据缓存机制,称为自缓存应用程序,比如数据库,这样可以提供更好的缓冲机制提高读写性能。这种方式带来的提升能够很明显,但同时开销也会很大,如果没有控制好读写将会导致磁盘的读写效率低下
- 减少数据拷贝次数:还是顾名思义,可以减少数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,常见的实现有Linux环境下的mmap(),sendfile() 以及 splice() 等
- 写时复制技术:指当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。
具体的实现说明
-
mmap + write 下面以数据上传作为简单的案例说明,从磁盘读取数据文件然后通过网卡发送,示意图如下: 用mmap()系统调用代替原先的read(),简要流程描述如下:
- 用户进程调用
mmap() ,进程发生从用户态到内核态的切换 - 内核缓冲区部分映射到用户缓冲区(这一步做了什么后面会说到)
- DMA控制器将数据从硬盘拷贝到内核缓冲区
mmap() 执行结束,进程发生从内核态到用户态的切换- 用户进程调用
write() ,尝试将数据写到内核里的套接字缓冲区(这里还是对自身的用户缓存进行操作,只不过由于映射的关系可以不需要copy),进程发生从用户态到内核态的切换 - CPU 将内核缓冲区中的数据拷贝到的套接字缓冲区
- DMA 控制器将数据从套接字缓冲区拷贝到网卡完成数据传输
write() 执行结束,进程发生从内核态到用户态的切换 先看下mmap() 究竟做了什么(下面只做简单说明,需要详细了解原理可参考mmap详解),这个在 **<sys/mman.h>**中被定义的函数,创建一个新的虚拟内存区域并将指定的对象映射到此区域,可以类比为编程应用上的引用传递,主要分为三个阶段:
- 用户进程调用库函数
mmap ,此时系统会在当前进程的虚拟地址空间中寻找满足要求的连续的虚拟地址,并进行存储结构的初始化 - 内核空间的系统调用函数
mmap ,定位文件磁盘物理地址,实现文件地址和虚拟地址区域的映射关系,注意此时并没有数据拷贝(也就是数据还不存在于内存中 - 发起访问后引发缺页中断(这是另一个知识点),通俗点讲就是找不到数据然后执行将数据拷贝到内存中(DMA拷贝)的过程
经过mmap的操作,用户进程可以像读取自身的缓存一样读取内核中被映射到缓存区,从而避免了一次从内核缓存到用户缓存的CPU-copy,总体上的拷贝次数是2次DMA拷贝和一次CPU拷贝,用户态和内核态的切换还是 4 次 优点总结:
- 节省内存空间。因为用户进程上的这一段内存是虚拟的,并不真正占据物理内存,只是映射到文件所在的内核缓冲区上,因此可以节省一半的内存占用
- 减少一次CPU拷贝
缺点/不足总结:
- 小文件的优化占比不高。内存映射技术是一个开销很大的虚拟存储操作,同时需要对齐页边界来操作,对于大文件来说边界对齐损耗占比可以忽略,但是对于小文件来说会显著降低内存利用率
- 存在被终止风险,也是无法映射变长文件的表现。当使用 mmap 映射一个文件时,如果这个文件被另一个进程所截断(大概就是把文件截短,一刀切的感觉),那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,可以使用文件的租借锁解决
-
sendfile sendFile 是Linux提供的函数,主要功能是实现了在内核空间中将数据从内核缓冲区直接拷贝到Socket缓冲区的过程,简单示意图如下: 简要流程描述如下:
- 用户进程发起
sendfile() 调用,上下文从用户态切换到内核态 - 之后就是DMA拷贝——>CPU拷贝——>DAM拷贝(看图就行了,懒得再说(u_u)
- 完成数据传输后上下文从内核态再切换回用户态,
sendfile() 系统调用执行返回 这个看起来比上面的mmap() 好多了,少了很多用户态和内核态的切换过程,但是缺点也很明显,用户态没法参与到整个的数据传输过程中,也就代表着用户进程无法对数据进行修改(一个单纯的传输机器 -
sendfile + DMA gather copy ? 看完上面的sendfile() 内容,是不是觉得还有地方可以改进的,既然都是内核空间操作的数据,数据还没有更改的需要的话,为啥还要经过一次copy到Socket Buffer ,然后再到网卡勒,直接到内核缓存拿它不香?为此,Linux 2.4 版本的内核对 sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作,将内核空间的读缓冲区中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区中(可以类似于映射的思路),由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区拷贝到网卡设备中,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作,简单示意图如下:
简单流程描述(由于前半部分有重复,以省略代替):
- …
- CPU 把读缓冲区的文件描述符和数据长度拷贝到Socket Buffer(抬杠!这不是拷贝吗?这个跟数据的拷贝没法比,别刚)
- 基于已拷贝的文件描述符和数据长度,CPU 利用 DMA 控制器的 gather/scatter 操作(这是神马操作,下面说)直接批量地将数据从内核的读缓冲区拷贝到网卡进行数据传输
- …
可以看到跟单纯的sendfile() 相比,少了一次CPU拷贝,其他过程基本不变
下面就来看下这个gather/scatter 操作 究竟干了啥。首先,需要传输的数据位于内核缓冲区,但不是所有位于内核缓冲区的数据都是需要传输的,这是一点;其次位于内核缓冲区内的待传输数据也不一定(大概率)是连续存储的,而发送到网卡的数据,也就是数据包需要是连续的,这就要求DMA在copy数据的阶段需要把分散存储的数据进行聚合(这是需要硬件支持的点,传统的DMA一次只能传输物理上连续的一个数据块)。这里再引用下其他大神写的博客:
为了避免操作系统内核造成的数据副本,需要用到一个支持收集操作的网络接口。主要的方式是待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。这样一来,从文件中读出的数据就根本不需要被拷贝到 socket 缓冲区中去,而只是需要将缓冲区描述符传到网络协议栈中去,之后其在缓冲区中建立起数据包的相关结构,然后通过 DMA 收集拷贝功能将所有的数据结合成一个网络数据包。网卡的 DMA 引擎会在一次操作中从多个位置读取包头和数据
作者:叫我不矜持 链接:https://www.jianshu.com/p/028cf0008ca5 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
-
splice splice() 是Linux 2.6.17新加入的系统调用,官方文档的描述是
? splice() is a syscall on Linux. It transfers data from a pipe to another within kernel space. Optionally, either the source or the destination can be a descriptor or a socket, but at least one pipe is needed. As splice() avoids copying data from and to the userspace, it is more efficient than a read()/write() combo.
大概的翻译就是它可以通过管道的方式进行文件间的数据传输而不需要额外的拷贝(这里指CPU拷贝),可以看到,该方案的核心是管道技术(有兴趣的可以学习一下,这里推荐一个参考资料),根本上还是指针的思想,废话不多说,示意图走起: 简要流程描述:
splice() 调用,用户态——>内核态- DMA copy将数据拷贝到内核缓冲
- CPU 在内核空间的读缓冲区和网络缓冲区(socket buffer)之间建立管道
- DMA copy将数据从Socket Buffer拷贝到网卡进行数据传输
splice() 返回,内核态——>用户态 可见,整个流程中发生了两次上下文的状态切换、两次DMA拷贝和0次CPU拷贝。但是看示意图也可以看出来,用户进程还是没有参与到数据的运输过程中,也就意味着数据无法修改的问题依旧存在,同时使用了Linux的管道缓冲机制,需要传输的两个文件描述符参数中有一个必须是管道设备。 -
缓冲区共享 关于零拷贝还有一种基于预先映射机制实现的共享缓存池技术,但是这中方式重写了传统基于拷贝到IO接口操作方式,目前来说还相对不是很成熟,有兴趣的可以自行了解,后面的参考链接中也有简单提到。
Windows上的零拷贝
可以看到上面所说的都是基于Linux系统上接口实现的零拷贝,那么Win系统有没有实现的方案呢?(当然,现下的网络应用部署方案绝大多数都是部署于Linux系统之上,Windows下实现零拷贝有意义但是相比之下好像没那么重要,放在这里讨论纯属个人好奇)经过相关查询,发现Windows平台下可以通过拓展的TransmitFile API支持来实现类似sendFile 的零拷贝,官方文档介绍如下
The TransmitFile function transmits file data over a connected socket handle. This function uses the operating system’s cache manager to retrieve the file data, and provides high-performance file data transfer over sockets.
TransmitFile 函数通过连接的套接字句柄传输文件数据。函数使用操作系统的缓存管理器来检索文件数据,并通过套接字提供高性能的文件数据传输。
通过该拓展Api,程序可以直接在一个已经连接的套接字上发送一个打开的文件,同时将文件句柄和套接字连接一并作为函数参数给出(具体请移步官方文档),文件数据的读取和发送都在核心/内核态下进行,避免了程序直接介入数据传输过程导致上下文的冗余切换
目前了解到的就差不多这样,具体还有没有其他的方式咱也不知道,毕竟windows不像Linux是开源的
主要参考:
PS:编辑器太难用拉!!!
|