大家好,又见面了,我是你们的朋友全栈君。
零拷贝技术指在计算机执行操作时,CPU不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及CPU的拷贝时间。它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除CPU的负载。
实现零拷贝用到的主要技术是DMA数据传输技术和内存区域映射技术
由于操作系统的进程之间是共享CPU和内存资源的,因此需要一套完整的内存管理机制防止内存泄漏。
现代操作系统提供了一种对贮存的抽象概念,即是虚拟内存。虚拟内存为每个进程提供了一个一致的,私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉
物理内存是相对于虚拟内存而言的。物理内存指通过物理内存条而获得的内存空间
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存,而实际上,虚拟内存通常是被分割成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换,加载到物理内存中来
虚拟内存地址和用户进程紧密相关,一般来说不同进程里的同一个虚拟地址指向的物理地址是不一样的,每个进程所能使用的虚拟地址大小和CPU位数有关。实际上物理内存可能远远小于虚拟内存的大小,每个用户进程维护了一个单独的页表,虚拟内存和物理内存就是用过这个页表实现地址空间的映射的。
用户进程申请并访问物理内存的过程
os的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。为了避免用户进程直接操作内核,os讲虚拟内存划分为两部分,一部分是内核空间,一部分是用户空间。在Linux系统中,内核模块运行在内核空间,对应的进程处于内核态,而用户程序运行在用户空间,对应的进程处于用户态。
内核空间总是驻留在内存中,它是为操作系统的内核保留的。应用程序是不允许直接在该区域进行读写或直接调用内核代码定义的函数的,按访问权限可以分为进程私有和进程共享:
每个普通的用户进程都有一个单独的用户空间,处于用户态的进程不能访问内核空间中的数据,也不能直接调用内核函数的,因此要进行系统调用时,就要将进程切换到内核态。用户空间包括以下几个内存区域:
Linux提供了轮询,I/O中断以及DMA传输这三种磁盘与主存之间的数据传输机制:
在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)]
DMA的全称叫做直接内存出去(Direct Memory Access),是一种允许外围设备直接访问系统主内存的机制。也就是说,基于DMA访问方式,系统主内存域硬盘或网卡之间的数据传输可以绕开CPU的全程调度。
整个数据传输操作在一个DMA控制器的控制下进行的。CPU除了在数据传输开始和结束时做一点处理外,在传输过程中CPU可以继续进行其他的工作。这样在大部分时间里,CPU计算和I/O操作都处于并行操作,使整个计算机系统的效率大大提高
传统的访问方式是通过write和read两个系统调用时显得,通过read函数读取文件到缓冲区中,然后通过write函数把缓存中的数据输出到网络端口
read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);
整个过程涉及2次CPU拷贝,2次DMA拷贝,以及4次上下文切换
零拷贝技术主要有三个实现思路:用户态直接I/O,减少数据拷贝次数以及写时复制技术
这种方式绕过内核,极大提高了性能
用户态直接I/O只能适用于不需要内核缓冲区处理的应用程序,这些应用程序通常在进程地址空间有自己的数据缓存机制,由于CPU和磁盘I/O之间的执行时间差距,会造成大量资源的浪费,解决方案是配合异步I/O使用
使用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拷贝
mmap主要是提高I/O性能,虽然减少了1次拷贝,但是如果mmap一个文件被另一个进程所截获,那么write系统调用会因为访问非法地址被SIGBUS信号终止,SIGBUS会杀死进程,服务器可能会因此被终止
sendfile系统调用在Linux内核版本2.1中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile不仅减少了CPU拷贝的次数,还减少了上下文切换的次数
sendfile(socket_fd, file_fd, len);
通过sendfile系统调用,数据可以直接在内核空间内部进行I/O传输,从而省去了数据在用户空间和内核空间之间的来回拷贝,但是对用户空间是完全不可见
基于sendfile的零拷贝方式过程会发生2次上下文切换,1次CPU拷贝和2次DMA拷贝:
sendfile存在的问题是用户程序不能对数据进行修改,只是单纯地完成了一次数据传输过程
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拷贝
sendfile+DMA gather copy只适用于将数据从文件拷贝到socket套接字上
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拷贝:
splice 拷贝方式也同样存在用户程序不能对数据进行修改的问题。除此之外,它使用了 Linux 的管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是它的两个文件描述符参数中有一个必须是管道设备。
写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时复制
Solaris上实现的fbuf(fast Buffer)
fbuf的思想是每个进程都维护着一个缓冲区池,这个缓冲区能被同时映射到用户空间和内核态,内核和用户共享这个缓冲区池
无论是传统 I/O 拷贝方式还是引入零拷贝的方式,2 次 DMA Copy 是都少不了的,因为两次 DMA 都是依赖硬件完成的。下面从 CPU 拷贝次数、DMA 拷贝次数以及系统调用几个方面总结一下上述几种 I/O 拷贝方式的差别。
拷贝方式 | CPU拷贝 | DMA拷贝 | 系统调用 | 上下文切换 |
---|---|---|---|---|
传统方式(read + write) | 2 | 2 | read / write | 4 |
内存映射(mmap + write) | 1 | 2 | mmap / write | 4 |
sendfile | 1 | 2 | sendfile | 2 |
sendfile + DMA gather copy | 0 | 2 | sendfile | 2 |
splice | 0 | 2 | splice | 2 |
在Java NIO中的通道(Channel)就相当于操作系统的内核空间的缓冲区,而缓冲区对应的相当于操作系统的用户空间中的用户缓冲区
堆外内存(DirectBuffer)在使用后需要应用程序手动回收,而堆内存(HeapBuffer)的数据在GC时可能会被自动回收。因此,在使用HeapBuffer读写数据时,为了避免缓冲区数据因为GC而丢失,NIO会先把HeapBuffer内部的数据拷贝到一个临时的DirectBuffer中的本地内存(native memory),这个拷贝涉及copyMemory()的调用,实现原理和memcpy()类似。最后将临时生成的DirectBuffer内部的数据的内存地址传给I/O调用函数,这样就避免了再去访问Java对象处理I/O读写
MappedByteBuffer是NIO基于内存映射(mmap)这种零拷贝方式的提供的一种实现,它继承自ByteBuffer。FileChannel定义了一个map()方法,它可以把一个文件从position位置开始的size大小区域映射为内存映像文件。
public abstract MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException;
RocketMQ 选择了 mmap + write 这种零拷贝方式,适用于业务级消息这种小块文件的数据持久化和传输;而 Kafka 采用的是 sendfile 这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。但是值得注意的一点是,Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。
消息队列 | 零拷贝方式 | 优点 | 缺点 |
---|---|---|---|
RocketMQ | mmap + write | 适用于小块文件传输,频繁调用时,效率很高 | 不能很好的利用 DMA 方式,会比 sendfile 多消耗 CPU,内存安全性控制复杂,需要避免 JVM Crash 问题 |
Kafka | sendfile | 可以利用 DMA 方式,消耗 CPU 较少,大块文件传输效率高,无内存安全性问题 | 小块文件效率低于 mmap 方式,只能是 BIO 方式传输,不能使用 NIO 方式 |
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/191201.html原文链接:https://javaforall.cn