物理内存

计算机物理内存条的容量,比如我们买电脑会关注内存大小有多少G,这个容量就是计算机的物理内存。

虚拟内存

操作系统为每个进程分配了独立的虚拟地址空间,也就是虚拟内存,虚拟地址空间又分为用户空间和内核空间,操作系统的位数不同,虚拟地址空间的大小也不同,32位操作系统虚拟地址内核空间为1G,用户空间大小为3G,64位操作系统用户空间和内核空间大小各为128T:

既然每个进程都拥有一块独立的虚拟地址空间,那么所有进程的虚拟地址空间大小加起来必定大于物理内存的大小,所以虚拟地址空间只是一个虚拟的概念,只有需要分配内存的时候才会为虚拟内存分配物理内存,并通过内存映射来管理虚拟地址和物理内存地址之间的映射关系。

用户空间 / 内核空间

用户空间:是运行用户程序代码的地方,为了保证系统内核的安全,它不能直接访问内存等硬件设备,必须通过系统调用进入到内核空间来访问那些受限的资源。

内核空间:是运行内核代码的地方,可以执行任意的指令访问系统资源,既可以访问内核空间也可以访问用户空间。

用户态:进程运行在用户空间时处于用户态。

内核态:进程运行在内核空间时处于内核态。

文件I/O

文件I/O与读写文件有关,比如我们启动了一个程序,此时运行在用户空间(用户态),接着准备做一个读取磁盘文件的操作,由于用户空间是无法直接从磁盘读取文件的,所以需要调用内核提供的接口来完成文件的读取,调用内核的接口的过程中由用户空间进入到了内核空间(内核态),DMA从磁盘读取文件到内核的缓冲区,之后再将数据从内核的缓冲区拷贝到用户空间完成文件的读取操作:

  1. 应用程序调用read函数发起系统调用,此时由用户空间切换到内核空间;
  2. 内核通过DMA从磁盘拷贝数据到内核缓冲区(DMA复制);
  3. 将内核缓冲区的数据拷贝到用户空间的缓冲区(CPU复制),切换回用户空间;

可以发现,整个读取过程发生了两次数据拷贝,一次是DMA将磁盘上的文件数据拷贝到内核缓冲区,一次是将内核缓冲区的数据拷贝到用户缓冲区。写操作与读取操作类似,只不过是将用户缓冲区的数据拷贝到内核缓冲区,再将内核缓冲区的数据拷贝到文件。

文件I/O从操作系统的角度来看还可以划分为缓存I/O、直接I/O和mmap内存映射。

缓存I/O

也称标准I/O,上面提到的文件I/O读取数据的例子就是使用的缓存I/O,它需要将数据先拷贝到内核缓冲区,再将内核缓冲区的数据拷贝到用户缓冲区,数据经过两次拷贝,内核缓冲区和用户缓冲区分别指向不同的物理内存,在文件I/O中,内核缓冲区是在Page Cache层,这也是称为缓存I/O的原因

JAVA中通过java.io包下进行读写文件使用的就是缓存I/O。

为什么需要缓存IO?

因为磁盘I/O是比较耗时的操作,如果每次都从磁盘上读取文件,性能将会大大下降,为了提升读取性能,增加了一层Page Cache,用于缓存读取的文件数据,Page Cache占用的是内存,从内存读取的速度远远大于从磁盘读取,内核缓冲区就是在Page Cache中开辟的一块内存,用户空间进行系统调用读取文件内容时,首先会判断Page Cache中是否缓存了文件的内容,如果缓存了直接读取即可,否则再从磁盘读取,所以缓存I/O可以减少磁盘I/O的次数提升性能。

文件的写操作同样如此,进行写操作时,将数据先写到Page Cache的缓冲区中,后续由操作系统将数据刷回到磁盘中。

缓存I/O的优缺点

优点:减少磁盘I/O次数,提升读写性能。

缺点:数据需要在内核空间和用户空间来回拷贝。

DirectByteBuffer

使用缓存I/O读取数据时,数据会经过两次拷贝,经过两次拷贝是从系统调用开始讲起,在JAVA中由于涉及到JVM堆内和堆外内存,如果使用java.io下的类进行文件读写实际上还会再多一次拷贝(详细可参考【JAVA】普通IO数据拷贝次数的问题探讨):

  1. 底层发起JNI调用,创建堆外缓冲区;
  2. JNI中发起read系统调用,此时需要由用户空间切换到内核空间;
  3. 进入到内核空间,DMA读取文件数据到内核缓冲区(DMA拷贝);
  4. 将内核缓冲区的数据拷贝到用户缓冲区(CPU拷贝),切换回用户空间;
  5. 将堆外缓冲区的数据拷贝到JVM堆内缓冲区中(CPU拷贝);

