NIO进阶篇:Page Cache、零拷贝、顺序读写、堆外内存

本文详细介绍了NIO中的关键概念,包括DMA原理、Page Cache的作用及其与文件系统的关系、零拷贝的实现方式如mmap和sendfile,以及顺序读写的优势。此外,还探讨了堆外内存(Direct ByteBuffer)的回收机制,强调了其在大内存分配和减少GC影响中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. DMA

在学习零拷贝等NIO技术之前,我们需要先知道什么是DMA。DMA(Direct Memory Access,直接存储器访问)。在DMA出现之前,CPU与外设之间的数据传送方式有程序传送方式、中断传送方式。CPU是通过系统总线与其他部件连接并进行数据传输。不管何种传送方式,都要消耗CPU,间接影响了其他任务的执行。

1.1 DMA原理

DMA的出现就是为了解决批量数据的输入/输出问题。DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。类比显卡,也是从CPU中剥离出来的功能。将这些特殊的模块进行剥离,使得CPU可以更加专注于计算工作。
通常系统总线是由CPU管理的,在DMA方式时,就希望CPU把这些总线让出来而由DMA控制器接管,控制传送的字节数,判断DMA是否结束,以及发出DMA结束信号。因此DMA控制器必须有以下功能:

1、能向CPU发出系统保持(HOLD)信号,提出总线接管请求;
2、当CPU发出允许接管信号后,对总线的控制由DMA接管;
3、能对存储器寻址及能修改地址指针,实现对内存的读写;
4、能决定本次DMA传送的字节数,判断DMA传送是否借宿。
5、发出DMA结束信号,使CPU恢复正常工作状态。

在这里插入图片描述

图一

在这里插入图片描述

图二

2. Page Cache

2.1 文件

从应用程序的角度看,操作系统提供了一个统一的虚拟机,在该虚拟机中没有各种机器的具体细节,只有进程、文件、地址空间以及进程间通信等逻辑概念。这种抽象虚拟机使得应用程序的开发变得相对容易。对于存储设备上的数据,操作系统向应用程序提供的逻辑概念就是"文件"。应用程序要存储或访问数据时,只需读或者写"文件"的一维地址空间即可,而这个地址空间与存储设备上存储块之间的对应关系则由操作系统维护。说白了,文件就是基于内核态Page Cache的一层抽象,下文有详细介绍。

2.2 Page Cache的作用

在这里插入图片描述

图三

图中描述了 Linux 操作系统中文件 Cache 管理与内存管理以及文件系统的关系示意图。从图中可以看到,在 Linux 中,具体文件系统,如 ext2/ext3、jfs、ntfs 等,负责在文件 Cache和存储设备之间交换数据,位于具体文件系统之上的虚拟文件系统VFS负责在应用程序和文件 Cache 之间通过 read/write 等接口交换数据,而内存管理系统负责文件 Cache 的分配和回收,同时虚拟内存管理系统(VMM)则允许应用程序和文件 Cache 之间通过 memory map的方式交换数据。可见,在 Linux 系统中,文件 Cache 是内存管理系统、文件系统以及应用程序之间的一个联系枢纽。

2.3 Page Cache相关的数据结构

每一个 Page Cache 包含若干 Buffer Cache。

1、内存管理系统与Page Cache交互,负责维护每项 Page Cache 的分配和回收,同时在使用 memory map 方式访问时负责建立映射;
2、VFS 与Page Cache交互,负责 Page Cache 与用户空间的数据交换,即文件读写;
3、具体文件系统则一般只与 Buffer Cache 交互,它们负责在外围存储设备和 Buffer Cache 之间交换数据。

Page Cache、Buffer Cache、文件以及磁盘之间的关系如图所示
在这里插入图片描述

图四

由图四中可见,一个Page Cache包含多个Buffer Cache,一个Buffer Cache与一个磁盘块一一对应。

在这里插入图片描述

图五

假定了 Page 的大小是 4K,则文件的每个4K的数据块最多只能对应一个 Page Cache 项,它通过一个是 radix tree来管理文件块和page cache的映射关系,Radix tree 是一种搜索树,Linux 内核利用这个数据结构来通过文件内偏移快速定位 Cache 项。

3. 零拷贝

