Linux-零拷贝技术

“零拷贝”,这名字一听就很厉害。

在看宋宝华老师的一篇吃瓜文章种,看到了这个逻辑,于是去搜索记录整理,其实东西都是东一搜西一搜搞到的,其中比较好的两篇文章:

一文彻底揭秘linux操作系统之「零拷贝」! - 知乎

linux内核零拷贝技术_HeroKern的博客-CSDN博客_linux 零拷贝

这篇文章里面引用了很多两位的内容,大家可以去看下原著,下面内容是自己的笔记记录。

简介:

零拷贝:

[1] DMA传输可以不经过CPU copy从一个硬件传递到另一个硬件中

      为啥不直接在两个硬件中传递呢?

      答:从后面的例子来看,这的传递并不能满足涉及,有的时候需要对数据的操作,比如socket传输中在kernel buffer中会增加一个buffer的size和位置信息,再copy到socket上。

由此引出问题:这个中间的操作是必须的吗?假如经过kernel不可以吗?

      答:自己思考,像是硬盘和socket,并不是挂在一个总线上,两个设备之间的信息传序,必须得经过kernel作为中间人来转换传输。后面再思考下吧。

[2] 零拷贝是:当用户空间发起调用,将一个数据从硬件1(硬盘)中读取然后再发送到硬件2(网口缓存)中,这个过程可以简化为:硬件1直接通过DMA传递到内核空间中,再经过DMA传递到硬件2上,这样就不用发生CPU copy了。

1、零拷贝是什么?

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

零拷贝是指的在用户与内核空间之间的复制的次数是0,那么如果不用零拷贝的话,复制的次数是几呢?如下面的例子:

例子:

从上图可以看出软件流程一共复制了4次数据,内核态到用户态切换4次。
读操作(复制两次,上下文切换两次):
        1.用户进程通过 read() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)
        2.CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)
        3。CPU将读缓冲区(read buffer)中的数据拷贝到用户空间(user space)的用户缓冲区(user buffer)
        4.上下文从内核态(kernel space)切换回用户态(user space),read 调用执行返回。
写操作(复制两次,上下文切换两次):
        1.用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)
        2.CPU 将用户缓冲区(user buffer)中的数据拷贝到内核空间(kernel space)的网络缓冲区(socket buffer)
        3.CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。(虽然网络驱动会分层解析网络数据帧,但网络数据是通过sk_buff指针缓存在各个层级进行数据,所以这里网络协议层不存数据拷贝)
        4.上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返
从上面可以看到用户态和内核态之间数据拷贝流程走了两次,熟悉linux内核调度结构的知道内核态和应用态数据拷贝是比较费时,同时拷贝数据是个冗余过程,仅仅从应用态过了一圈。所以引入了零拷贝技术。

          这个例子和下面的例子是重复的,但是放在这里可以更好的描述一下多次拷贝的情况。同时里面的描述也可以做为一个基础参考,可以先看仔细看看这个例子。

2、零拷贝给我们带来的好处

• 减少甚至完全避免不必要的 CPU 拷贝,从而让 CPU 解脱出来去执行其他的任务;

• 减少内存带宽的占用;

• 通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换。

3、操作系统中谁负责IO拷贝?

DMA 负责内核间的 IO 传输,CPU 负责内核和应用间的 IO 传输。

两种拷贝类型:

(1)CPU COPY

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

(2)DMA COPY

DMA(DIRECT MEMORY ACCESS) 是现代计算机的重要功能,它有一个重要特点:当需要与外设进行数据交换时, CPU 只需要初始化这个动作便可以继续执行其他指令,剩下的数据传输的动作完全由DMA来完成可以看到 DMA COPY 是可以避免大量的 CPU 中断的

问题:DMA的传输可以节约CPU资源,实现的原理是避免了大量的CPU中断?

回答:1 - DMA传输时是不经过cpu的,即这个时候不会占用cpu资源,因此使用DMA可以达到两个效果:

[1] 从cpu角度来说,可以大大减少CPU占用率,可以使得CPU的占用更多的应用与其他操作。

[2] 从整体角度(CPU+操作系统)来说,减少了对CPU的使用,提高了效率

但是这个时候的CPU和DMA是同时存在的,在一条总线上,这两部分如何运行呢?

这个时候存在3个方案:

(1)停止CPU访内存;

CPU暂停访存是指在启动DMA后,CPU停止对主存的访问直到接收到DMA中断,CPU将总线控制权交给I/O操作。

(2)周期挪用;

周期挪用/周期窃取是指在CPU访存过程中,接收到DMA请求后挪用出一两个周期用于I/O,I/O完成后CPU继续访存。

