I'm trying to understand zero-copy | The interview cycle (author)

talk about background

The first time I came into contact with zero copy, I was cracking up on various concepts of kernel, context switching, DMA, MMAP... After reading a lot of articles, I don't know if you also feel that it is foggy and confusing. Maybe to make one thing clear, we must first get close to the "0 distance" scene that programmers can feel.

Maybe you think zero copy is a common thread in interview outlines and useless. But you do touch it every day, and you don't find it. For example: consumers of rocketMQ and Kafka.

Look carefully, why do these two involve zero-copy? Copy - Ctrl+Ca familiar operation.

The process of consumer initiation of consumption is as follows: data is read from disk and transmitted to consumers through network transmission. And the process of transferring data from disk to network card is data copy. Data movement definitely requires resource consumption, such as CPU, context switching, etc. However, in the process of simple data copying, the internal data flow is not simple. So after reading the following introduction, you will definitely understand why zero copy is needed?

Traditional IO copy

Example

The following is an example of consumer data consumption. In order to simulate the process of data from disk to network card, I borrowed a piece of code so that Java students can feel what we are doing. More precisely, we are explaining why traditional IO is not ideal. :

// 模拟读取topic_data.db这个数据文件
File file = new File("D://topic_data.db");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);

// 将读取的字节码通过socket传输出去
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
复制代码

Graphic description

The following is a flow chart of traditional IO reading and writing that I hand-painted based on online data to explain the execution process of the above code:

Figure 1: Traditional IO read and write

内核空间与用户空间: In order to ensure the security of the kernel, current operating systems generally enforce that user processes cannot directly operate the kernel. The specific implementation method basically divides the virtual address space into two parts by the operating system, one part is the kernel space and the other part is the user space.

The reading process and writing process are roughly like this. If it is not particularly easy to understand, it is recommended to force memory, because this is the basis for the following to continue to explore:

  1. The application calls kernel instructions to read the file.
  2. The file is copied to the kernel buffer ( ReadBuffer) by the DMA controller
  3. The cpu copies the data from the kernel cache buffer to the application buffer.
  4. The cpu copies the data from the application buffer to the kernel buffer ( SocketBuffer)
  5. Copy data to network card via DMA controller
  6. Notify the application when the copy is complete.

那么,DMA又是个啥?DMA这东东翻译过来叫直接内存访问,顾名思义,直接访问到内存。你想想,将数据从一块区域拷贝到另外一块区域,cpu肯定得负责搬运。而这个DMA的诞生让cpu尽量不参与搬运,更多的时间去处理其他的事情。你可以参考正规的解释:

Direct Memory Access(存储器直接访问)。这是指一种高速的数据传输操作,允许在外部设备和存储器之间直接读写数据。整个数据传输操作在一个称为"DMA控制器"的控制下进行的。CPU除了在数据传输开始和结束时做一点处理外(开始和结束时候要做中断处理),在传输过程中CPU可以进行其他的工作(前提是未设置停止CPU访问)。这样,在大部分时间里,CPU和输入输出都处于并行操作。因此,使整个计算机系统的效率大大提高。

探究细节

简单的流程梳理,我们对一些细节做下统计:

  • 上下文切换次数(图1中的粉色圆圈):4次
  • 数据拷贝次数(图1中的绿色圆圈):4次
  • cpu参与次数:2次

很明显,每次操作都需要内核及硬件的成本付出,如何减少对应的次数就是零拷贝真正要解决的问题。

上下文切换

为什么用户空间切换到内核空间开销比较大?甚至有人叫这破玩意叫上下文切换?我摘抄一段文字。你可以磨一磨、品一品:

当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去系统调用,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当int中断执行时就会由用户态栈转向内核态栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。

系统调用一般都需要保存用户程序的上下文(context), 在进入内核的时候需要保存用户态的寄存器,在内核态返回用户态的时候会恢复这些寄存器的内容。这是一个开销的地方。 如果需要在不同用户程序间切换的话,那么还要更新cr3寄存器,这样会更换每个程序的虚拟内存到物理内存映射表的地址,也是一个比较高负担的操作。

