零拷贝原来这么简单!

news2024/11/14 16:19:22

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。

接下来,让我们来理一理啊。

拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存、硬盘等等一大堆东西。

这么复杂的设备要进行读写操作,其中繁琐和复杂程度可想而知。

传统I/O的读写过程

如果要了解零拷贝,那就必须要知道一般情况下,计算机是如何读写数据的,我把这种情况称为传统 I/O。

数据读写的发起者是计算机中的应用程序,比如我们常用的浏览器、办公软件、音视频软件等。

而数据的来源呢,一般是硬盘、外部存储设备或者是网络套接字(也就是网络上的数据通过网口+网卡的处理)。

过程本来是很复杂的,所以大学课程里要通过《操作系统》、《计算机组成原理》来专门讲计算机的软硬件。

简化版读操作流程

那么细的没办法讲来,所以,我们把这个读写过程简化一下,忽略大多数细节,只讲流程。

图片

上图是应用程序进行一次读操作的过程。

  1. 应用程序先发起读操作,准备读取数据了;

  2. 内核将数据从硬盘或外部存储读取到内核缓冲区;

  3. 内核将数据从内核缓冲区拷贝到用户缓冲区;

  4. 应用程序读取用户缓冲区的数据进行处理加工;

详细的读写操作流程

下面是一个更详细的 I/O 读写过程。这个图可好用极了,我会借助这个图来厘清 I/O 操作的一些基础但非常重要的概念。

图片

先看一下这个图,上面红粉色部分是读操作,下面蓝色部分是写操作。

如果一下子看着有点儿迷糊的话,没关系,看看下面几个概念就清楚了。

应用程序

就是安装在操作系统上的各种应用。

系统内核

系统内核是一些列计算机的核心资源的集合,不仅包括CPU、总线这些硬件设备,也包括进程管理、文件管理、内存管理、设备驱动、系统调用等一些列功能。

外部存储

外部存储就是指硬盘、U盘等外部存储介质。

内核态

  • 内核态是操作系统内核运行的模式,当操作系统内核执行特权指令时,处于内核态。

  • 在内核态下,操作系统内核拥有最高权限,可以访问计算机的所有硬件资源和敏感数据,执行特权指令,控制系统的整体运行。

  • 内核态提供了操作系统管理和控制计算机硬件的能力,它负责处理系统调用、中断、硬件异常等核心任务。

用户态

这里的用户可以理解为应用程序,这个用户是对于计算机的内核而言的,对于内核来说,系统上的各种应用程序会发出指令来调用内核的资源,这时候,应用程序就是内核的用户。

  • 用户态是应用程序运行的模式,当应用程序执行普通的指令时,处于用户态。

  • 在用户态下,应用程序只能访问自己的内存空间和受限的硬件资源,无法直接访问操作系统的敏感数据或控制计算机的硬件设备。

  • 用户态提供了一种安全的运行环境,确保应用程序之间相互隔离,防止恶意程序对系统造成影响。

模式切换

计算机为了安全性考虑,区分了内核态和用户态,应用程序不能直接调用内核资源,必须要切换到内核态之后,让内核来调用,内核调用完资源,再返回给应用程序,这个时候,系统在切换会用户态,应用程序在用户态下才能处理数据。

上述过程其实一次读和一次写都分别发生了两次模式切换。

图片

内核缓冲区

内核缓冲区指内存中专门用来给内核直接使用的内存空间。可以把它理解为应用程序和外部存储进行数据交互的一个中间介质。

应用程序想要读外部数据,要从这里读。应用程序想要写入外部存储,要通过内核缓冲区。

用户缓冲区

用户缓冲区可以理解为应用程序可以直接读写的内存空间。因为应用程序没法直接到内核读写数据, 所以应用程序想要处理数据,必须先通过用户缓冲区。

磁盘缓冲区