(3)DMA与CPU交替访问内存(DMA与CPU分时复用数据总线).

cpu交出总线后,会停止数据总线上的工作,不涉及总线的操作应该时可以进行的?(有待考究)

4、拷贝过程中会发生什么?

上下文切换:

从内核态到用户态时会发生上下文切换,上下文切换时指由用户态切换到内核态, 以及由内核态切换到用户态。

现在我们对零拷贝有一定的概念基础了,接下来,让我们深入去了解一下 Linux 操作系统与 IO 复制之间的来龙去脉。

- 原理篇 -

1、内存管理

Linux 内存管理结构的历史:在 Linux 内核2.4版本之前,内存管理结构中 page cache 和 buffer cache 是分开的,分别是两个独立的。

从 Linux 内核2.4 版本开始操作系统在内存管理机制进行了优化,才支持了零拷贝机制。

2、Linux内存管理结构

Linux 内核的文件 Cache 管理机制来进行实现的,在 Linux 的实现中,文件 Cache 分为两个层面,一是 Page Cache,另一个 Buffer Cache,每一个 Page Cache 包含若干 Buffer Cache。

内存管理系统和 VFS 只与 Page Cache 交互(原来如此),内存管理系统负责维护每项 Page Cache 的分配和回收,同时在使用 memory map 方式访问时负责建立映射。

VFS 负责 Page Cache 与用户空间的数据交换。而具体文件系统则一般只与 Buffer Cache 交互,它们负责在外围存储设备和 Buffer Cache 之间交换数据。

标注:VFS(virtual File System) 的作用就是采用标准的 Unix 系统调用读写位于不同物理介质上的不同文件系统,即为各类文件系统提供了一个统一的操作界面和应用编程接口。VFS 是一个可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作的粘合层。

3、应用上下文与内核上下文共享内存交互过程

将 Cache 项映射到用户空间,使得应用程序可以像使用内存指针一样访问文件,Memory map 访问 Cache 的方式在内核中是采用请求页面机制实现的:

• 当我们应用程序调用 mmap(下图中1),陷入到内核中后调用 dommappgoff (图中2)该函数从应用程序的地址空间中分配一段区域作为映射的内存地址,并使用一个 VMA(vmareastruct)结构代表该区域,之后就返回到应用程序(图中3);

• 然后当应用程序访问 mmap 所返回的地址指针时(图中4),由于虚实映射尚未建立,会触发缺页中断(图中5);

• 之后系统会调用缺页中断处理函数(图中6),在缺页中断处理函数中,内核通过相应区域的 VMA 结构判断出该区域属于文件映射,于是调用具体文件系统的接口读入相应的 Page Cache 项(图中7、8、9),并填写相应的虚实映射表;

• 经过这些步骤之后,应用程序就可以正常访问相应的内存区域了。

- IO 零拷贝 -

1、存在多次拷贝的原因

操作系统为了保护系统不被应用程序有意或无意地破坏,为操作系统设置了用户态和内核态两种状态,用户态想要获取系统资源(例如访问硬盘),必须通过系统调用进入到内核态, 由内核态获取到系统资源,再切换回用户态返回应用程序。

出于 "readahead cache" 和异步写入等等性能优化的需要, 操作系统在内核态中也增加了一个"内核缓冲区"(kernel buffer)。读取数据时并不是直接把数据读取到应用程序的 buffer, 而先读取到 kernel buffer, 再由 kernel buffer 复制到应用程序的 buffer。因此,数据在被应用程序使用之前,可能需要被多次拷贝。

所以kernel buffer时多次 复制的关键,多了一个中间件。

2、非零拷贝IO流程

总结所有系统中, 不管是 WEB 应用服务器, FTP 服务器,数据库服务器, 静态文件服务器等等, 所有涉及到数据传输的场景,无非就一种:

——从硬盘上读取文件数据, 发送到网络上去。

这个场景我们简化为一个模型:

File.read(fileDesc, buf, len);

Socket.send(socket, buf, len);

为了方便描述,上面这两行代码, 我们给它起个名字: read-send模型。

操作系统在实现这个 read-send 模型时,需要有以下步骤:

  1. 应用程序开始读文件的操作;
  2. 应用程序发起系统调用, 从用户态切换到内核态(第一次上下文切换);
  3. 内核态中把数据从硬盘文件读取到内核中间缓冲区(kernel buf);
  4. 数据从内核中间缓冲区(kernel buf)复制到(用户态)应用程序缓冲区(app buf),从内核态切换回到用户态(第二次上下文切换);
  5. 应用程序开始发送数据到网络上;
  6. 应用程序发起系统调用,从用户态切换到内核态(第三次上下文切换);
  7. 内核中把数据从应用程序(app buf)的缓冲区复制到socket的缓冲区(socket);
  8. 内核中再把数据从socket的缓冲区(socket buf)发送的网卡的缓冲区(NIC buf)上;
  9. 从内核态切换回到用户态(第四次上下文切换)

