io零拷贝

零拷贝是一种减少CPU在数据传输中的参与的技术,主要通过sendfile系统调用和mmap内存映射实现。sendfile避免了数据从内核到用户空间再到内核的拷贝,而mmap通过内存映射减少了拷贝次数,两者都提升了文件传输和网络I/O的效率。Java的NIO中的MappedByteBuffer利用了mmap,但小文件或单进程访问时效率可能不高。
摘要由CSDN通过智能技术生成

零拷贝

“零拷贝”描述了计算机操作,其中CPU不执行将数据从一个存储区复制到另一个存储区的任务。通过网络传输文件时,通常用于节省CPU周期和内存带宽。

减少拷贝次数,减少不必要的数据拷贝,就算作“零拷贝”。

狭义零拷贝

Linux 2.4 内核新增 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。这是真正操作系统意义上的零拷贝(也就是狭义零拷贝)。
在这里插入图片描述

为了解决CPU的上下文切换,聪明的程序员们提出了 DMA(Direct Memory Access,直接内存存取),是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。

在这里插入图片描述

  • DMA 等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;
  • 用户进程,将内核缓冲区的数据 copy 到用户空间。

普通的io发生四次复制

  • 第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存)引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。(用户态到内核)
  • 第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,(内核到用户)
  • 第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 socket 缓冲区(用户到内核)
  • 第四次拷贝,数据异步的从 socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。()
  • write 方法返回,再次从内核态切换到用户态。

零拷贝
目的:减少 IO 流程中不必要的拷贝

Linux 支持的(常见)零拷贝
1、mmap 内存映射
在 Linux 中我们可以使用 mmap 用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。虚拟内存与物理内存的映射
在这里插入图片描述

映射关系可以分为两种

  • 文件映射:磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。
  • 匿名映射:初始化全为 0 的内存空间。

而对于映射关系是否共享又分为

  • 私有映射(MAP_PRIVATE) 多进程间数据共享,修改不反应到磁盘实际文件,是一个 copy-on- write(写时复制) 的映射方式。
  • 共享映射(MAP_SHARED) 多进程间数据共享,修改反应到磁盘实际文件中。

因此总结起来有4种组合
私有文件映射:多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中。
私有匿名映射:mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存 (malloc分配大内存会调用mmap)。 例如开辟新进程时,会为每个进程分配虚拟的地址空间,这些虚拟地址映射的物理内存空间各个进程间读的时候共享,写的时候会 copy-on-write。
共享文件映射:多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件的修改会反应到实际物理文件中,他也是进程间通信(IPC)的一种机制。
共享匿名映射:这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理内存页,这也就实现了父子进程通信(IPC)。

mmap 只是在虚拟内存分配了地址空间,只有在第一次访问虚拟内存的时候才分配物理内存。
在 mmap 之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多。

mmap 是怎么对上面传统 IO 进行优化的。
在这里插入图片描述

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
现在,你只需要从内核缓冲区拷贝到 socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。
在这里插入图片描述

继续优化:sendfile函数
其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。

如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用 write 方法时,从内核缓冲区进入到 socket,这时,是没有上下文切换的,因为都在内核空间。

最后,数据从 socket 缓冲区进入到协议栈。此时,数据经过了 3 次拷贝,2 次上下文切换。那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?

3、Sendfile With DMA Scatter/Gather Copy
避免了从内核缓冲区拷贝到 socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

Scatter/Gather 可以看作是 sendfile 的增强版,批量 sendfile。

在这里插入图片描述

现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 socket buffer,基本无消耗。

mmap 和 sendFile 的区别

  • mmap 适合小数据量读写,sendFile 适合大文件传输。
  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
  • RocketMQ 在消费消息时,使用了 mmap。Kafka 使用了 sendFile。

零拷贝在Java中的应用
NIO
MappedByteBuffer:
NIO 中的 FileChannel.map() 方法其实就是采用了操作系统中的内存映射方式,底层就是调用 Linux mmap() 实现的。
将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过 SocketChannel 发送,还是需要CPU进行数据的拷贝。
使用 MappedByteBuffer,小文件,效率不高;一个进程访问,效率也不高。
MappedByteBuffer 只能通过调用 FileChannel 的 map() 取得,再没有其他方式。
使用 MappedByteBuffer 类要注意的是:mmap的文件映射,在 full gc 时才会进行释放。当 close 时,需要手动清除内存映射文件,可以反射调用 sun.misc.Cleaner 方法。
sendfile

  • FileChannel.transferTo() 方法直接将当前通道内容传输到另一个通道,没有涉及到 Buffer 的任何操作,NIO 中的 Buffer 是 JVM 堆或者堆外内存,但不论如何他们都是操作系统内核空间的内存。
    如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值