Android 内存映射mmap浅谈

Android通信系列目录

  1. Android Binder通信原理详解
  2. Android 内存映射mmap浅谈
  3. Google Protobuf 实践使用开发

博客创建时间:2020.11.01
博客更新时间:2022.10.06

以Android studio build=7.0.0,SDKVersion 31来分析讲解。如图文和网上其他资料不一致,可能是别的资料版本较低而已。


前言

最能想到mmap函数的是跨进程通信Binder机制中使用到了,它的强大之处在于通过内存映射直接对文件进行读写,减少了对数据的拷贝次数和提高了IO读写的效率。

Linux 内存层构

首先对Linux内核内存做一些知识科普。linux中的内存我们也常称之为 内核空间 。以一次磁盘中读请求为例,展示其经历的内存层次。
在这里插入图片描述
七层结构

虚拟文件系统层
作用是屏蔽下层具体文件系统操作的差异,为上层的操作提供一个统一的接口。

文件系统层
具体的文件系统层,一个文件系统一般使用块设备上一个独立的逻辑分区。

Page Cache (层页高速缓存层)
引入 Cache 层的目的是为了提高 Linux 操作系统对磁盘访问的性能。Cache层会缓存内存中的部分数据,当请求到达时,如果Cache中有数据就会直接将数据返回,米面对底层磁盘的操作,提高性能。

通用块层
作用是接收上层发出的磁盘请求,并最终发出 I/O 请求。

I/O 调度层
作用是管理块设备的请求队列。

块设备驱动层
利用驱动程序,驱动具体的物理块设备。

物理块设备层
具体的物理磁盘块。


Cache内存
Page Cache层,也称页缓存、内核空间。实际上就是内核中的物理内存,在磁盘和用户空间之间多了一层缓存层,由内核负责管理控制。由于物理内存的速度远远快于磁盘的速度,数据放入Page Cache中可以更快的进行访问。

数据被访问后,短时间内有极大会再一次被访问,在短时间内集中访问同一数据的原理就叫做局部性原理

Cache中的数据是可以被回收的,当内存空间不够时,就会根据一定规则将部分数据回收。


用户空间与内核空间

Linux的进程是相互独立的,一个进程是不能直接操作或者访问别一个进程空间的。每个进程空间还分为用户空间和内核(Kernel)空间,相当于把Kernel和上层的应用程序抽像的隔离开。

用户空间内核空间,用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。

这里有两个隔离,一个进程间是相互隔离的,二是进程内有用户空间和内核空间的隔离。

  • 进程间,用户空间的数据不可共享,所以用户空间 = 不可共享空间
  • 进程间,内核空间的数据可共享,所以内核空间 = 可共享空间,所以Linu系统的内存通常是MemFree+Cache
  • 所有进程共用1个内核空间

系统调用

进程间的交互就叫进程间通信(IPC,或称跨进程通信),而进程内的用户和内核的交互就是系统调用

用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。

系统调用示例
进程内用户空间 内核空间 进行交互需通过系统调用两个函数。

  • copy_from_user():将用户空间的数据拷贝到内核空间
  • copy_to_user():将内核空间的数据拷贝到用户空间
    在这里插入图片描述

内存映射原理

mmap是一种内存映射文件的方法,它将一个文件映射到进程的地址空间中,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。

当磁盘地址和进程虚拟地址建立关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写数据到磁盘上,即直接完成了对文件的操作而不必在调用read/write等系统调用函数。同样的如果磁盘中内容有修改,也会直接反映到用户空间其数据改变了。

所以通过mmap映射方式可以使不同进程间共享磁盘文件,其共享对象可为普通文件或匿名文件

映射内存的分配

mmap映射区域大小必须是物理页大小(page_size)的整倍数(在Linux中内存页通常是4k)。因为内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。

例如,有一个文件的大小是5K,mmap函数从文件的起始位置映射5K到虚拟内存中,由于内存物理页是4K,虽然映射的文件只有5K,但是实际上映射到内存区域的内存是8K,以便满足物理页大小的整数倍。映射后对5~8K的内存区域用零填充,对这部分的操作不会报错也不会写入到原文件中。

传统I/O读写流程

  1. 用户进程发起文件数据的读请求
  2. 内核通过查找进程文件符表,定位内核已打开文件集上的文件信息,从而找到文件inode
  3. inode在address_space上查找要请求的文件页是否已缓存在页缓存中
  4. 如已在缓存页中,则直接返回这片文件页上的内容
  5. 如不在缓存页上,就会引发缺页中断。 当发生缺页中断时,内核则调用nopage函数把所缺的页从磁盘装入到内存内核中及Page Cache中。接着再发起读页面过程,从而将数据从页缓存中拷贝到用户空间中

在这里插入图片描述
特点:

  1. 常规文件操作为了读写效率和保护磁盘,使用了页缓存机制
  2. 页缓存处在内核空间中,不能直接被用户进程直接寻址,需要将数据从页缓存中拷贝到主内存

