# 【NO.601】【linux】彻底搞懂零拷贝（Zero-Copy）技术

## 1.前言

Linux系统中一切皆文件，仔细想一下Linux系统的很多活动无外乎**读操作**和**写操作**，零拷贝就是为了提高读写性能而出现的。

## **2. 数据拷贝基础过程**

在Linux系统内部缓存和内存容量都是有限的，更多的数据都是存储在磁盘中。对于Web服务器来说，经常需要从磁盘中读取数据到内存，然后再通过网卡传输给用户：

![img](https://pic1.zhimg.com/80/v2-18654f1649f5c08d6367705eff5d8d74_720w.webp)

上述数据流转只是大框，接下来看看几种模式。

### **2.1 仅CPU方式**

- 当应用程序需要读取磁盘数据时，调用read()从用户态陷入内核态，read()这个系统调用最终由CPU来完成；
- CPU向磁盘发起I/O请求，磁盘收到之后开始准备数据；
- 磁盘将数据放到磁盘缓冲区之后，向CPU发起I/O中断，报告CPU数据已经Ready了；
- CPU收到磁盘控制器的I/O中断之后，开始拷贝数据，完成之后read()返回，再从内核态切换到用户态；

![img](https://pic2.zhimg.com/80/v2-8e97bf4d009f6f77869851f4709d38b1_720w.webp)

### **2.2 CPU&DMA方式**

CPU的时间宝贵，让它做杂活就是浪费资源。

直接内存访问（Direct Memory Access），是一种硬件设备绕开CPU独立直接访问内存的机制。所以DMA在一定程度上解放了CPU，把之前CPU的杂活让硬件直接自己做了，提高了CPU效率。

目前支持DMA的硬件包括：网卡、声卡、显卡、磁盘控制器等。

![img](https://pic3.zhimg.com/80/v2-f13f3f2bd3f05587c8b3852425550dd6_720w.webp)

有了DMA的参与之后的流程发生了一些变化：

![img](https://pic3.zhimg.com/80/v2-f8eaddb47acfc712111368a32dcc3f62_720w.webp)

最主要的变化是，CPU不再和磁盘直接交互，而是DMA和磁盘交互并且将数据从磁盘缓冲区拷贝到内核缓冲区，之后的过程类似。

> “【**敲黑板**】无论从仅CPU方式和DMA&CPU方式，都存在多次冗余数据拷贝和内核态&用户态的切换。 ”

我们继续思考Web服务器读取本地磁盘文件数据再通过网络传输给用户的详细过程。

## **3.普通模式数据交互**

一次完成的数据交互包括几个部分：系统调用syscall、CPU、DMA、网卡、磁盘等。

![img](https://pic1.zhimg.com/80/v2-abffda68664639789fb8b13d35a382d4_720w.webp)

系统调用syscall是应用程序和内核交互的桥梁，每次进行调用/返回就会产生两次切换：

- 调用syscall 从用户态切换到内核态
- syscall返回 从内核态切换到用户态

![img](https://pic3.zhimg.com/80/v2-fbcdf266265a92248b7a1062d1713eba_720w.webp)

来看下完整的数据拷贝过程简图：

![img](https://pic3.zhimg.com/80/v2-4ef1b254fb56c5586a4fda03f66611c6_720w.webp)

读数据过程：

- 应用程序要读取磁盘数据，调用read()函数从而实现用户态切换内核态，这是第1次状态切换；
- DMA控制器将数据从磁盘拷贝到内核缓冲区，这是第1次DMA拷贝；
- CPU将数据从内核缓冲区复制到用户缓冲区，这是第1次CPU拷贝；
- CPU完成拷贝之后，read()函数返回实现用户态切换用户态，这是第2次状态切换；

写数据过程：

- 应用程序要向网卡写数据，调用write()函数实现用户态切换内核态，这是第1次切换；
- CPU将用户缓冲区数据拷贝到内核缓冲区，这是第1次CPU拷贝；
- DMA控制器将数据从内核缓冲区复制到socket缓冲区，这是第1次DMA拷贝；
- 完成拷贝之后，write()函数返回实现内核态切换用户态，这是第2次切换；

综上所述：

- 读过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝；
- 写过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝；

可见传统模式下，涉及多次空间切换和数据冗余拷贝，效率并不高，接下来就该零拷贝技术出场了。

## **4. 零拷贝技术**

### **4.1 出现原因**

我们可以看到，如果应用程序不对数据做修改，从内核缓冲区到用户缓冲区，再从用户缓冲区到内核缓冲区。两次数据拷贝都需要CPU的参与，并且涉及用户态与内核态的多次切换，加重了CPU负担。

我们需要降低冗余数据拷贝、解放CPU，这也就是零拷贝Zero-Copy技术。

### **4.2 解决思路**

目前来看，零拷贝技术的几个实现手段包括：mmap+write、sendfile、sendfile+DMA收集、splice等。

![img](https://pic1.zhimg.com/80/v2-60367b931941389dd8199315f9b1a580_720w.webp)

#### **4.2.1 mmap方式**

mmap是Linux提供的一种内存映射文件的机制，它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射，从而实现内核缓冲区与用户缓冲区的共享。

这样就减少了一次用户态和内核态的CPU拷贝，但是在内核空间内仍然有一次CPU拷贝。

![img](https://pic3.zhimg.com/80/v2-2bb29cb76bc6027b7f5b54de60cc723a_720w.webp)

mmap对大文件传输有一定优势，但是小文件可能出现碎片，并且在多个进程同时操作文件时可能产生引发coredump的signal。

#### **4.2.2 sendfile方式**

mmap+write方式有一定改进，但是由系统调用引起的状态切换并没有减少。

sendfile系统调用是在 Linux 内核2.1版本中被引入，它建立了两个文件之间的传输通道。

sendfile方式只使用一个函数就可以完成之前的read+write 和 mmap+write的功能，这样就少了2次状态切换，由于数据不经过用户缓冲区，因此该数据无法被修改。

![img](https://pic1.zhimg.com/80/v2-5bd306e1838167320a6a954742623830_720w.webp)

![img](https://pic1.zhimg.com/80/v2-289f0e6921b56ab66b3e4f2d829282e4_720w.webp)

从图中可以看到，应用程序只需要调用sendfile函数即可完成，只有2次状态切换、1次CPU拷贝、2次DMA拷贝。

但是sendfile在内核缓冲区和socket缓冲区仍然存在一次CPU拷贝，或许这个还可以优化。

#### **4.2.3 sendfile+DMA收集**

Linux 2.4 内核对 sendfile 系统调用进行优化，但是需要硬件DMA控制器的配合。

升级后的sendfile将内核空间缓冲区中对应的数据描述信息（文件描述符、地址偏移量等信息）记录到socket缓冲区中。

DMA控制器根据socket缓冲区中的地址和偏移量将数据从内核缓冲区拷贝到网卡中，从而省去了内核空间中仅剩1次CPU拷贝。

![img](https://pic2.zhimg.com/80/v2-21b528e0695d382f8bb10c926c2d1895_720w.webp)

这种方式有2次状态切换、0次CPU拷贝、2次DMA拷贝，但是仍然无法对数据进行修改，并且需要硬件层面DMA的支持，并且sendfile只能将文件数据拷贝到socket描述符上，有一定的局限性。

#### **4.2.4 splice方式**

splice系统调用是Linux 在 2.6 版本引入的，其不需要硬件支持，并且不再限定于socket上，实现两个普通文件之间的数据零拷贝。

![img](https://pic1.zhimg.com/80/v2-b910394097e2301a8fb092ab082cfccc_720w.webp)

splice 系统调用可以在内核缓冲区和socket缓冲区之间建立管道来传输数据，避免了两者之间的 CPU 拷贝操作。

![img](https://pic4.zhimg.com/80/v2-558c6409bac4096d4412375898eaa1e3_720w.webp)

splice也有一些局限，它的两个文件描述符参数中有一个必须是管道设备。

## **5.本文小结**

本文通过介绍数据交互的基本过程、传统模式的缺点，进而介绍了零拷贝的一些实现方法。

零拷贝技术是非常底层且重要的读写优化，对于服务并发能力的提升有很大帮助。

原文地址：https://zhuanlan.zhihu.com/p/500800127

作者：CPP后端技术