传统拷贝与零拷贝技术
操作系统处理IO,大致为以下两个步骤:
- 数据取至内核空间(kernel space)
- 内核空间传递数据到用户空间(user space)
IO大体又有两类,阻塞IO与非阻塞IO
传统拷贝流程
首先来看传统拷贝操作,有以下步骤:
| 步骤 | 操作描述 | 详细说明 |
|---|---|---|
| 1 | 发起read()调用 | 用户态→内核态(第1次切换) |
| 2 | 内核发起DMA IO请求 | 内核准备DMA传输 |
| 3 | 磁盘DMA拷贝到内核缓冲区 | 零CPU参与的数据传输 |
| 4 | 内核缓冲区拷贝到用户缓冲区 | 第1次CPU拷贝,调用返回后内核态→用户态(第2次切换) |
| 5 | 发起write()调用 | 用户态→内核态(第3次切换) |
| 6 | 用户缓冲区拷贝到内核缓冲区 | 第2次CPU拷贝 |
| 7 | 内核缓冲区DMA拷贝到网卡 | 零CPU参与的数据传输 |
| 8 | write()调用返回 | 内核态→用户态(第4次切换) |
开销总计:
- 4次上下文切换
- 2次CPU拷贝
- 2次DMA拷贝
为什么要消除CPU拷贝?/为什么CPU拷贝非常昂贵?
- CPU拷贝占用了大量的CPU时间片,且仅作数据搬运而不是复杂运算,我们认为这是”无意义劳动”
- 大量内存块复制污染了CPU缓存,导致缓存命中降低
- 涉及了内核态与用户态的切换,有明显的上下文成本
mmap
MMap(Memory Map)是 Linux 中提供的一种将文件映射到进程地址空间的一种机制,我们可以将用户缓冲区和内核缓冲区进行映射处理,省略数据在内核缓冲区和用户缓冲区之间的 CPU 拷贝。
缺点:
- 引入了新的CPU拷贝——内核缓冲区内部数据拷贝(PageCache到SocketBuffer)
- 仍需2次系统调用(4次上下文切换)[此处只讨论mmap流程,实际可能涉及其他write操作]
sendFile
sendFile()是一个用于高效地将文件数据从内核空间直接传输到网络套接字(Socket)上的系统调用,实现零拷贝。
其实现的核心思想在于,数据完全不进入用户空间,完全在内核空间进行传输,从而减少了内核缓冲区到用户缓冲区的CPU拷贝,同时压缩系统调用到1次,减少了2次用户态-内核态切换。但这也导致与mmap一样,引入了内核缓冲区内部的CPU拷贝。
在 Linux 2.4 后,sendfile引入了SG-DMA技术,对 DMA 拷贝加入了 scatter/gather 操作使之可以直接从内核空间缓冲区中读取数据到网卡,进而真正实现了零拷贝(该改进需要网卡硬件支持Scatter-Gather DMA)
splice
Linux 2.6.17 引入 splice/tee/vmsplice,核心是利用 pipe buffer(页引用 + 引用计数) 在内核态搬运数据,通常避免用户态往返拷贝。
补充: “对部分文章表述 ‘与sendfile 方法不同,splice 不需要硬件支持’ 的理解与解释”
以”file → TCP socket”的典型路径为例,若网卡设备支持S-G DMA,二者路径高度相似
但如果网卡设备不支持S-G DMA(或中间路径被迫线性化),二者都会退化(引入额外的内核内拷贝)
splice的优势应该是实现了更通用的,尽可能零拷贝的通用接口,而sendfile在不是”file→socket”的场景就不合适,更加特化
此处不需要硬件支持应该是指:pipe通过pipe buffer增加页引用计数而不拷贝页的核心思想并不需要硬件支持
splice将数据从磁盘读取到内核缓冲区,在内核缓冲区和 socket 缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。
由于实现要求,splice的一端必须是管道,因此在file->socket的典型路径下,需要2次splice系统调用(file-pipe,pipe->socket),即4次用户态-内核态切换,0次CPU拷贝。
几种方式对比:
| 拷贝机制 | 系统调用 | CPU拷贝次数 | DMA拷贝次数 | 上下文切换次数 | 特点 |
|---|---|---|---|---|---|
| 传统拷贝方式 | read/write | 2 | 2 | 4 | 消耗系统资源较多,效率较慢 |
| mmap | mmap/write | 1 | 2 | 4 | 省略了用户缓冲区与内核缓冲区间的数据拷贝 |
| sendfile | sendfile | 1 | 2 | 2 | 与 mmap 相比,减少了内存文件映射的步骤,但更特化 |
| sendfile With DMA scatter/gather | sendfile | 0 | 2 | 2 | 需要网卡硬件支持S-G DMA,真正的零拷贝 |
| splice(理想情况下) | splice | 0 | 2 | 4 | 真正的零拷贝,更泛用 |