mmap读写流程

  1. 用户进程调用进程内存映射函数库mmap,当前进程在线程虚拟地址空间中寻找一段空闲的满足要求的虚拟地址。

     1. 在当前进程的虚拟地址空间中,寻找一段满足要求的虚拟地址
     2. 为此虚拟地址分配一个虚拟内存区域,vm_area_struct结构
     3. 初始化该虚拟内存区域
     4. 插入该虚拟内存区域到进程的虚拟地址区域链表中
    
  2. 内核同样收到请求后会调用内核的mmap函数,实现地址映射关系配对,即进程虚拟地址空间<< >>文件磁盘地址 关系映射,该映射与内核内存没有任何关联

     1. 进程调用mmap函数,内核同样会得到消息,最终内核调用自身的系统调用函数mmap。(两mmap函数不一样)
     2.  内核mmap函数通过`虚拟文件系统`定位到文件磁盘物理地址。
     3. 通过remap_pfn_range()建立页表,实现了文件地址和虚拟地址区域的映射关系。
    
  3. 进程的读/写操作访问虚拟地址空间这一段地址,如果读写操作该改变了虚拟地址空间内容,则一段时间后系统会自动回写脏页面到对应的磁盘地址中,即完成了写入文件的操作。

    修改的脏页面不会立即更新,而是有延时,可以通过msync()来强制同步。通过此法能将所写的内容立即保存到磁盘中
    

在这里插入图片描述

特点:

  1. 用户空间内核空间磁盘块通过映射直接交互,不在间接通过页缓存
  2. 文件读写操作跨过了页缓存,数据拷贝次数减少为只需一次
  3. 借助硬盘的大空间,对于大规模数据的读写避免对页内存空间大小的依赖,提高操作效率。

mmap数据读写的性能提升就在于对数据的读写拷贝次数,mmap只需要一次系统调用(一次拷贝),后续操作不需要系统调用。并且访问的数据不需要在page cache和用户缓冲区之间拷贝。


mmap读写优势

  1. 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

  2. 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

  3. 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。

     如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
    
  4. 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。


mmap的使用

mmap的函数位于内核的<sys/mman.h> 头文件中,与其相关的几个函数也列出如下:

// 用户进程调用,    函数用于将文件映射到内存
void* mmap(void addr, size_t length, int prot, int flags, int fd, off_t offset);


// 函数用于取消映射,进程在映射空间对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap() 后才执行该操作。
int munmap(void *addr, size_t length);
// 函数用于实现磁盘文件内容与共享内存区中的内容一致,即同步操作。
// 除了调用munmap取消映射,我们也可以调用msync()实现磁盘上文件内容与内核内存的内容一致
int msync(void * addr, size_t len, int flags);

mmap的使用场景

1.Linux进程的创建
Linux执行一个程序,这个程序在磁盘上,为了执行这个程序,需要把程序加载到内存中,这时也是采用的是mmap。你可以从/proc/pid/maps看到每个进程的mmap状态。

2. 内存分配
我们使用c库的malloc申请内存,malloc的分配内存有两个系统调用,一个brk,另一个就是mmap。

mmap不仅可以映射文件,也可以映射内存,当mmap使用的flag是MAP_ANONYMOUS,称为建立匿名映射,此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。匿名映射存储的数据就是在物理内存上,不属于任何文件。malloc分配内存底层就是用mmap的匿名映射来操作的。

3. Binder进程间通信
了解进程间通信的人都知道Android使用的是Binder进行进程间通信,它的效率高于Linux其他传统的进程间通信,因为它只要一次拷贝,而之所以只需要进行一次拷贝的原因就在于使用了mmap。


mmap优缺点

Linux将虚拟内存与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程就是内存映射(memory mapping)

对文件进行mmap后,会在进程的虚拟内存分配地址空间,创建映射关系。这样就可以采用指针的方式来读写操作这一段内存,而系统会自动回写到对应的文件磁盘上。
在这里插入图片描述
I/O在系统的操作流程
虚拟内存被操作系统划分为两块:用户空间和内核空间,用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全期间两者是隔离得,即时用户的程序崩溃了,内核也不受影响。
在这里插入图片描述
文件读写流程:

  1. 调用write,告诉内核需要写入数据的开始地址和长度
  2. 内核将数据拷贝到内核缓存
  3. 有操作系统调用,将数据拷贝到磁盘,完成写入

优点

  1. mmap 防止数据丢失,提高读写效率
  2. 精简数据,以最少量的数据局量表示最多的信息,减少数据大小
  3. 增量新增,避免每次数据新增时的全量写入
  4. mmap对文件的读写操作只需要对磁盘到用户主存的一次数据拷贝过程,减少了数据的拷贝次数,提高文件读写效率。
  5. mmap使用逻辑内存对磁盘文件进行映射,操作内存就相当于操作文件,不需要开启线程,操作mmap的速度和操作内存的速度一样快。
  6. mmap提供一块随时写入的内存,app只管往里写入数据,由操作系统如内存不足。进程退出时负责将内存写回到文件。不必担心crash导致数据丢失。
  7. mmap的适用场景是大文件的频繁读写,这样就可以节省很多IO的耗时。
  8. 即使进程意外死亡, 也能够通过 Linux 内核的保护机制, 将进行了文件映射的内存数据刷入到文件中, 提升了数据写入的可靠性

缺点:

  1. 因为mmap是按照页存储方式进行存储,每页4096字节,如果数据只有100字节,则正页将有大大的浪费。
  2. 写回文件的工作由系统负责,但是并不是实时的,是定期写回到磁盘的,中间如果发生内核崩溃、断电等,还是会丢失数据,不过可以通过msync将数据同步回磁盘。

总结

本篇博文主要讲解的是mmap内存映射相关知识,主要分析了mmap的原理和使用案例流程分析,在Android中Binder机制中其核心就使用了mmap内存映射。


相关链接

  1. Android Binder通信原理详解
  2. Android 内存映射mmap浅谈
  3. Google Protobuf 实践使用开发

扩展链接:

  1. Android CameraX 使用入门
  2. Android 今日头条屏幕适配详细使用攻略
  3. Android 史上最新最全的ADB及命令百科,没有之一

博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值