前言

Kafka是一个非常优秀的消息开源系统,作为分布式的消息队列之所以能够实现高吞吐,其中的一个原因就是sendFile 的零拷贝

关于零拷贝

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

CPU COPY

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

DMA COPY

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

上下文切换

本文中的上下文切换时指由用户态切换到内核态, 以及由内核态切换到用户态
存在多次拷贝的原因?

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

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

所有涉及到数据传输的场景, 无非就一种:从硬盘上读取文件数据, 发送到网络上去。
这个场景我们简化为一个模型:

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. 从内核态切换回到用户态(第四次上下文切换)

![图片说明](https://uploadfiles.nowcoder.com/images/20191211/9094293_1576070784154_4D4779930518B8C6481F57B613C44A83 "图片标题")

由上图可以很清晰地看到, 一次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)缓冲区内, 从而可以减少上下文的切换和不必要数据的复制
有了sendFile这个系统调用后, 我们read-send模型就可以简化为:

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

![图片说明](https://uploadfiles.nowcoder.com/images/20191211/9094293_1576071188171_B15E58899E60865C572A5B5DEC8B1A1C "图片标题")
如图所示:
涉及到数据拷贝变成:

  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, 从这个定义上来说, 现在的这个零拷贝技术已经是真正的"零"了