底层原理 | 零拷贝

1 术语

1.1 零拷贝

"零拷贝"中的"拷贝"是操作系统在I/O操作中,将数据从一个内存区域复制到另外一个内存区域。而"零"并不是指0次复制,更多的是指在用户态和内核态之间的复制是0次。

1.2 CPU COPY

通过计算机的组成原理我们知道,内存的读写操作是需要CPU的协调数据总线、地址总线和控制总线来完成的。因此在"拷贝"发生的时候,往往需要CPU暂停现有的处理逻辑,来协助内存的读写,这种我们称为CPU COPY。

CPU COPY不但占用了CPU资源,还占用了总线的带宽。

1.3 DMA COPY

DMA(DIRECT MEMORY ACCESS)是现代计算机的重要功能。它的一个重要的特点就是,当需要与外设进行数据交换时,CPU只需要初始化这个动作便可以继续执行其他指令,剩下的数据传输的动作完全由DMA来完成。就是 DMA 替他完成一部分的拷贝工作,这样 CPU 就能去做其他事情了。

可以看到DMA COPY是可以避免大量的CPU中断的。

1.4 SOCKET BUFFER

TCP/IP协议栈维护着两个缓冲区:send buffer和recv buffer,它们合称为socket buffer。

1.5 上下文切换

本文上下文切换是指由用户态切换到内核态,以及由内核态切换到用户态。

1.5 内核空间和用户空间

现代操作系统用的都是虚拟存储器(即对上层应用来说,地址是连续的,但是在磁盘上不一定是连续的),一般32位的寻址空间为4G(2的32次方)。为了安全,操作系统将内存空间分为两部分,一部分为内核空间,一部分为用户空间。在linux中,将最高的1G字节供内核使用,成为内核空间,将较低的3G字节供各个进程使用,称为用户空间。

2 存在多次拷贝的原因

  • 操作系统为了保护系统不被应用程序有意或无意地破坏,为操作系统设置了用户态和内核态两种状态。用户态想要获取系统资源(例如访问硬盘),必须通过系统调用进入到内核态,由内核态获取到系统资源,再切换回用户态返回应用程序。
  • 出于"readahead cache"和异步写入等等性能优化的需要,操作系统在内核态中也增加了一个"内核缓冲区"(kernel buffer),读取数据时并不是直接把数据读取到应用程序的buffer,而先读取到kernel buffer,再由kernel buffer复制到应用程序的buffer。因此,数据在被应用程序使用之前,可能需要被多次拷贝。

3 传统的文件传输

在这里插入图片描述
读数据过程:

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

写数据过程:

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

综上所述:

  • 读过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝(CPU中断);
  • 写过程涉及2次空间切换、1次DMA拷贝、1次CPU拷贝(CPU中断);

很明显,第2次和第3次的copy只是把数据复制到user buffer又原封不动的复制回来,为此带来了两次的cpu copy和两次上下文切换,是完全没有必要的。

Linux的零拷贝技术就是为了优化掉这两次不必要的拷贝。

4 实现一:sendFile

Linux内核2.1开始引入一个叫sendFile系统调用,这个系统调用可以在内核态内把数据从内核缓冲区直接复制到socket buffer内,从而可以减少上下文的切换和不必要数据的复制。

这个系统调用其实就是一个高级I/O函数, 函数签名如下:

#include<sys/sendfile.h>
ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);
  • out_fd是写出的文件描述符,而且必须是一个socket;
  • in_fd是读取内容的文件描述符,必须是一个真实的文件,不能是管道或socket;
  • offset是开始读的位置;
  • count是将要读取的字节数;

sendfile方式只使用一个函数就可以完成之前的 read+write 和 mmap+write 的功能,这样就少了2次状态切换,由于数据不经过用户缓冲区,因此该数据无法被修改。
在这里插入图片描述
读写数据过程:

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

综上所述:

  • 过程涉及2次空间切换、2次DMA拷贝、1次CPU拷贝(CPU中断);