PageCache

  • PageCache 是 Linux 内核对文件系统进行缓存的一种机制。它使用空闲内存来缓存从文件系统读取的数据块,加速文件的读取和写入操作。

  • 当应用程序或进程读取文件时,数据会首先从文件系统读取到 PageCache 中。如果之后再次读取相同的数据,就可以直接从 PageCache 中获取,避免了再次访问文件系统。

  • 同样,当应用程序或进程将数据写入文件时,数据会先暂存到 PageCache 中,然后由 Linux 内核异步地将数据写入磁盘,从而提高写入操作的效率。

再说数据读写操作流程

上面弄明白了这几个概念后,再回过头看一下那个流程图,是不是就清楚多了。

读操作
  1. 首先应用程序向内核发起读请求,这时候进行一次模式切换了,从用户态切换到内核态;

  2. 内核向外部存储或网络套接字发起读操作;

  3. 将数据写入磁盘缓冲区;

  4. 系统内核将数据从磁盘缓冲区拷贝到内核缓冲区,顺便再将一份(或者一部分)拷贝到 PageCache;

  5. 内核将数据拷贝到用户缓冲区,供应用程序处理。此时又进行一次模态切换,从内核态切换回用户态;

写操作
  1. 应用程序向内核发起写请求,这时候进行一次模式切换了,从用户态切换到内核态;

  2. 内核将要写入的数据从用户缓冲区拷贝到 PageCache,同时将数据拷贝到内核缓冲区;

  3. 然后内核将数据写入到磁盘缓冲区,从而写入磁盘,或者直接写入网络套接字。

瓶颈在哪里

但是传统I/O有它的瓶颈,这才是零拷贝技术出现的缘由。瓶颈是啥呢,当然是性能问题,太慢了。尤其是在高并发场景下,I/O性能经常会卡脖子。

那是什么地方耗时了呢?

数据拷贝

在传统 I/O 中,数据的传输通常涉及多次数据拷贝。数据需要从应用程序的用户缓冲区复制到内核缓冲区,然后再从内核缓冲区复制到设备或网络缓冲区。这些数据拷贝过程导致了多次内存访问和数据复制,消耗了大量的 CPU 时间和内存带宽。

用户态和内核态的切换

由于数据要经过内核缓冲区,导致数据在用户态和内核态之间来回切换,切换过程中会有上下文的切换,如此一来,大大增加了处理数据的复杂性和时间开销。

每一次操作耗费的时间虽然很小,但是当并发量高了以后,积少成多,也是不小的开销。所以要提高性能、减少开销就要从以上两个问题下手了。

这时候,零拷贝技术就出来解决问题了。

什么是零拷贝

问题出来数据拷贝和模态切换上。

但既然是 I/O 操作,不可能没有数据拷贝的,只能减少拷贝的次数,还有就是尽量将数据存储在离应用程序(用户缓冲区)更近的地方。

而区分用户态和内核态有其他更重要的原因,不可能单纯为了 I/O 效率就改变这种设计吧。那也只能尽量减少切换的次数。

零拷贝的理想状态就是操作数据不用拷贝,但是显示情况下并不一定真的就是一次复制操作都没有,而是尽量减少拷贝操作的次数。

要实现零拷贝,应该从下面这三个方面入手:

  1. 尽量减少数据在各个存储区域的复制操作,例如从磁盘缓冲区到内核缓冲区等;

  2. 尽量减少用户态和内核态的切换次数及上下文切换;

  3. 使用一些优化手段,例如对需要操作的数据先缓存起来,内核中的 PageCache 就是这个作用;

实现零拷贝方案

直接内存访问(DMA)

DMA 是一种硬件特性,允许外设(如网络适配器、磁盘控制器等)直接访问系统内存,而无需通过 CPU 的介入。在数据传输时,DMA 可以直接将数据从内存传输到外设,或者从外设传输数据到内存,避免了数据在用户态和内核态之间的多次拷贝。

图片

DMA1

如上图所示,内核将数据读取的大部分数据读取操作都交个了 DMA 控制器,而空出来的资源就可以去处理其他的任务了。

sendfile

一些操作系统(例如 Linux)提供了特殊的系统调用,如 sendfile,在网络传输文件时实现零拷贝。通过 sendfile,应用程序可以直接将文件数据从文件系统传输到网络套接字或者目标文件,而无需经过用户缓冲区和内核缓冲区。

