欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

内存映射相关

程序员文章站 2024-03-02 23:56:46
...

一、概述

本篇博文的主要目的是学习整理,以备忘。同时,本篇文章涉及的知识点也是理解Binder机制的前提。

二、相关概念

1、虚拟地址空间

       在操作系统中,进程与进程间的内存是不能共享的,即进程A是不能直接访问进程B的数据的,也就是所谓的进程隔离。进程A与进程B之间进行数据交互,只能通过IPC(进程间通信)机制。

       在操作系统中,每个进程(操作系统本身也是一个进程,只是比较特殊)都有自己独立的虚拟内存空间,其目的是为了更好的管理内存,并且能够保证内存对程序员而言是透明的,也就是写程序的时候对每一个程序来说都是一样的地址空间。为了安全,操作系统将虚拟空间划分成了用户空间和内核空间,简单的说就是内核空间是系统内核运行的空间,用户空间是用户程序的运行空间。现在的操作系统都是采用的虚拟存储器,对于32位的操作系统,它的寻址空间(虚拟存储空间)是2的32次方,也就是4G。对于Linux而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间。

     内存映射相关

       与虚拟空间相关的就是虚拟地址了。虚拟地址是相对于真实的物理地址而言的。这里在普及两个概念,以更好地理解什么是虚拟地址空间、虚拟地址:MMU、TLB、PTE等。

       MMU(内存管理单元):主要负责CPU内存访问的时候,将虚拟地址转换为物理地址的单元。也就是说CPU想要访问内存就必须经过MMU的转换,获得真正的物理地址,才能读到物理内存的数据。其实MMU只是一个简单的计算单元,它通过虚拟地址查找页表,找到对应的物理地址,然后返回给CPU。

      TLB:页表缓冲,里面存放的是一些页表文件(虚拟地址到物理地址的转换表)。

      PTE:物理地址或者硬盘存储地址。

内存映射相关

它们的关系:

      当CPU得到一个虚拟地址去访问内存的时候,会先将虚拟地址发送给MMU,然后MMU会去TLB查询是否存在这个虚拟地址到物理地址的缓存。如果存在,则直接返回给CPU;否则,将虚拟地址经过计算获得物理内存中页表项的地址,然后读取得到PTE(Linux可能要读取四次)。然后MMU会将给PTE缓存在TLB中,最后使用这个物理地址再次访问物理内存或者硬盘地址获得想要访问的内容。

2、用户态和内核态

       虽然在逻辑上划分为用户空间和内核空间,但不可避免的是用户空间需要访问内核资源,比如文件操作和网络访问等等。为了突破隔离的限制,就需要借助系统调用来实现,系统调用时用户空间访问内核空间的唯一方式。保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。

Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。

       当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。

       当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。

系统调用主要通过如下两个函数来实现:

copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间

内存映射相关

       用户态不能直接和物理设备打交道的,如果想要把硬盘上的一块区域读到用户态,则需要两次Copy(硬盘 - > 内核缓存区 -> 用户空间),过程如上图所示,不再详述。

三、内存映射mmap

1、概念

      设备或者硬盘存储的一块空间映射到物理内存,然后操作这块物理内存就是在操作实际的硬盘空间,不需要经过内核态传递。比如你的硬盘上有一个文件,你可以使用linux提供的mmap接口,将这个文件映射到进程的一块虚拟地址空间中,这块空间会对应一块物理内存,当你读写这块物理内存的时候,就是在读取时机的磁盘文件,直接&高效。

2、原理

      调用mmap的时候,内核会在该进程的虚拟内存空间的映射区域查找一块满足需求的空间用于映射该文件,然后生成该虚拟地址的页表项,该页表项此时的有效位(标志着是否已经在物理内存中)为0,页表项的内容是文件的磁盘地址,此时mmap的任务已经完成。

      当mmap建立建立完页表的映射后,就可以操作该块内存了,进行的所有改动都会自动写回磁盘文件。第一次访问该块内存的时候,因为页表项的有效位还是0,就会发生缺页中断,然后cpu使用该页表项的内容,也就是磁盘的文件地址,将该地址指向的内容加载到物理内存中,并修改页表项的内容为该物理地址,有效位置为1。

内存映射相关

      注意:内存映射只发生一次Copy<硬盘 -> 物理内存【硬盘->用户态】>,而不需要两次Copy(硬盘 - > 内核缓存区 -> 用户空间)。

三、Java I/O与内存映射

       在传统的文件I/O操作中,我们都是调用操作系统提供的底层标准IO系统调用函数  read()、write(),此时调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。至于为什么要多此一举搞一个内核IO缓冲区把原本只需一次拷贝数据的事情搞成需要2次数据拷贝呢?  减少I/O操作。

       OS根据局部性原理【即我们访问了文件的某一段数据,那么接下去很可能还会访问接下去的一段数据】会在一次 read()系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中,当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程私有空间,避免了再次的低效率磁盘IO操作。

       既然如此,JAVA的IO包中为啥还要提供一个 BufferedInputStream 类来作为缓冲区呢。关键在于四个字,"系统调用"!当读取OS内核缓冲区数据的时候,便发起了一次系统调用操作(通过native的C函数调用),而系统调用的代价相对来说是比较高的,涉及到进程用户态和内核态的上下文切换等一系列操作。

--> 内核缓冲区减少I/O操作,而Java提供的BufferedInputStream 是为了减少系统调用。

参阅:

mmap内存映射JavaNIO之浅谈内存映射文件原理