零拷贝技术

本文详细介绍了零拷贝技术,包括其原理、主要实现手段(如DMA和内存映射)、物理内存和虚拟内存的区别,以及Linux中的各种I/O操作方式(如mmap+write、sendfile和splice)。着重讨论了JavaNIO和RocketMQ/Kafka的零拷贝实践,突出了其优缺点。

零拷贝技术

概述

零拷贝技术指在计算机执行操作时,CPU不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及CPU的拷贝时间。它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除CPU的负载。

实现零拷贝用到的主要技术是DMA数据传输技术和内存区域映射技术

  • 零拷贝机制可以减少数据在内核缓冲区和用户进程缓冲区之间反复的I/O拷贝操作
  • 零拷贝机制可以减少用户进程地址空间之间因为上下文切换而带来的CPU开销

物理内存和虚拟内存

由于操作系统的进程之间是共享CPU和内存资源的,因此需要一套完整的内存管理机制防止内存泄漏。

现代操作系统提供了一种对贮存的抽象概念,即是虚拟内存。虚拟内存为每个进程提供了一个一致的,私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉

物理内存

物理内存是相对于虚拟内存而言的。物理内存指通过物理内存条而获得的内存空间

虚拟内存

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存,而实际上,虚拟内存通常是被分割成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换,加载到物理内存中来

虚拟内存地址和用户进程紧密相关,一般来说不同进程里的同一个虚拟地址指向的物理地址是不一样的,每个进程所能使用的虚拟地址大小和CPU位数有关。实际上物理内存可能远远小于虚拟内存的大小,每个用户进程维护了一个单独的页表,虚拟内存和物理内存就是用过这个页表实现地址空间的映射的。

用户进程申请并访问物理内存的过程

  1. 用户进程向操作系统发出内存申请请求

  2. 系统会检查进程的虚拟地址空间是否被用完,如果有剩余,给进程分配虚拟地址

  3. 系统为这块虚拟地址创建内存映射,并将其放到该进程的页表当中

  4. 系统返回虚拟地址给用户进程,用户进程开始访问该虚拟地址

  5. CPU根据虚拟地址在此进程的页表中找到相应的内存映射,但是这个内存映射没有和物理内存关联,于是产生缺页中断

  6. os受到缺页中断后,分配真正的物理内存并将它关联到了页表相应的内存映射。中断处理完成后CPU就可以访问内存了

    缺页中断并不是每次都会发生的,只有os觉得有必要延迟分配内存的时候才用得着,很多时候会在系统分配真正物理内存并和内存映射进行关联

虚拟内存的优点
  • 地址空间:提供更大的地址空间,并且地址空间是连续的,使得程序编写,连接更加简单
  • 进程隔离:不同进程的虚拟地址之间没有关系,所以一个进程的操作不会对其他进程造成影响
  • 数据保护:每块虚拟内存都有相应的读写属性,这样就能保护程序的代码段不被修改,数据块不能被执行
  • 内存映射:有了虚拟内存之后,可以直接映射磁盘上的文件到虚拟地址空间
  • 共享内存
  • 物理内存管理

内核空间和用户空间

os的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。为了避免用户进程直接操作内核,os讲虚拟内存划分为两部分,一部分是内核空间,一部分是用户空间。在Linux系统中,内核模块运行在内核空间,对应的进程处于内核态,而用户程序运行在用户空间,对应的进程处于用户态。

内核空间

内核空间总是驻留在内存中,它是为操作系统的内核保留的。应用程序是不允许直接在该区域进行读写或直接调用内核代码定义的函数的,按访问权限可以分为进程私有和进程共享:

  • 进程私有的虚拟内存:每个进程都有单独的内核栈,页表,task结构以及mem_map结构等
  • 进程共享的虚拟内存:属于所有进程共享的内存区域,包括物理存储器,内核数据和内核代码区域

用户空间

每个普通的用户进程都有一个单独的用户空间,处于用户态的进程不能访问内核空间中的数据,也不能直接调用内核函数的,因此要进行系统调用时,就要将进程切换到内核态。用户空间包括以下几个内存区域:

  • 运行时栈:每当一个函数被调用时,该函数的返回类型和一些调用的信息被存储到栈顶,调用结束后调用信息会被弹出并释放掉内存
  • 运行时堆:用于存放进程运行中被动态分配的内存段,位于BSS和栈中间的地址位。堆的效率要比栈低的多
  • 代码段:存放CPU可以执行的机器指令,该部分内存只能读不能写。通常代码区是共享的,其他执行程序可调用它
  • 未初始化的数据段:存放未初始化的全局变量
  • 已初始化的数据段:存放已初始化的全局变量,比如静态全局变量,静态局部变量和常量等
  • 内存映射区域:例如将动态库,共享内存等虚拟空间的内存映射到物理空间的内存