可以看到,利用sendFile系统调用后,可以将4次数据拷贝减少到3次,4次上下文切换减少到2次,2次CPU中断减少到1次。相对传统I/O, 这种零拷贝技术通过减少两次上下文切换,1次cpu copy,可以将I/O性能提高50%以上(网络数据, 未亲测)。

开始的术语中说到, 所谓的零拷贝的"零"是指用户态和内核态之间的拷贝次数为0,从这个定义上来说, 现在的这个零拷贝技术已经是真正的"零"了。然而,对性能追求极致的伟大的科学家和工程师们并不满足于此。精益求精的他们对中间第2次的cpu copy依旧耿耿于怀,想尽千方百计要去掉这一次没有必要的数据拷贝和CPU中断。

5 实现二:sendFile+scatter-gather

在内核2.4以后的版本中, Linux内核对socket缓冲区描述符做了优化。通过这次优化,sendFile系统调用可以在只复制kernel buffer少量元信息的基础上,把数据直接从kernel buffer 复制到网卡buffer中去,从而避免了从"kernel buffer"拷贝到"socket buffer"的这一次拷贝。这个优化后的sendFile,我们称之为支持scatter-gather特性的sendFile。
在这里插入图片描述
读写数据过程:

  • 应用程序要读取磁盘数据,调用sendfile()函数从而实现用户态切换内核态,这是第1次状态切换;
  • DMA控制器将数据从磁盘拷贝到内核缓冲区,这是第1次DMA拷贝;
  • 将内核缓冲区中数据的offset和length拷贝到socket缓冲区;
  • DMA控制器根据socekt缓冲区的offset和length从内核缓冲区中直接拷贝数据到网卡缓冲区,这是第2次DMA拷贝;
  • 完成拷贝之后,sendfile()函数返回实现内核态切换用户态,这是第2次状态切换;

综上所述,最后数据拷贝变成只有两次DMA COPY:

  • 硬盘拷贝到内核缓冲区(DMA COPY);
  • 内核缓冲区拷贝到网卡的buf(DMA COPY);

6 实现三:mmap+write

mmap(内存映射文件),是指将文件映射到进程的地址空间去, 实现硬盘上的物理地址跟进程空间的虚拟地址的一一对应关系。

mmap是另外一个用于实现零拷贝的系统调用。跟sendFile不一样的地方是,它是利用共享内存空间的方式,避免user buffer和kernel buf之间的数据拷贝(两个buffer共享同一段内存)。

执行过程:

buf = mmap(diskfd, len);
write(sockfd, buf, len);

在这里插入图片描述

读数据过程:

  • 应用程序要读取磁盘数据,调用 mmap() 函数从而实现用户态切换内核态,这是第1次状态切换;
  • DMA控制器将数据从磁盘拷贝到内核缓冲区,这是第1次DMA拷贝;
  • 接着,应用进程跟操作系统内核「共享」这个缓冲区;
  • 一切都完成后,mmap()函数返回实现内核态切换用户态,这是第2次状态切换;

写数据过程:

  • 应用程序要向网卡写数据,调用write()函数实现用户态切换内核态,这是第1次切换;
  • 操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU来搬运数据,这是第1次CPU拷贝;
  • DMA控制器将数据从socket缓冲区复制到网卡,这是第1次DMA拷贝;
  • 完成拷贝之后,write()函数返回实现内核态切换用户态,这是第2次切换;

综上所述:

  • 通过使用mmap() 来代替 read(), 可以减少一次数据拷贝的过程;
  • 但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。

7 实现四:splice

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

splice 系统调用可以在内核缓冲区和socket缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。
在这里插入图片描述
splice也有一些局限,它的两个文件描述符参数中有一个必须是管道设备。


参考文章:
  1. 关于零拷贝的一些理解
  2. 你真的理解零拷贝吗?
  3. 终于有人把零拷贝Zero-Copy讲懂了
  4. 零拷贝技术详细解读(Zero Copy)
  5. 浅析Linux中的零拷贝技术
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值