零复制(Zero-copy)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。
常规的文件拷贝,需要四个步骤:
- 从磁盘拷贝带内核空间
- 从内核空间拷贝到用户空间
- 从用户空间拷贝打到socket缓存区
- 将socket缓存区的内容发到网卡上。
其中,linux使用DMA技术,快速的将内容拷贝到内核空间。
DMA(Direct Memory Access) 技术很容易理解,本质上,DMA 技术就是我们在主板上放一块独立的芯片。在进行内存和 I/O 设备的数据传输的时候,我们不再通过 CPU 来控制数据传输,而直接通过 DMA 控制器(DMA Controller,简称 DMAC)。这块芯片,我们可以认为它其实就是一个协处理器(Co-Processor)
然后CPU负责2、3的拷贝。这里面有两处大的花销:
- 四次文件内容的复制。
- 四次上下文的切换。内核和用户空间两次,用户空间和socket缓冲区两次。
为了减少这个过程的花销,出现几种零拷贝技术。
1. mmap (Memory Map)
核心要点是:将该部分的内核空间和应用程序共享。那么文件拷贝到内核后,不再被拷贝到用户空间,而是CPU直接将内容copy到socket缓存区。
精简了一部分开销:
- 减少了内核空间到用户空间的拷贝1次
- 减少了用户空间和socket缓冲区的上下文切换1次。
优点:
- mmap基于懒加载模式,通过 mmap 申请 1000G 内存可能仅仅占用了 100MB 的虚拟内存空间,甚至没有分配实际的物理内存空间。当你访问相关内存地址时,才会进行真正的 write、read 等系统调用。CPU 会通过陷入缺页异常的方式来将磁盘上的数据加载到物理内存中,此时才会发生真正的物理内存分配。
- 数据一致性有OS保证。当发生数据修改时,内存出现脏页,与磁盘文件出现不一致。mmap 机制下由操作系统自动完成内存数据落盘(脏页回刷),用户进程通常并不需要手动管理数据落盘。
- 节约内存。由于内核空间和用户空间是共享方式,所以节约了一倍的内存。尤其是大文件时,效果明显。
缺点:
- 由于mmap在使用时必须指定内存映射的大小,所以不适合变长文件;
- 在大量写操作的场景下,由于系统需要处理脏页回写,所以不一定比带缓存的一般写有优势;
- 读写小文件(16k已下),存在更高的延迟和开销。
- 由于mmap需要预先指定内存映射大小,所以处理大文件时,需要根据系统内存大小限制,对文件进行分割,然后处理。这种方式下,需要拷贝的文件数量增加了,反而加大了文件拷贝的开销。
2. sendfile
sendfile是对mmap的优化,其核心在于:直接将文件描述符对接到socket上。
优点:
- 一次sendfile调用,复制过程在内核空间完成,减少了内核和用户空间的上下文切换开销。
缺点:
- 只能把数据从文件拷贝到socket,不能反过来。
- 目前为止,还存在一次CPU的拷贝:内核缓存到socket缓存。该部分可以通过硬件和驱动的加持来优化。
优化核心是:复制到socket缓存的只是内核空间的文件文件和长度信息,文件内容通过DMA引擎从内核缓存区直接拷贝到协议引擎中去。
3. splice
splice是在两个文件描述符之间移动数据,而不需要和内核空间交互。原理是利用Linux的管道缓冲区技术,所以其存在一个限制:两者之间,必须有一方是管道设备。
4. Direct I/O
用户空间读取的文件直接与磁盘交互,没有中间 page cache 层,所以文件数据不进入内核空间的page cache。
优点:
- 不需要内核空间参与,所以无上下文的切换
- 使用DMA技术,所以数据拷贝无需CPU,CPU只需负责管理即可。
不考虑磁盘自带缓存的情况下,可以视为direct IO下,文件读写是不走缓存的。写入成功则直接落盘;读取成功,则无系统缓存的影响。
注意的是,虽然文件数据不经过系统缓存,但是文件元数据还是被系统缓存所控制的,所以部分系统下,文件写入是数据落盘,但是文件元数据不一定。此类情况下,需要执行fsync将文件元数据刷入到磁盘中。
缺点:
- 由于数据拷贝通过DMA完成,所以用户空间的数据缓冲区需要预先锁定(page pinning),防止拷贝过程中,页地址的丢失。页锁定开销不必CPU拷贝低,所以应用程序必须分配和注册一个持久的内存池,用于数据缓冲。
- 如果数据不在应用程序的缓存中是,每次都需要从磁盘读取,开销大。
- 应用层需要自己管理Direct IO,增加了复杂度。
谁会用Direct I/O
答案:自缓存应用程序( self-caching applications)可以选择使用 Direct I/O。
比如数据库,其倾向于使用数据的逻辑表达,而非物理表达(就是自己有缓存命中机制,而不是直接使用磁盘中的数据做缓存)。
Kafka
Kafka分为Provider和Consumer。
- Provider向 Kakfa 发送消息,Kakfa 负责将消息以日志的方式持久化落盘
- Consumer向 Kakfa 进行拉取消息,Kafka 负责从磁盘中读取一批日志消息,然后再通过网卡发送;
写入使用mmap,能基于顺序磁盘I/O提供高效的写入性能。
读取使用sendfile,在减少数据拷贝次数和上下文切换的情况下,使用了内核空间的Page Cache的缓存技术,一次磁盘读取,为多个Consumer服务。
无法利用 sendfile 来持久化数据,利用 mmap 来实现 CPU 全程不参与数据搬运的数据拷贝。
Mysql