Linux内核中与Page Cache操作相关的API有很多,按其使用方式可以分成两类:一类是以拷贝方式操作的相关接口, 如read/write/sendfile等;另一类是以地址映射方式操作的相关接口,如mmap。其中sendfile和mmap都是零拷贝的实现方案。

我们经常听说Kafka和RocketMQ等消息中间件有利用零拷贝技术来加速数据处理,提高吞吐量。所谓零拷贝,就是用户态与内核态的数据拷贝的次数为零

3.1 常规文件读写

我们先看下正常文件读写所经历的阶段,即FileChannel#readFileChannel#write,共涉及四次上下文切换(内核态和用户态的切换,包括read调用,read返回,write调用,write返回)和四次数据拷贝
在这里插入图片描述

图六

在这里插入图片描述

图七

JAVA中关于正常文件读写的示例代码:

    @Test
    public void testChannel() throws IOException {
   
   
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        FileChannel inputChannel = null;
        FileChannel outputChannel = null;
        try {
   
   
            inputStream = new FileInputStream(new File("/Users/djg/Downloads/branch.jpg"));
            outputStream = new FileOutputStream(new File("/Users/djg/Downloads/branch2.jpg"));

            // 获取通道
            inputChannel = inputStream.getChannel();
            outputChannel = outputStream.getChannel();

            // 分配缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

            // 将通道中数据存入缓冲区
            while(inputChannel.read(byteBuffer) != -1){
   
   
                // 切换成读取数据的模式
                byteBuffer.flip();
                //缓冲区中数据写到通道中区
                outputChannel.write(byteBuffer);
                // 清空缓冲区
                byteBuffer.clear();
            }
            System.out.println("读写成功");
        } catch (FileNotFoundException e) {
   
   
            e.printStackTrace();
        } catch (IOException e) {
   
   
            e.printStackTrace();
        } finally {
   
   
            inputChannel.close();
            outputChannel.close();
            System.out.println("数据关闭成功");
        }
    }
3.2 mmap

mmap 把文件映射到用户空间里的虚拟地址空间,实现文件和进程虚拟地址空间中一段虚拟地址的一一对映关系。

省去了从内核缓冲区复制到用户空间的过程,进程就可以采用指针的方式读写操作这一段内存(文件 / page cache),而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作。相反,内核空间对这段区域的修改也直接反映到用户空间,从而可以实现用户态和内核态对此内存区域的共享。

但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作,只有真正使用这些数据时,虚拟内存管理系统 VMS 才根据缺页加载的机制从磁盘加载对应的数据块到内核态的Page Cache。这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

概括而言,mmap有以下特点:

  1. 文件(page cache)直接映射到用户虚拟地址空间,内核态和用户态共享一片page cache,避免了一次数据拷贝
  2. 建立mmap之后,并不会立马加载数据到内存,只有真正使用数据时,才会引发缺页异常并加载数据到内存

在这里插入图片描述

图八

在这里插入图片描述

图九

memory map具体步骤如下:

首先,应用程序调用mmap(图中1),陷入到内核中后调用do_mmap_pgoff(图中2)。该函数从应用程序的地址空间中分配一段区域作为映射的内存地址,并使用一个VMA(vm_area_struct)结构代表该区域,之后就返回到应用程序(图中3)。当应用程序访问mmap所返回的地址指针时(图中4),由于虚实映射尚未建立,会触发缺页中断(图中5)。之后系统会调用缺页中断处理函数(图中6),在缺页中断处理函数中,内核通过相应区域的VMA结构判断出该区域属于文件映射,于是调用具体文件系统的接口读入相应的Page Cache项(图中7、8、9),并填写相应的虚实映射表。经过这些步骤之后,应用程序就可以正常访问相应的内存区域了。

Java中关于mmap的示例:

    @Test
    public void channelTest() throws IOException {
   
   
        FileChannel inputChannel = FileChannel.open(Paths.get("/Users/djg/Downloads/branch.jpg"), StandardOpenOption.READ);
        FileChannel outputChannel = FileChannel.open(Paths.get("/Users/djg/Downloads/branch2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
        // 内存映射文件
        MappedByteBuffer inputBuffer = inputChannel.map(FileChannel.MapMode.READ_ONLY,0,inputChannel.size());
        MappedByteBuffer outputBuffer = outputChannel.map(FileChannel.MapMode.READ_WRITE,
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值