网站建设方案产业,简介常用的网页制作工具,wordpress模板 国内,百度站长工具seo综合查询我们总会在各种地方看到零拷贝#xff0c;那零拷贝到底是个什么东西。
接下来#xff0c;让我们来理一理啊。
拷贝说的是计算机里的 I/O 操作#xff0c;也就是数据的读写操作。计算机可是一个复杂的家伙#xff0c;包括软件和硬件两大部分#xff0c;软件主要指操作系统…我们总会在各种地方看到零拷贝那零拷贝到底是个什么东西。
接下来让我们来理一理啊。
拷贝说的是计算机里的 I/O 操作也就是数据的读写操作。计算机可是一个复杂的家伙包括软件和硬件两大部分软件主要指操作系统、驱动程序和应用程序。硬件那就多了CPU、内存、硬盘等等一大堆东西。
这么复杂的设备要进行读写操作其中繁琐和复杂程度可想而知。
传统I/O的读写过程
如果要了解零拷贝那就必须要知道一般情况下计算机是如何读写数据的我把这种情况称为传统 I/O。
数据读写的发起者是计算机中的应用程序比如我们常用的浏览器、办公软件、音视频软件等。
而数据的来源呢一般是硬盘、外部存储设备或者是网络套接字也就是网络上的数据通过网口网卡的处理。
过程本来是很复杂的所以大学课程里要通过《操作系统》、《计算机组成原理》来专门讲计算机的软硬件。
简化版读操作流程
那么细的没办法讲来所以我们把这个读写过程简化一下忽略大多数细节只讲流程。 上图是应用程序进行一次读操作的过程。 应用程序先发起读操作准备读取数据了 内核将数据从硬盘或外部存储读取到内核缓冲区 内核将数据从内核缓冲区拷贝到用户缓冲区 应用程序读取用户缓冲区的数据进行处理加工
详细的读写操作流程
下面是一个更详细的 I/O 读写过程。这个图可好用极了我会借助这个图来厘清 I/O 操作的一些基础但非常重要的概念。 先看一下这个图上面红粉色部分是读操作下面蓝色部分是写操作。
如果一下子看着有点儿迷糊的话没关系看看下面几个概念就清楚了。
应用程序
就是安装在操作系统上的各种应用。
系统内核
系统内核是一些列计算机的核心资源的集合不仅包括CPU、总线这些硬件设备也包括进程管理、文件管理、内存管理、设备驱动、系统调用等一些列功能。
外部存储
外部存储就是指硬盘、U盘等外部存储介质。
内核态 内核态是操作系统内核运行的模式当操作系统内核执行特权指令时处于内核态。 在内核态下操作系统内核拥有最高权限可以访问计算机的所有硬件资源和敏感数据执行特权指令控制系统的整体运行。 内核态提供了操作系统管理和控制计算机硬件的能力它负责处理系统调用、中断、硬件异常等核心任务。
用户态
这里的用户可以理解为应用程序这个用户是对于计算机的内核而言的对于内核来说系统上的各种应用程序会发出指令来调用内核的资源这时候应用程序就是内核的用户。 用户态是应用程序运行的模式当应用程序执行普通的指令时处于用户态。 在用户态下应用程序只能访问自己的内存空间和受限的硬件资源无法直接访问操作系统的敏感数据或控制计算机的硬件设备。 用户态提供了一种安全的运行环境确保应用程序之间相互隔离防止恶意程序对系统造成影响。
模式切换
计算机为了安全性考虑区分了内核态和用户态应用程序不能直接调用内核资源必须要切换到内核态之后让内核来调用内核调用完资源再返回给应用程序这个时候系统在切换会用户态应用程序在用户态下才能处理数据。
上述过程其实一次读和一次写都分别发生了两次模式切换。 内核缓冲区
内核缓冲区指内存中专门用来给内核直接使用的内存空间。可以把它理解为应用程序和外部存储进行数据交互的一个中间介质。
应用程序想要读外部数据要从这里读。应用程序想要写入外部存储要通过内核缓冲区。
用户缓冲区
用户缓冲区可以理解为应用程序可以直接读写的内存空间。因为应用程序没法直接到内核读写数据 所以应用程序想要处理数据必须先通过用户缓冲区。
磁盘缓冲区
PageCache PageCache 是 Linux 内核对文件系统进行缓存的一种机制。它使用空闲内存来缓存从文件系统读取的数据块加速文件的读取和写入操作。 当应用程序或进程读取文件时数据会首先从文件系统读取到 PageCache 中。如果之后再次读取相同的数据就可以直接从 PageCache 中获取避免了再次访问文件系统。 同样当应用程序或进程将数据写入文件时数据会先暂存到 PageCache 中然后由 Linux 内核异步地将数据写入磁盘从而提高写入操作的效率。
再说数据读写操作流程
上面弄明白了这几个概念后再回过头看一下那个流程图是不是就清楚多了。
读操作 首先应用程序向内核发起读请求这时候进行一次模式切换了从用户态切换到内核态 内核向外部存储或网络套接字发起读操作 将数据写入磁盘缓冲区 系统内核将数据从磁盘缓冲区拷贝到内核缓冲区顺便再将一份或者一部分拷贝到 PageCache 内核将数据拷贝到用户缓冲区供应用程序处理。此时又进行一次模态切换从内核态切换回用户态
写操作 应用程序向内核发起写请求这时候进行一次模式切换了从用户态切换到内核态 内核将要写入的数据从用户缓冲区拷贝到 PageCache同时将数据拷贝到内核缓冲区 然后内核将数据写入到磁盘缓冲区从而写入磁盘或者直接写入网络套接字。
瓶颈在哪里
但是传统I/O有它的瓶颈这才是零拷贝技术出现的缘由。瓶颈是啥呢当然是性能问题太慢了。尤其是在高并发场景下I/O性能经常会卡脖子。
那是什么地方耗时了呢
数据拷贝
在传统 I/O 中数据的传输通常涉及多次数据拷贝。数据需要从应用程序的用户缓冲区复制到内核缓冲区然后再从内核缓冲区复制到设备或网络缓冲区。这些数据拷贝过程导致了多次内存访问和数据复制消耗了大量的 CPU 时间和内存带宽。
用户态和内核态的切换
由于数据要经过内核缓冲区导致数据在用户态和内核态之间来回切换切换过程中会有上下文的切换如此一来大大增加了处理数据的复杂性和时间开销。
每一次操作耗费的时间虽然很小但是当并发量高了以后积少成多也是不小的开销。所以要提高性能、减少开销就要从以上两个问题下手了。
这时候零拷贝技术就出来解决问题了。
什么是零拷贝
问题出来数据拷贝和模态切换上。
但既然是 I/O 操作不可能没有数据拷贝的只能减少拷贝的次数还有就是尽量将数据存储在离应用程序用户缓冲区更近的地方。
而区分用户态和内核态有其他更重要的原因不可能单纯为了 I/O 效率就改变这种设计吧。那也只能尽量减少切换的次数。
零拷贝的理想状态就是操作数据不用拷贝但是显示情况下并不一定真的就是一次复制操作都没有而是尽量减少拷贝操作的次数。
要实现零拷贝应该从下面这三个方面入手 尽量减少数据在各个存储区域的复制操作例如从磁盘缓冲区到内核缓冲区等 尽量减少用户态和内核态的切换次数及上下文切换 使用一些优化手段例如对需要操作的数据先缓存起来内核中的 PageCache 就是这个作用
实现零拷贝方案
直接内存访问DMA
DMA 是一种硬件特性允许外设如网络适配器、磁盘控制器等直接访问系统内存而无需通过 CPU 的介入。在数据传输时DMA 可以直接将数据从内存传输到外设或者从外设传输数据到内存避免了数据在用户态和内核态之间的多次拷贝。 DMA1
如上图所示内核将数据读取的大部分数据读取操作都交个了 DMA 控制器而空出来的资源就可以去处理其他的任务了。
sendfile
一些操作系统例如 Linux提供了特殊的系统调用如 sendfile在网络传输文件时实现零拷贝。通过 sendfile应用程序可以直接将文件数据从文件系统传输到网络套接字或者目标文件而无需经过用户缓冲区和内核缓冲区。
如果不用sendfile如果将A文件写入B文件。 需要先将A文件的数据拷贝到内核缓冲区再从内核缓冲区拷贝到用户缓冲区 然后内核再将用户缓冲区的数据拷贝到内核缓冲区之后才能写入到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 操作的机制通过利用内存的快速访问速度减少对慢速磁盘的频繁访问提高数据读取和写入的性能和效率。