在Java的NIO中,提供了DirectByteBuffer,可以直接分配堆外内存,减少了一次从堆外内存到堆内内存的复制(CPU复制)

直接I/O

缓存I/O经过了Page Cache,读取过程中需要将数据从Page Cache的缓冲区中拷贝到用户空间的缓存区,那么有没有一种方式可以省去这个拷贝的过程?

答案是有的,那就是直接I/O,应用程序直接访问磁盘数据,绕过了Page Cache,省去了从内核缓冲区拷贝到用户缓冲区的过程:

目前JAVA并没有原生的直接/O操作方式,不过公众号博主Kirito提供了在JAVA中进行直接I/O操作的方法,具体参见【Kirito的技术分享】Java 文件 IO 操作之 DirectIO

内存映射

内存映射就是将虚拟空间地址映射到物理空间地址,每个进程维护了一张页表,记录虚拟地址和物理地址之间的映射关系,当进程访问的虚拟地址在页表中无法查到映射关系时,系统产生缺页异常,进入内核空间为虚拟地址分配物理内存,并更新页表,记录映射关系。

文件映射

内存映射除了映射虚拟空间地址和物理空间地址,还包括将磁盘的文件内容映射到虚拟地址空间,称为文件映射,此时可以通过访问内存来访问文件里面的数据 。

mmap系统调用可以将文件映射到虚拟内存空间。文件映射的流程如下:

  1. 进行mmap系统调用,将文件和虚拟地址空间建立映射,注意此时还没有分配物理内存空间,只是在逻辑上建立了虚拟地址和文件之间的映射关系,物理内存只有真正使用的时候才会分配。
  2. 应用程序访问用户空间虚拟内存中的某个地址,发现无法在页表中查到数据,产生缺页异常,此时进入内核空间
  3. 因为不能直接使用物理地址,所以需要使用内核的虚拟地址临时建立与物理内存的映射关系,将文件内容读取到物理内存中,待数据读取完毕之后取消临时映射即可。
  4. 缺页异常处理完毕,物理内存中已经加载了文件的数据,此时用户空间就可以通过虚拟地址直接访问物理内存中映射的文件数据。

从文件映射的流程中可以看出它与缓存I/O相比,少了从内核缓冲区将数据拷贝到用户缓冲区的步骤,减少了一次拷贝。

Java NIO中提供了MappedByteBuffer来处理文件映射,下面是一个读取文件的例子:

public class MappedByteBufferTest {

    public static void main(String[] args) {

        try (RandomAccessFile file = new RandomAccessFile(new File("/Users/sml/test.txt"), "r")) {
// 获取FileChannel
FileChannel fileChannel = file.getChannel();
long size = fileChannel.size();
// 调用map方法进行文件映射,返回MappedByteBuffer
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
byte[] bytes = new byte[(int)size];
for (int i = 0; i < size; i++) {
// 读取数据
bytes[i] = mappedByteBuffer.get();
}
} catch (Exception e) {
e.printStackTrace();
} }
}

零拷贝

零拷贝一般指的是从磁盘读取文件发送到网络或者从网络接收数据写入到磁盘文件的过程中,减少数据的拷贝次数。

网络I/O

网络I/O与网络数据发送/接收有关,与文件I/O的底层原理一致,同样以读取数据为例,文件I/O是从磁盘读取文件,网络I/O是从网卡中读取数据。比如客户端与服务端建立了一个连接,客户端向服务端发送数据,服务端从网卡中读取客户端发送的数据到内核中的socket缓冲区,再将socket缓冲区的数据复制到用户空间的缓冲区:

使用缓存I/O发送数据到网络

首先看一下使用缓存I/O从磁盘文件读取数据并发送到网络上的过程:

  1. 用户发起系统调用,进入到内核态,DMA从磁盘上读取数据到内核缓冲区(DMA复制);
  2. CPU将内核缓冲区的数据拷贝到用户缓冲区(CPU复制),切换回到用户空间;
  3. 再次从用户空间切换到内核空间,CPU将用户缓冲区的数据拷贝到socket缓冲区(CPU复制);
  4. DMA将socket缓冲区的数据拷贝到网卡(DMA复制),之后从内核空间切换回用户空间;