再谈零拷贝

讲了这么多传统IO,目的是为了理解零拷贝做铺垫,零拷贝是基于传统IO的改进版。

在开始之前,我们先看看什么是虚拟内存地址:

虚拟内存地址

所有现代操作系统都使用虚拟内存,使用虚拟地址取代物理地址,这样做的好处就是:

1、多个虚拟内存可以指向同一个物理地址 2、虚拟内存空间可以远远大于物理内存空间

如果把图1内核空间用户空间的虚拟地址映射到同一个物理地址,就不需要cpu将数据在内核空间用户空间来回拷贝。

图2:虚拟内存地址

mmap+write与sendfile

mmap+write就是利用虚拟内存地址的方式,减少内核空间和用户空间的数据拷贝,从而减少数据拷贝次数。我们看下mmap+write的读写流程:

图3:mmap的读流程

从上图可以看出,mmap与传统IO读流程的区别只是在内核空间与用户空间数据采用的虚拟内存地址的方式共享内存,减少了一次Cpu的数据拷贝,然而,上下文切换次数并未减少。

write()流程如下:

图4:mmap的写流程

由于应用程序缓冲区与内核缓冲区共享内存,cpu只需要将ReadBuffer数据拷贝到SocketBuffer

那么,来综合看下mmap+write的方式成本消耗如何?

  • 上下文切换次数:4次
  • 数据拷贝次数:3次
  • cpu参与次数:1次

mmap+write相对传统Io,减少了一次cpu的数据拷贝,然而上下文切换次数并没有减少,你试想一下,如果应用程序与内核只做一次交互不就可以减少2次上下文切换,因此,sendfile()相对mmap()+write()就是做了这一点的结合性改善。参考下图(盗图一张,不留名,嘿嘿):

写在最后

说了这么多传统IO、mmap以及sendfile,我们来做下比对:

  • 传统 IO 执行的话需要 4 次上下文切换(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态)和 4 次拷贝(磁盘文件 DMA 拷贝到内核缓冲区,内核缓冲区 CPU 拷贝到用户缓冲区,用户缓冲区 CPU 拷贝到 Socket 缓冲区,Socket 缓冲区 DMA 拷贝到协议引擎)。
  • mmap 将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上,适合小数据量读写,需要 4 次上下文切换(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态)和3 次拷贝(磁盘文件DMA拷贝到内核缓冲区,内核缓冲区 CPU 拷贝到 Socket 缓冲区,Socket 缓冲区 DMA 拷贝到协议引擎)。
  • sendfile 是将读到内核空间的数据,转到 socket buffer,进行网络发送,适合大文件传输,只需要 2 次上下文切换(用户态 -> 内核态 -> 用户态)和 2 次拷贝(磁盘文件 DMA 拷贝到内核缓冲区,内核缓冲区 DMA 拷贝到协议引擎)。

此外,零拷贝其实也没有真正意义上的清零,只是相对传统IO进行了性能优化:

  • 1.采用虚拟内存地址的方式共享内存,减少内核与用户空间数据拷贝的次数。
  • 2.拷贝次数的减少,间接减少了cpu的参与次数。
  • 3.sendfile这种方式减少了上下文切换的次数。
  • 4.同时,DMA控制也是一种减少cpu参与数据拷贝的方式。

因此,减少数据拷贝CPU参与上下文切换才是零拷贝最具灵魂、最绝的一笔!

作者介绍

keaizhuzhu,公众号面试怪圈小编,网站面试怪圈站长,曾就职于阿里巴巴本地生活,目前就职于京东做后端开发。

编写过《Java面试怪圈内卷手册》面试秘籍,全网阅读量过万次。

官网:http://www.msgqer.com。旨在分享前端、后端、大数据、各种中间件技术的面试资料,总访问量数万次。点击【阅读原文】可直达。

Java后端在线面试题地址:http://www.msgqer.com/case/fwCase

Guess you like

Origin juejin.im/post/7084136930811576357