Linux I/O读写方式

Linux提供了轮询,I/O中断以及DMA传输这三种磁盘与主存之间的数据传输机制:

  1. 轮询:基于死循环堆I/O端口进行不断检测
  2. I/O中断方式:当数据到达时,磁盘主动向CPU发起中断请求,由CPU自身负责数据的传输过程
  3. DMA传输:在I/O终端的基础上引入DMA磁盘控制器,由DMA磁盘控制器负责数据的传输,降低了I/O终端操作对CPU资源的大量消耗

I/O中断原理

在DMA技术出现之前俺,应用程序域磁盘之间的I/O操作都是通过CPU中断完成的。每次用户进程读取磁盘数据时,都需要CPU中断,然后发起I/O请求等待数据读取和拷贝完成,每次的I/O中断都导致CPU的上下文切换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BtUqpfIh-1638711733230)(C:\Users\Leoyu\AppData\Roaming\Typora\typora-user-images\image-20211130215135794.png)]

  1. 用户进程向CPU发起read系统抵用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回
  2. CPU在接收到指令以后对磁盘发起I/O请求,将磁盘数据先放入磁盘控制器缓冲区
  3. 数据准备完成以后,磁盘向CPU发起I/O中断
  4. CPU受到I/O中断以后将磁盘缓冲歌曲中的数据拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区
  5. 用户进程切换回用户态,解除阻塞状态

DMA传输

DMA的全称叫做直接内存出去(Direct Memory Access),是一种允许外围设备直接访问系统主内存的机制。也就是说,基于DMA访问方式,系统主内存域硬盘或网卡之间的数据传输可以绕开CPU的全程调度。

整个数据传输操作在一个DMA控制器的控制下进行的。CPU除了在数据传输开始和结束时做一点处理外,在传输过程中CPU可以继续进行其他的工作。这样在大部分时间里,CPU计算和I/O操作都处于并行操作,使整个计算机系统的效率大大提高

  1. 用户进程向CPU发起read系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回
  2. CPU在接收到指令以后对DMA磁盘控制器发起调度指令
  3. DMA磁盘控制器对磁盘发起I/O请求,将磁盘数据先放入磁盘控制器缓冲区,CPU全程不参与此过程
  4. 数据读取完成后,DMA磁盘控制器会接收到磁盘的通知,将数据从磁盘控制器缓冲区拷贝到内核缓冲区
  5. DMA磁盘控制器向CPU发送数据都玩的信号,由CPU负责将数据从内核缓冲区拷贝到用户缓冲区
  6. 用户进程由内核态切换回用户态,解除阻塞状态

传统I/O操作

传统的访问方式是通过write和read两个系统调用时显得,通过read函数读取文件到缓冲区中,然后通过write函数把缓存中的数据输出到网络端口

read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);

整个过程涉及2次CPU拷贝,2次DMA拷贝,以及4次上下文切换

  • 上下文切换:当用户程序向内核发起系统调用时,CPU将用户进程从用户态切换为内核态,当系统调用返回时,CPU将用户进程从内核态切换回用户态
  • CPU拷贝:由CPU直接处理数据的传送,数据拷贝时会一直占用CPU的资源
  • DMA拷贝:由CPU向DMA磁盘下达指令,让DMA控制器处理数据传送,传送完毕时把信息反馈给CPU,从而减轻CPU资源的占用率

零拷贝方式(前面铺垫一大堆,终于是到重点了)

零拷贝技术主要有三个实现思路:用户态直接I/O,减少数据拷贝次数以及写时复制技术

  • 用户态直接I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。直接I/O不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝
  • 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前的主流零拷贝技术的实现思路
  • 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间,如果只是数据读取则不需要进行拷贝

用户态直接I/O

这种方式绕过内核,极大提高了性能

用户态直接I/O只能适用于不需要内核缓冲区处理的应用程序,这些应用程序通常在进程地址空间有自己的数据缓存机制,由于CPU和磁盘I/O之间的执行时间差距,会造成大量资源的浪费,解决方案是配合异步I/O使用