使用缓存I/O数据经过了四次拷贝,需要多次在内核空间和用户空间来回切换,影响系统性能。从数据拷贝的过程可以看到有些步骤其实是多余的,比如第二步,如果可以直接将内核缓存区的数据拷贝到socket缓冲区,或者直接将内核缓冲区的数据拷贝到网卡,岂不是减少了数据拷贝的次数?零拷贝就是这样一种致力于减少数据拷贝的技术。

Linux中的零拷贝

sendfile

Linux在2.1版本中引入了sendfile函数,可以实现将数据从一个文件描述符传输到另外一个文件描述符:

  1. 发起sendfile系统调用,进入到内核空间;
  2. DMA从磁盘读取文件到内核缓冲区(DMA复制);
  3. 将内核缓冲区数据拷贝到socket缓冲区(CPU复制);
  4. 将socket缓冲区数据拷贝到网卡(DMA复制),之后切换回用户空间;

sendfile减少了一次数据从内核缓冲区拷贝到用户缓冲区的过程,可以直接将内核缓冲区的数据拷贝到socket缓冲区。

sendfile + DMA GATHER

Linux在2.4版本中引入了gather技术,我们知道内核缓冲区在内存中有对应的地址,gather操作可以将内核缓冲区的内存地址、地址偏移量信息记录到socket缓冲区中,之后DMA根据地址信息从内存中读取数据到网卡中,减少了数据从内核缓冲区到socket缓冲区的拷贝过程:

可以看到零拷贝并不是指的数据一次拷贝都没有发生,而是指减少CPU进行数据拷贝的次数。

Java中的零拷贝

MappedByteBuffer

在内存映射中说过,可以通过文件映射的方式将磁盘的文件内容映射到虚拟地址空间,用户空间就可以通过虚拟地址直接访问物理内存中的映射的文件数据,而Java NIO中也提供了MappedByteBuffer来处理文件映射,使用MappedByteBuffer向网络中发送数据的过程如下:

  1. 使用MappedByteBuffer建立文件映射,用户空间可以通过虚拟地址直接访问映射的文件数据;

  2. 将映射的文件数据拷贝到socket网络缓冲区(CPU复制);

  3. DMA将socket缓冲区的数据拷贝到网卡(DMA复制);

MappedByteBuffer减少了从内核缓冲区到用户缓冲区的数据拷贝,可以直接将内核缓冲区的数据拷贝到网络缓冲区。

FileChannel

Java NIO中的FileChannel可以实现将数据从FileChannel直接传输到另一个Channel,它是sendfile的一种实现:

            RandomAccessFile file = new RandomAccessFile(new File("/Users/sml/test.txt"), "r");
// 获取FileChannel
FileChannel fileChannel = file.getChannel();
long size = fileChannel.size();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
fileChannel.transferTo(0,size,socketChannel);

参考

【极客时间-倪朋飞】Linux性能优化实战

【极客时间-刘超】趣谈Linux操作系统

【拉勾教育-若地】Netty 核心原理剖析与 RPC 实践

【 Kirito的技术分享】文件IO操作的最佳实践

【小码农叔叔】java使用nio读写文件

【占小狼】深入浅出MappedByteBuffer

【零壹技术栈】深入剖析Linux IO原理和几种零拷贝机制的实现

【tomas家的小拨浪鼓】堆外内存 之 DirectByteBuffer 详解

网络IO和磁盘IO详解

