关于零拷贝的一些理解

术语

零拷贝

"零拷贝"中的"拷贝"是操做系统在I/O操做中,将数据从一个内存区域复制到另一个内存区域. 而"零"并非指0次复制, 更多的是指在用户态和内核态以前的复制是0次.java

CPU COPY

经过计算机的组成原理咱们知道, 内存的读写操做是须要CPU的协调数据总线,地址总线和控制总线来完成的linux

所以在"拷贝"发生的时候,每每须要CPU暂停现有的处理逻辑,来协助内存的读写.这种咱们称为CPU COPYweb

cpu copy不但占用了CPU资源,还占用了总线的带宽.数据库

DMA COPY

DMA(DIRECT MEMORY ACCESS)是现代计算机的重要功能. 它的一个重要 的特色就是, 当须要与外设进行数据交换时, CPU只须要初始化这个动做即可以继续执行其余指令,剩下的数据传输的动做彻底由DMA来完成性能优化

能够看到DMA COPY是能够避免大量的CPU中断的bash

上下文切换

本文中的上下文切换时指由用户态切换到内核态, 以及由内核态切换到用户态服务器

存在屡次拷贝的缘由

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

  2. 出于"readahead cache"和异步写入等等性能优化的须要, 操做系统在内核态中也增长了一个"内核缓冲区"(kernel buffer). 读取数据时并非直接把数据读取到应用程序的buffer, 而先读取到kernel buffer, 再由kernel buffer复制到应用程序的buffer. 所以,数据在被应用程序使用以前,可能须要被屡次拷贝app

都有哪些没必要要的拷贝

再回答这个问题以前, 咱们先来看一个应用场景异步

回想现实世界的全部系统中, 不论是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. 从内核态切换回到用户态(第四次上下文切换)
复制代码

以下图表示:

image

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

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

其中涉及到2次cpu中断, 还有4次的上下文切换

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

linux的零拷贝技术就是为了优化掉这两次没必要要的拷贝

sendFile

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. 从内核态切换到用户态(第二次上下文切换)
复制代码

以下图所示:

image

涉及到数据拷贝变成:

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中断

支持scatter-gather特性的sendFile

在内核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. 从内核态返回到用户态(第二次上下文切换)
复制代码

这个过程以下图所示:

image

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

1. 硬盘拷贝到内核缓冲区(DMA COPY)
2. 内核缓冲区拷贝到网卡的buf(DMA COPY)
复制代码

完美

mmap和sendFile

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

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

mmap相对于sendFile的好处:

  1. 多个进程访问同一个文件时, 能够节省大量内存.
  2. 因为数据在内核中直接发送到网络上, 用户态中的应用程序没法再次操做数据.

mmap相对于sendFile的缺点:

  1. 当内存映射一个文件,而后调用write,而另外一个进程截断同一个文件,可能被总线错误信号SIGBUS中断, 这个信号的默认行为是kill掉进程和dump core.这个是通常服务器不能接受的
  2. 连续顺序访问小文件时,不如sendFile的readahead cahce高效

参考

juejin.im/post/5c70d8… www.jianshu.com/p/e9f422586… www.ibm.com/developerwo… blog.csdn.net/u014303647/… www.jianshu.com/p/f3bea2f6c…

相关文章
相关标签/搜索