mmap+write

使用mmap+write代替原来的read+write方式,减少了1次CPU拷贝操作。mmap是Linux提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址

tmp_buf = mmap(file_fd, len);
write(socket_fd, tmp_buf, len);

使用mmap的目的是将内核中读缓冲区的地址与用户空间的缓冲区进行映射,从而实现内核缓冲区与应用陈鼓内存的共享,省去了将数据从内核读缓冲区拷贝到用户缓冲区的过程,内核读缓冲区仍需将数据到内核写缓冲区

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qlYjcY5R-1638711733234)(C:\Users\Leoyu\AppData\Roaming\Typora\typora-user-images\image-20211201000703550.png)]

基于mmap+write的零拷贝方式,整个拷贝过程会发生4次上下文切换,1次CPU拷贝和2次DMA拷贝

  1. 用户进程通过mmap函数向内核发起系统调用,上下文从用户态切换为内核态
  2. 将用户进程的内核空间的读缓冲区与用户空间的缓存区进行内存地址映射
  3. CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间的读缓冲区(read buffer)
  4. 上下文从内核态切换回用户态,mmap系统调用执行返回
  5. 用户进程通过write函数向内核发起系统调用,上下文从用户态切换为内核态
  6. CPU将读缓冲区中的数据拷贝到的网络缓冲区(socket buffer)
  7. CPU利用DMA控制器将数据从网络缓冲区拷贝到网卡进行数据传输
  8. 上下文奇幻会用户态,write系统调用执行返回

mmap主要是提高I/O性能,虽然减少了1次拷贝,但是如果mmap一个文件被另一个进程所截获,那么write系统调用会因为访问非法地址被SIGBUS信号终止,SIGBUS会杀死进程,服务器可能会因此被终止

sendfile

sendfile系统调用在Linux内核版本2.1中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile不仅减少了CPU拷贝的次数,还减少了上下文切换的次数

sendfile(socket_fd, file_fd, len);

通过sendfile系统调用,数据可以直接在内核空间内部进行I/O传输,从而省去了数据在用户空间和内核空间之间的来回拷贝,但是对用户空间是完全不可见

基于sendfile的零拷贝方式过程会发生2次上下文切换,1次CPU拷贝和2次DMA拷贝:

  1. 用户进程通过sendfile函数向内核发起系统调用,上下文从用户态切换为内核态
  2. CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间的读缓冲区
  3. CPU将读缓冲区的数据拷贝到网络缓冲区
  4. CPU利用DMA控制器将数据从网络缓冲区拷贝到网卡进行数据传输
  5. 上下文切换回用户态,sendfile系统调用返回

sendfile存在的问题是用户程序不能对数据进行修改,只是单纯地完成了一次数据传输过程

sendfile+DMA gather copy

Linux 2.4版本的内核对sendfile系统调用进行修改,为DMA拷贝引入了gather操作。它将内核空间的读缓冲区中对应的数据描述信息记录到相应的网络缓冲区中,由DMA根据内存地址,地址偏移量将数据批量地从读缓冲区拷贝到网卡设备中,这样就省去了内核空间中仅剩的1次CPU拷贝操作

sendfile(socket_fd, file_fd, len);

在硬件的支持下,sendfile 拷贝方式不再从内核缓冲区的数据拷贝到 socket 缓冲区,取而代之的仅仅是缓冲区文件描述符和数据长度的拷贝,这样 DMA 引擎直接利用 gather 操作将页缓存中数据打包发送到网络中即可,本质就是和虚拟内存映射的思路类似。

基于sendfile+DMA gather copy系统调用的零拷贝方式,整个过程会发生2次上下文切换,0次CPU拷贝以及2次DMA拷贝

  1. 用户进程通过sendfile函数向内核发起系统调用,上下文切换为内核态
  2. CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间的读缓冲区
  3. CPU将读缓冲区的文件描述符和数据长度拷贝到网络缓冲区
  4. 基于文件描述符和数据长度,CPU利用DMA控制器的gather/scatter操作直接批量地将数据从内核的读缓冲区拷贝到网卡进行数据传输
  5. 上下文切换回用户态,sendfile系统调用执行返回

sendfile+DMA gather copy只适用于将数据从文件拷贝到socket套接字上

splice

Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝。