【Java】Java中的零拷贝的更多相关文章

  1. Java 开发中的对象拷贝

    前言 在 Java 开发中,很多时候需要将两个属性基本相同的对象进行属性复制,比如 DO 转 VO等等. 本文主要介绍自己实现的简易拷贝工具类与 Spring 提供的属性拷贝的对比. Spring 提 ...

  2. Linux 中的零拷贝技术,第 2 部分

    技术实现 本系列由两篇文章组成,介绍了当前用于 Linux 操作系统上的几种零拷贝技术,简单描述了各种零拷贝技术的实现,以及它们的特点和适用场景.第一部分主要介绍了一些零拷贝技术的相关背景知识,简要概 ...

  3. Linux 中的零拷贝技术,第 1 部分

    概述 本系列由两篇文章组成,介绍了当前用于 Linux 操作系统上的几种零拷贝技术,简单描述了各种零拷贝技术的实现,以及它们的特点和适用场景.本文是本系列文章的第一部分,主要是介绍一些零拷贝技术的相关 ...

  4. 【转】浅析Linux中的零拷贝技术

    本文探讨Linux中主要的几种零拷贝技术以及零拷贝技术适用的场景.为了迅速建立起零拷贝的概念,我们拿一个常用的场景进行引入: 引文## 在写一个服务端程序时(Web Server或者文件服务器),文件 ...

  5. [转帖]Linux 中的零拷贝技术,第 2 部分

    Linux 中的零拷贝技术,第 2 部分 https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/index.html   Linux 中 ...

  6. [转帖]Linux 中的零拷贝技术,第 1 部分

    Linux 中的零拷贝技术,第 1 部分 https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy1/index.html   引言 传统的 ...

  7. Linux中的零拷贝

    零拷贝 本文图片和一些内容均来自后面的参考,非原创只是把文章中的一些关键内容整理一下,算作是一个学习笔记. 传统的I/O操作 传统的IO操作是用户应用程序只是需要调用两个系统调用 read() 和 w ...

  8. 理解Netty中的零拷贝(Zero-Copy)机制【转】

    理解零拷贝 零拷贝是Netty的重要特性之一,而究竟什么是零拷贝呢? WIKI中对其有如下定义: “Zero-copy” describes computer operations in which ...

  9. Netty:Netty中的零拷贝(Zero Copy)

    零复制概念: " 零复制"描述了计算机操作,其中CPU不执行将数据从一个存储区复制到另一个存储区的任务.通过网络传输文件时,通常用于节省CPU周期和内存带宽. WIKI的定义中,我 ...

随机推荐

  1. C++ 练气期之函数探幽

    1. 函数基础 一个C++程序中,往往需要包含若干个函数,可以说函数是C++程序的基础组成元件,是程序中的头等公民. 如果要理解程序中使用函数的具体意义,则需要了解语言发展过程中致力要解决的 2 问题 ...

  2. Redis 15 主从复制

    参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 概述 主从复制 ...

  3. 【漏洞分析】KaoyaSwap 安全事件分析

    相关信息 KaoyaSwap 是 BSC 链上的一个自动做市商 AMM.然后,现在他们的官网 https://www.kaoyaswap.com/ 已经打不开了(如果我打开方式没错的话).所以就直接进 ...

  4. KingbaseES通过sys_waldump解析wal日志

    前言 oracle中的redo日志我们无法直接读取,然而对于KingbaseES数据库,我们可以利用sys_waldump工具解析wal日志,查看wal日志记录的信息. 我们可以利用 sys_wald ...

  5. 硬核解析MySQL的MVCC实现原理,面试官看了都直呼内行

    1. 什么是MVCC MVCC全称是Multi-Version Concurrency Control(多版本并发控制),是一种并发控制的方法,通过维护一个数据的多个版本,减少读写操作的冲突. 如果没 ...

  6. android 逆向 smali手写helloworld

    编写Hello.smali文件 .class public LHelloWorld; .super Ljava/lang/Object; .method public static main([Lja ...

  7. Taurus.MVC-Java 版本打包上传到Maven中央仓库(详细过程):2、PGP下载安装与密钥生成发布

    文章目录: Taurus.MVC-Java 版本打包上传到Maven中央仓库(详细过程):1.JIRA账号注册 Taurus.MVC-Java 版本打包上传到Maven中央仓库(详细过程):2.PGP ...

  8. 【设计模式】Java设计模式 - 适配器模式

    [设计模式]Java设计模式 - 适配器模式 不断学习才是王道 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 原创作品,更多关注我CSDN: 一个有梦有戏的人 准备将博客园.CSDN一 ...

  9. 引擎之旅 Chapter.2 线程库

    预备知识可参考我整理的博客 Windows编程之线程:https://www.cnblogs.com/ZhuSenlin/p/16662075.html Windows编程之线程同步:https:// ...

  10. SpringMVC--从理解SpringMVC执行流程到SSM框架整合

    前言 SpringMVC框架是SSM框架中继Spring另一个重要的框架,那么什么是SpringMVC,如何用SpringMVC来整合SSM框架呢?下面让我们详细的了解一下. 注:在学习SpringM ...