malloc 和mmap

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap(不考虑共享内存)。

  1. brk 的实现方式是将 Data Segment 的最高地址指针 _edata 往高地址推(分配的内存小于 128k )。
  2. mmap 的实现方式是在 Memory Mapping Segment 找一块空闲的虚拟内存(分配的内存大于 128k )。

(Data segment 和 Memory Mapping Segment 的相关内容查看这里。)

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准 C 库中,提供了 malloc / free 函数分配释放内存,这两个函数底层是由 brk,mmap,munmap 这些系统调用实现的。

example 1

1、进程调用 A = malloc ( 30k ) 以后,内存空间如下图所示。malloc 函数会调用 brk 系统调用,将 _edata 指针往高地址推 30K,就完成虚拟内存分配。

你可能会问:只要把_edata + 30K 就完成内存分配了?

事实是这样的,_edata + 30K 只是完成虚拟地址的分配,A 这块内存现在还是没有物理页与之对应的,等到进程第一次读写 A 这块内存的时候,发生缺页中断,这个时候,内核才分配 A 这块内存对应的物理页。也就是说,如果用 malloc 分配了 A 这块内容,然后从来不访问它,那么,A 对应的物理页是不会被分配的。 

 

 

 

example2

进程调用 B = malloc(40K) 以后,内存空间如下图所示。

 

 

 

example 3

3、当 malloc 分配大于 128k 的内存时,使用 mmap 分配内存。在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为 0 )。

这么做的原因是 brk 分配的内存需要等到高地址内存释放以后才能释放(例如,在 B 释放之前,A 是不可能释放的,这就是内存碎片产生的原因,什么时候收缩看下面),而 mmap 分配的内存可以单独释放。,如下图所示,这里分配 200k 。

 

 

example 4

 4、进程调用 D = malloc(100k) 以后,内存空间如下图所示。

 

 

 

example 5

 5、进程调用 free(C) 以后,C 对应的虚拟内存和物理内存一起释放

 

 

example 6

6、进程调用 free(B) 以后,如下图所示,B 对应的虚拟内存和物理内存都没有释放,因为只有一个 _edata 指针,如果往回推,那么 D 这块内存怎么办呢?当然,B 这块内存是可以重用的,如果这个时候再来一个 40K 的请求,那么 malloc 很可能就将 B 这块内存返回的。 

 

 

example 7

 

7、进程调用 free(D) 以后,如下图所示,B 和 D 连接起来变成一块 140K 的空闲内存。当最高地址空间的空闲内存超过128K(可由 M_TRIM_THRESHOLD 选项调节)时,执行内存紧缩操作(trim)。在上一个步骤 free 的时候,发现最高地址空闲内存超过 128 K,于是内存紧缩,如下图所示。

 

 

 

2 mmap
了解完 虚拟内存 ,再回过头来讲一下 mmap ,也就是内存映射 。内存映射是将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容的过程。

2.1 基础概念
先讲下内存映射里的一些概念。

映射对象类型

虚拟内存区域可以映射以下两种类型的对象:

普通文件:即磁盘文件中的一块 连续 的区域。
匿名文件:一个由内核创建的全为 二进制零 的文件。当CPU首次引用此区域时,将以二进制零填充到页表中。
共享对象

在上一节 虚拟内存 可得知,系统为每个进程提供了单独的页表,从而也实现了进程间数据访问权限的管理以及数据的保护。但同时,通过内存映射的机制,将对象作为 共享对象 映射到两个进程的虚拟内存亦可实现数据的共享。

 

 

 

2.2 使用方式
然后先讲下如果我们应该如何通过内存映射的方式来访问文件。 mmap() 的函数定义如下:

 

void * mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)

 



其中参数的含义分别是:

start: 期望的进程虚拟内存起始位置,填 NULL 时由内核来决定起始位置
length: 需要映射的对象字节大小
fd: 文件句柄
offset: 距离文件开始处的偏移量
prot: 映射对象的访问权限,用于可指定是否可读写、执行。
flags: 映射对象的类型,例如指定是映射普通文件还是请求二进制零、映射共享对象还是私有的写时复制对象等。
前4项地含义可通过下图更直观地了解:

 

 

 

而在 iOS 开发中,当我们需要的数据类型是 NSData 时,可以更简便地通过调用以下方法

 
 

2.3 读取过程
当我们通过 mmap 读取文件时,将经历以下步骤:

在当前用户虚拟内存空间中分配一片 指定映射大小 的虚拟内存区域。
将磁盘中的文件映射到这片内存区域,等待后续 按需 进行页面调度。
当CPU真正访问数据时,触发 缺页异常 将所需的数据页从磁盘拷贝到物理内存,并将物理页地址记录到页表。
进程通过页表得到的物理页地址访问文件数据。

 

 


而作为对比,当通过 标准IO 读取一个文件时,步骤为:

将 完整 的文件从磁盘拷贝到物理内存(内核空间)。
将完整文件数据从 内核空间 拷贝到 用户空间 以供进程访问。

 

 


2.4 优劣
通过上面 mmap 与 标准IO 的对比,不难发现调用mmap具有以下的优势:

物理内存占用延后:数据直到真正被使用时才会发生拷贝。
物理内存占用减少:对于同一份文件无需在物理内存中存放两份,且文件区被划分成片,缺页异常时只将所需的页拷贝到物理内存。
方便实现跨进程数据交互、共享:当映射到虚拟内存的对象被设置为共享对象,则不同进程对映射对象的写操作相互可见。
然而也能发现 mmap 存在以下 劣势 :

无法映射变长文件:调用mmap()时需指定要映射的文件位置和需要映射的大小范围。
如果需要映射的文件过大,会导致过度占用虚拟内存:在调用mmap()后,虚拟内存空间就创建了,此时虽然不会占用物理内存,但依然会占用虚拟内存。此时可考虑只映射文件中自己需要的部分。
由此,当我们需要访问一个比较大的文件,尤其是当我们只需要访问其中的一小部分数据的时候,我们可以尝试通过 mmap 的方式来进行访问,减少由于该文件过大而对物理内存的过度占用。
 

 

 

posted on 2021-04-07 19:50  tycoon3  阅读(3331)  评论(0编辑  收藏  举报

导航