如下图表示:

由上图可以很清晰地看到, 一次 read-send 涉及到了四次拷贝:

  1. 硬盘拷贝到内核缓冲区(DMA COPY);
  2. 内核缓冲区拷贝到应用程序缓冲区(CPU COPY);
  3. 应用程序缓冲区拷贝到socket缓冲区(CPU COPY);
  4. socket buf拷贝到网卡的buf(DMA COPY)。

上面共发生了4次复制和4次上文切换。直接把数据从“硬盘”中发送到“网卡缓冲区上”如何?

其中涉及到2次 CPU 中断, 还有4次的上下文切换。很明显,第2次和第3次的的 copy 只是把数据复制到 app buffer 又原封不动的复制回来, 为此带来了两次的 CPU COPY 和两次上下文切换, 是完全没有必要的。

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

3、sendFile 系统调用的IO流程

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

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

#include<sys/sendfile.h>

ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);

  1. out_fd 是写出的文件描述符,而且必须是一个 socket;
  2. in_fd 是读取内容的文件描述符,必须是一个真实的文件, 不能是管道或 socket;
  3. offset 是开始读的位置;
  4. count 是将要读取的字节数。

有了sendFile这个系统调用后, 我们 read-send 模型就可以简化为:

  1. 应用程序开始读文件的操作;
  2. 应用程序发起系统调用, 从用户态切换到内核态(第一次上下文切换);
  3. 内核态中把数据从硬盘文件读取到内核中间缓冲区;
  4. 通过 sendFile,在内核态中把数据从内核缓冲区复制到socket的缓冲区;
  5. 内核中再把数据从 socket 的缓冲区发送的网卡的 buf 上;
  6. 从内核态切换到用户态(第二次上下文切换)。

如下图所示:

涉及到数据拷贝变成:

  1. 硬盘拷贝到内核缓冲区(DMA COPY);
  2. 内核缓冲区拷贝到socket缓冲区(CPU COPY);
  3. socket 缓冲区拷贝到网卡的buf(DMA COPY)。

可以看到,一次 read-send 模型中, 利用 sendFile 系统调用后, 可以将4次数据拷贝减少到3次, 4次上下文切换减少到2次, 2次 CPU 中断减少到1次。

相对传统 I/O, 这种零拷贝技术通过减少两次上下文切换, 1次 CPU COPY, 可以将I/O 性能提高50%以上(网络数据, 未亲测)

开篇的概念中说到, 所谓的零拷贝的"零", 是指用户态和内核态之间的拷贝次数为0, 从这个定义上来说, 现在的这个零拷贝技术已经是真正的"零"了。

然而, 对性能追求极致的伟大的科学家和工程师们并不满足于此,精益求精的他们对中间第2次的 CPU COPY 依旧耿耿于怀, 想尽千方百计要去掉这一次没有必要的数据拷贝和 CPU 中断。

4、零拷贝的IO流程

支持 scatter-gather 特性的 sendFile 的 IO 流程:

Linux在内核2.4以后的版本中, Linux 内核对 socket 缓冲区描述符做了优化。通过这次优化, sendFile 系统调用可以在只复制 kernel buffer 的少量元信息的基础上, 把数据直接从 kernel buffer 复制到网卡的 buffer 中去,从而避免了从"内核缓冲区"拷贝到"socket缓冲区"的这一次拷贝。

这个优化后的 sendFile, 我们称之为支持 scatter-gather 特性的 sendFile。

在支持 scatter-gather 特性的 sendFile 的支撑下, 我们的 read-send 模型可以优化为:

  1. 应用程序开始读文件的操作;
  2. 应用程序发起系统调用, 从用户态进入到内核态(第一次上下文切换);
  3. 内核态中把数据从硬盘文件读取到内核中间缓冲区;
  4. 内核态中把数据在内核缓冲区的位置(offset)和数据大小(size)两个信息追加(append)到socket的缓冲区中去;
  5. 网卡的buf上根据socekt缓冲区的offset和size从内核缓冲区中直接拷贝数据;
  6. 从内核态返回到用户态(第二次上下文切换);

这个过程如下图所示:

最后数据拷贝变成只有两次 DMA COPY:

  1. 硬盘拷贝到内核缓冲区(DMA COPY);
  2. 内核缓冲区拷贝到网卡的 buf(DMA COPY)。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值