如果不用sendfile,如果将A文件写入B文件。

  1. 需要先将A文件的数据拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区;

  2. 然后内核再将用户缓冲区的数据拷贝到内核缓冲区,之后才能写入到B文件;

而用了sendfile,用户缓冲区和内核缓冲区的拷贝都不用了,节省了一大部分的开销。

共享内存

使用共享内存技术,应用程序和内核可以共享同一块内存区域,避免在用户态和内核态之间进行数据拷贝。应用程序可以直接将数据写入共享内存,然后内核可以直接从共享内存中读取数据进行传输,或者反之。

图片

通过共享一块儿内存区域,实现数据的共享。就像程序中的引用对象一样,实际上就是一个指针、一个地址。

内存映射文件(Memory-mapped Files)

内存映射文件直接将磁盘文件映射到应用程序的地址空间,使得应用程序可以直接在内存中读取和写入文件数据,这样一来,对映射内容的修改就是直接的反应到实际的文件中。

当文件数据需要传输时,内核可以直接从内存映射区域读取数据进行传输,避免了数据在用户态和内核态之间的额外拷贝。

虽然看上去感觉和共享内存没什么差别,但是两者的实现方式完全不同,一个是共享地址,一个是映射文件内容。

Java 实现零拷贝的方式

Java 标准的 IO 库是没有零拷贝方式的实现的,标准IO就相当于上面所说的传统模式。只是在 Java 推出的 NIO 中,才包含了一套新的 I/O 类,如 ByteBuffer 和 Channel,它们可以在一定程度上实现零拷贝。

ByteBuffer:可以直接操作字节数据,避免了数据在用户态和内核态之间的复制。

Channel:支持直接将数据从文件通道或网络通道传输到另一个通道,实现文件和网络的零拷贝传输。

借助这两种对象,结合 NIO 中的API,我们就能在 Java 中实现零拷贝了。

首先我们先用传统 IO 写一个方法,用来和后面的 NIO 作对比,这个程序的目的很简单,就是将一个100M左右的PDF文件从一个目录拷贝到另一个目录。

public static void ioCopy() {
  try {
    File sourceFile = new File(SOURCE_FILE_PATH);
    File targetFile = new File(TARGET_FILE_PATH);
    try (FileInputStream fis = new FileInputStream(sourceFile);
         FileOutputStream fos = new FileOutputStream(targetFile)) {
      byte[] buffer = new byte[1024];
      int bytesRead;
      while ((bytesRead = fis.read(buffer)) != -1) {
        fos.write(buffer, 0, bytesRead);
      }
    }
    System.out.println("传输 " + formatFileSize(sourceFile.length()) + " 字节到目标文件");
  } catch (IOException e) {
    e.printStackTrace();
  }
}

下面是这个拷贝程序的执行结果,109.92M,耗时1.29秒。

传输 109.92 M 字节到目标文件 耗时: 1.290 秒

FileChannel.transferTo() 和 transferFrom()

FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。

这两个方法首选用 sendfile 方式,只要当前操作系统支持,就用 sendfile,例如Linux或MacOS。如果系统不支持,例如windows,则采用内存映射文件的方式实现。

transferTo()