splice(fd_in, off_in, fd_out, off_out, len, flags);

splice系统调用可以在内核空间的读缓冲区和网络缓冲区之间建立管道,从而避免了两者之间的CPU拷贝操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-odtI1T7l-1638711733236)(C:\Users\Leoyu\AppData\Roaming\Typora\typora-user-images\image-20211201004623176.png)]

基于splice系统调用的零拷贝方式,整个拷贝过程会发生2

次上下文切换,0次CPU拷贝以及2次DMA拷贝:

  1. 用户进程通过splice函数向内核发起系统调用,上下文切换为内核态
  2. CPU利用DMA控制器将数据从主存或硬盘拷贝至内核的读缓冲区
  3. CPU在内核的读缓冲区和网络缓冲区之间建立起管道
  4. CPU利用DMA控制器将数据从网络缓冲区拷贝至网卡进行数据传输
  5. 上下文切换回用户态,splice系统调用执行返回

splice 拷贝方式也同样存在用户程序不能对数据进行修改的问题。除此之外,它使用了 Linux 的管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是它的两个文件描述符参数中有一个必须是管道设备。

写时复制

写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时复制

缓冲区共享

Solaris上实现的fbuf(fast Buffer)

fbuf的思想是每个进程都维护着一个缓冲区池,这个缓冲区能被同时映射到用户空间和内核态,内核和用户共享这个缓冲区池

Linux零拷贝对比

无论是传统 I/O 拷贝方式还是引入零拷贝的方式,2 次 DMA Copy 是都少不了的,因为两次 DMA 都是依赖硬件完成的。下面从 CPU 拷贝次数、DMA 拷贝次数以及系统调用几个方面总结一下上述几种 I/O 拷贝方式的差别。

拷贝方式CPU拷贝DMA拷贝系统调用上下文切换
传统方式(read + write)22read / write4
内存映射(mmap + write)12mmap / write4
sendfile12sendfile2
sendfile + DMA gather copy02sendfile2
splice02splice2

Java NIO零拷贝实现

在Java NIO中的通道(Channel)就相当于操作系统的内核空间的缓冲区,而缓冲区对应的相当于操作系统的用户空间中的用户缓冲区

  • 通道是全双工通信的,既可能是读缓冲区,也可能是网络缓冲区
  • 缓冲区分为堆内存和堆外内存,这是通过malloc()分配出来的用户态内存

堆外内存(DirectBuffer)在使用后需要应用程序手动回收,而堆内存(HeapBuffer)的数据在GC时可能会被自动回收。因此,在使用HeapBuffer读写数据时,为了避免缓冲区数据因为GC而丢失,NIO会先把HeapBuffer内部的数据拷贝到一个临时的DirectBuffer中的本地内存(native memory),这个拷贝涉及copyMemory()的调用,实现原理和memcpy()类似。最后将临时生成的DirectBuffer内部的数据的内存地址传给I/O调用函数,这样就避免了再去访问Java对象处理I/O读写

MappedByteBuffer

MappedByteBuffer是NIO基于内存映射(mmap)这种零拷贝方式的提供的一种实现,它继承自ByteBuffer。FileChannel定义了一个map()方法,它可以把一个文件从position位置开始的size大小区域映射为内存映像文件。

public abstract MappedByteBuffer map(MapMode mode, long position, long size)
        throws IOException;
  • mode:限定内存映射区域对内存映像文件的访问模式,包括只可读,可读可写和写时拷贝三种模式
  • position:文件映射的起始地址,对应内存映射区域的首地址
  • size:文件映射的字节长度,从position往后的字节数,对应内存映射区域的大小

RocketMQ和Kafka对比

RocketMQ 选择了 mmap + write 这种零拷贝方式,适用于业务级消息这种小块文件的数据持久化和传输;而 Kafka 采用的是 sendfile 这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。但是值得注意的一点是,Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。

消息队列零拷贝方式优点缺点
RocketMQmmap + write适用于小块文件传输,频繁调用时,效率很高不能很好的利用 DMA 方式,会比 sendfile 多消耗 CPU,内存安全性控制复杂,需要避免 JVM Crash 问题
Kafkasendfile可以利用 DMA 方式,消耗 CPU 较少,大块文件传输效率高,无内存安全性问题小块文件效率低于 mmap 方式,只能是 BIO 方式传输,不能使用 NIO 方式
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值