本篇文章我们来探讨一下Linux中的几种“零拷贝”技术,我们在 java nio,kafka,RocketMQ等框架中多多少少都有听到这个概念,零拷贝是IO性能提升非常重要的技术,也是Netty高性能的原因之一。
内存主要作用是在计算机运行时为操作系统和各种程序提供临时储存,操作系统的进程和进程之间是共享CPU和内存资源的。为了防止内存泄露需要一套完善且高效的内存管理机制。因此现代操作系提供了一种基于主内存抽象出来的概念:虚拟内存(Virtual Memory)。
虚拟内存
虚拟内存是计算机系统内存管理的一种技术,主要为每个进程提供私有的地址空间,让每个进程拥有一片连续完整的内存空间。而实际上,虚拟内存通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换,加载到物理内存中来
物理内存
物理内存指通过内存条而获得的内存空间,而虚拟内存则是指将硬盘的一块区域划分来作为内存。也就是说每个虚拟内存都对应一个特定的地址空间(物理内存或者磁盘存储空间)
在用户进程和物理内存引入虚拟内存后,当程序向系统申请内存时,系统为程序分配虚拟内存,虚拟内存地址会映射到物理地址,为了获取到实际的数据,CPU 需要将虚拟地址转换成物理地址。
这里的页表可以理解成是虚拟内存映射到物理列出的链表。
操作系统的核心是内核,可以访问受保护的内存空间,也有访问底层硬件设备的权限,为了避免用户进程直接操作内核,操作系统将虚拟内存划分为内核空间(Kernel-space)和 用户空间(User-space)。
内核空间
内核空间总是驻留在内存中,它是为操作系统的内核保留的。应用程序是不允许直接在该区域进行读写或直接调用内核代码定义的函数的
用户空间
每个用户进程都有一个独立的用户空间,处于用户态的进程不能访问内核空间中的数据和调用内核函数 ,因此要进行系统调用的时候,就要将进程切换到内核态。
DMA (Direct Memory Access):DMA的意思是直接内存访问,它允许外围设备(硬件子系统)直接访问系统主内存。有了 DMA之后,系统主内存 与 硬盘或网卡之间的数据传输可以绕开 CPU 的全程调度,大大解放了CPU的劳动力,下面我们来理解一下DMA
我们针对下面案例来分析一下IO的执行流程
RandomAccessFile randomAccessFile = new RandomAccessFile(new File("file.txt"),"rw");
byte[] arr = new byte[(int)file.length()];
//读
randomAccessFile.read(arr);
//把数据写到Sokcet
Socket socket = new ServerSocket(5555).accept();
//写
socket.getOutputStream().write(arr);
上面的案例完成了一次读写操作,先是从磁盘读取 file.txt 文件,内容存储到 byte[]中,然后把 byte[]中的数据写到socket 。那么在没有DMA的情况下IO是如何工作的呢?
由于整个IO过程都需要CPU亲力亲为,在数据的拷贝是非常消耗CPU性能的,为了提升IO性能出现了DMA技术。
解释一下图中的步骤
所以为什么要出现DMA呢?如果没有DMA,那么所有的拷贝操作都需要CPU的参与,拷贝数据非常消耗CPU资源,导致整体系统性能下降。所以DMA的出现解放了CPU,使得系统性能得到提升。
经过上面的流程,数据已经读取到用户缓冲区,接下来执行 write 向网络发送数据,先将数据从用户空间的页缓存拷贝到内核空间的网络缓冲区(socket buffer)中,然后再将写缓存中的数据拷贝到网卡设备完成数据发送,流程如下:
解释一下图中的步骤
整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝总共 4 次拷贝,以及 4 次内核切换,如下图:
下面是内核切换完整流程:
DMA拷贝虽然一定程度解放了CPU,但是涉及到的内核切换次数和数据拷贝次数太多,依然不能让IO性能达到最优。
零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。
它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载
也就是说所谓的零拷贝是消除CPU拷贝,但是DMA拷贝肯定是需要的。
使用 MMAP 的目的是将内核中缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,
从而实现内核缓冲区与应用程序内存的共享,这样在进行网络传输时,就可以减少内核空间到用户空间的拷贝,大致流程如下:
然而内核读缓冲区(read buffer)仍需将数据拷贝到内核写缓冲区(socket buffer), 整个拷贝过程会发生 4 次内核切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。
MMAP的问题是 4次内核切换,3次数据拷贝,拷贝次数和切换次数依然很多。
Sendfile在Linux2.1被引入 ,Sendfile 系统调用的引入,不仅减少了 CPU 拷贝的次数,还减少了上下文切换的次数通。过 Sendfile 数据可以直接在内核空间内部进行 I/O 传输,也就是说数据直接通过内核缓冲区(Kernel Buffer)拷贝到Socket缓冲区(Socket Buffer), 数据根部不经过用户空间,对于用户来说数据是不可见的。
基于 Sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝
Sendfile模式只需要2次内核态的切换,数据拷贝次数还是3次,它的问题是用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程。
在Linux2.4 对Sendfile进行了优化 ,它将内核缓冲区中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区中,由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中 。
也就是说它实现了将内核缓冲区中的数据直接拷贝到网卡设备,省去了内核缓冲区数据拷贝到网络缓冲区的过程,彻底消除了CPU考别。
整个拷贝过程会发生 2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝。
这种方式用户程序依然不能对数据进行修改的问题,它只适用于将数据从文件拷贝到 socket 套接字上的传输过程。
Linux 在 2.6.17 版本引入 Splice 系统调用 , 它通过在内核缓冲区和网络缓冲区之间建立通道(pipeline),来避免了两者之间的 CPU 拷贝操作。
整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝。
Splice 拷贝的问题是用户程序同样不能对数据进行修改。
它的思想是为每个进程都维护着一个缓冲区,这个缓冲区池能被同时映射到用户空间和内核态,内核和用户共享这个缓冲区池,这样就避免了一系列的拷贝操作。就目前而言缓冲区共享并不是一个非常成熟的方案,这里也不进行探讨。
零拷贝在数据进行IO时,对性能的影响是非常大的,零拷贝不是不拷贝,而是以消除CPU拷贝,减少拷贝,减少内核切换次数来提升IO性能为目的。
本文简单介绍了物理内存,虚拟内存,用户态,内核态等概念,并介绍了Linux系统中的零拷贝技术的集中方案,下面是各种零拷贝技术的对比
拷贝模式 | 函数 | CPU拷贝次数 | DMA拷贝次数 | 内核切换次数 |
---|---|---|---|---|
传统IO | read/write | 2 | 2 | 4 |
mmap | mmap/write | 1 | 2 | 4 |
sendfile | sendfile | 1 | 2 | 2 |
sendfile优化 | sendfile | 0 | 2 | 2 |
splice | splice | 0 | 2 | 2 |
文章到这就结束了,点赞还是要求一下的,万一屏幕面前的大帅哥,或者大漂亮一不小心就一键三连了啦,那我就是熬夜到头发掉光,也出下章,敬请期待《六.Netty入门到超神系列-NIO中的零拷贝实现》