下面是一个 transferTo 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferTo() {
  try {
    File sourceFile = new File(SOURCE_FILE_PATH);
    File targetFile = new File(TARGET_FILE_PATH);
    try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
         FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
      long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

      System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
}

只耗时0.536秒,快了一倍。

传输 109.92 M 字节到目标文件 耗时: 0.536 秒

transferFrom()

下面是一个 transferFrom 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferFrom() {
  try {
    File sourceFile = new File(SOURCE_FILE_PATH);
    File targetFile = new File(TARGET_FILE_PATH);

    try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
         FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
      long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
      System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
}

执行时间:

传输 109.92 M 字节到目标文件 耗时: 0.603 秒

Memory-Mapped Files

Java 的 NIO 也支持内存映射文件(Memory-mapped Files),通过 FileChannel.map() 实现。

下面是一个 FileChannel.map()的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

    public static void nioMap(){
        try {
            File sourceFile = new File(SOURCE_FILE_PATH);
            File targetFile = new File(TARGET_FILE_PATH);

            try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
                 FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
                long fileSize = sourceChannel.size();
                MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
                targetChannel.write(buffer);
                System.out.println("传输 " + formatFileSize(fileSize) + " 字节到目标文件");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

执行时间:

传输 109.92 M 字节到目标文件 耗时: 0.663 秒

磁盘缓冲区是计算机内存中用于暂存从磁盘读取的数据或将数据写入磁盘之前的临时存储区域。它是一种优化磁盘 I/O 操作的机制,通过利用内存的快速访问速度,减少对慢速磁盘的频繁访问,提高数据读取和写入的性能和效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/811860.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【自动化测试】Jest体验之旅

目录 简介 快速体验 配置文件 Jest CLI 选项 --watchAll --watch 使用 ES6 模块 简介 Jest 是 Facebook 出品的一个 JavaScript 开源测试框架。相对其他测试框架,其一大特点就是就是内置了常用的测试工具,比如零配置、自带断言、测试覆盖率工具等…

从二叉搜索树到红黑树

二叉搜索树(Binary Search Tree) 特性 任意一个节点的所有左子树都比它小,所有右子树都比它大; 复杂度 当我们查找某个节点的时候,先从根节点开始比较,如果比根节点大走右子树,如果比根节点…

码农该如何延长周末体验感

码农该如何延长周末体验感 码农该如何延长周末体验感 码农该如何延长周末体验感1.制定合理的工作计划:2.实践工作与生活的平衡:3.学习新技术或扩展知识领域4.参与开源项目或个人项目:5.与同事或朋友组织活动:6.自己写博客或者总结…

jeecgboot新建moudle,但是访问404,需要在open moudle setting 里面设置

jeecgboot新建moudle&#xff0c;但是访问404&#xff0c;需要在open moudle setting 里面设置 首先需要确定以下3个pom.xml 最最外层的pom.xml 最最外层的pom.xml <modules><module>jeecg-boot-base-core</module><module>jeecg-module-demo</m…

string类(使用+实现)(C++)

string string类“登场”string类 - 了解string类的常用接口常见构造容量操作访问及遍历操作迭代器分类作用 增删查改操作非成员函数 string类的实现string类重要的方法实现分析介绍构造函数拷贝构造函数赋值运算符重载总结 string类整体实现代码 写时拷贝&#xff08;了解&…

金三银四好像消失了,IT行业何时复苏!

疫情时候不敢离职&#xff0c;以为熬过来疫情了&#xff0c;行情会好一些&#xff0c;可是疫情结束了&#xff0c;反而行情更差了&#xff0c; 这是要哪样 我心中不由一万个 草泥&#x1f434; 路过 我心中不惊有了很多疑惑和感叹&#xff01; 自我10连问 我的心情 自去年下…

什么是合者生情?

有人说&#xff0c;自己的命运自己掌握&#xff0c;但必须以预知自己命运如何为前提。 若不知道自己的命运如何&#xff0c;却要掌握自己的命运&#xff0c;那只是一句空话&#xff0c;是自欺欺人。 而易学在我国历史上&#xff0c;直至现在&#xff0c;都起到了重大而深远的作…

Python入门【LEGB规则、面向对象简介、面向过程和面向对象思想、面向对象是什么? 对象的进化 、类的定义、对象完整内存结构 】(十三)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小王&#xff0c;CSDN博客博主,Python小白 &#x1f4d5;系列专栏&#xff1a;python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 &#x1f4e7;如果文章知识点有错误…

ARM裸机-5

1、可编程器件的编程原理 1.1、电子器件的发展方向 模拟器件-->数字器件 ASIC-->可编程器件 1.2、可编程器件的特点 CPU在固定频率的时钟控制下节奏运行。 CPU可以通过总线读取外部存储设备中的二进制指令集&#xff0c;然后解码执行。 这些可以被CPU解码执行的二进制指…

SpringBoot临时属性设置

在Spring Boot中&#xff0c;可以通过设置临时属性来覆盖应用程序中定义的属性。这在某些情况下很有用&#xff0c;例如在命令行中指定配置参数或在测试环境中覆盖默认值。 你可以使用--&#xff08;双破折号&#xff09;语法来设置临时属性。以下是一些示例&#xff1a; 1. …

【点云处理教程】04 Python 中的点云过滤

一、说明 这是我的“点云处理”教程的第 4 篇文章。“点云处理”教程对初学者友好&#xff0c;我们将在其中简单地介绍从数据准备到数据分割和分类的点云处理管道。 在本教程中&#xff0c;我们将学习如何使用 Open3D 在 python 中过滤点云以进行下采样和异常值去除。使用 Open…

【GitOps系列】在 GitOps 工作流中实现蓝绿发布

文章目录 前言蓝绿发布概述手动实现蓝绿发布创建蓝色环境创建蓝色环境 Ingressroute部署绿色环境切换到绿色环境 蓝绿发布自动化安装 Argo Rollout创建 Rollout 对象创建 Service 和 Ingress访问蓝色环境发布自动化 访问 Argo Rollout Dashboard自动化原理结语 前言 在前几篇【…

Netty学习(四)

文章目录 四. 优化与源码1. 优化1.1 扩展序列化算法jdk序列化与反序列化Serializer & AlgorithmConfigapplication.properties MessageCodecSharableMessage&#xff08;抽象类&#xff09; 测试序列化测试反序列化测试 1.2 参数调优1&#xff09;CONNECT_TIMEOUT_MILLIS2&…

最强,自动化测试-自定义日志类及日志封装(实战)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 在自定义日志之前…

【机器学习】习题3.3Python编程实现对数几率回归

参考代码 结合自己的理解&#xff0c;添加注释。 代码 导入相关的库 import numpy as np import pandas as pd import matplotlib from matplotlib import pyplot as plt from sklearn import linear_model导入数据&#xff0c;进行数据处理和特征工程 # 1.数据处理&#x…

Windows系统如何修改文件日期属性

winr键&#xff0c;输入powershell,在弹出的命令窗口输入命令&#xff0c;案例如下&#xff1a; file_address E:\_OrderingProject\\PIC1101\ldv1s_0830_ec_result.tiftime_change "07/12/2022 20:42:23" 修改文件创建时间&#xff1a;creationtime $(Get-Item fi…

COMSOL三维Voronoi图泰森多边形3D模型轴压模拟及建模教程

多晶体模型采用三维Voronoi算法生成&#xff0c;试件尺寸为150150300mm棱柱模型&#xff0c;对晶格指定五种不同材料&#xff0c;实现晶格间的差异性。 对试件进行力学模拟&#xff0c;下侧为固定边界&#xff0c;限制z方向的位移&#xff0c;上表面通过给定位移的方式实现轴…

P2P网络NAT穿透原理(打洞方案)

1.关于NAT NAT技术&#xff08;Network Address Translation&#xff0c;网络地址转换&#xff09;是一种把内部网络&#xff08;简称为内网&#xff09;私有IP地址转换为外部网络&#xff08;简称为外网&#xff09;公共IP地址的技术&#xff0c;它使得一定范围内的多台主机只…

某拍房数据采集

某拍房数据采集 某拍房数据采集声明1.逆向目标2.寻找加密位置3.分析加密参数4.python代码书写 某拍房数据采集 声明 本文章中所有内容仅供学习交流&#xff0c;抓包内容、敏感网址、数据接口均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的…

yo!这里是Linux常见命令总结

目录 前言 常见命令 ls指令 pwd指令 cd指令 touch指令 tree指令 mkdir指令&&rmdir指令 rm指令 man指令 cp指令 mv指令 echo指令 cat指令&&tac指令 more指令 less指令 head指令&&tail指令 find指令 grep指令 alias指令&&u…