第49章 内存映射
概述
映射分为两种:
- 文件映射:将一个文件的一部分或全部映射到调用进程的虚拟内存中,就可以通过在内存区操作来访问文件内容了
- 匿名映射:没有对应的文件,映射的分页被初始化为0
映射的类型:
- 私有映射:映射内容发生的变更对其他进程不可见,对于文件映射来说,变更将不会在底层文件上进行
- 共享映射:映射内容发生的变更对所有共享同一映射的其他进程都可见,对于文件映射来说,变更将会在底层文件上进行
可细分为四种:
- 私有文件映射:映射内存初始化为文件的内容,主要用途是使用一个文件的内容初始化一块内存
- 私有匿名映射:每次调用mmap都会创建一个与其他匿名映射不同的新映射(映射不会共享物理分页),主要为一个进程分配新内存(用零填充)
- 共享文件映射:所有映射一个文件同一区域的进程共享同样的内存物理分页,两个用途:允许内存映射IO,一个文件被加载到进程虚拟内存的一个区域并且对这块内容的更改自动写到这个文件;允许无关进程共享一块内容以便以一种类似System V共享内存的方式执行IPC
- 共享匿名映射:每次调用mmap都会创建一个与其他匿名映射不同的新映射(映射不会共享物理分页),它允许以一种类似System V 共享内存的方式进行IPC,但只有相关进程可以这样做
进程在执行exec时映射会丢失,通过fork后的子进程会继承映射,映射类型也会继承
通过Linux特有的/proc/PID/maps能够查看一个进程的映射信息
创建一个映射
在调用进程的虚拟空间创建一个新映射:
#include <sys/mmap.h>
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t offset);
// 若成功,返回映射起始地址,若出错,返回MAP_FAILED
// addr为NULL,内核自动选择一个合适的地址,这是推荐的可移植的做法
// len指定了映射的字节数
// prot的取值(一个进程若在访问时违反了保护位,内核产生SIGSEGV信号):
PROT_NONE: 区域内容无法访问
PROT_READ: 区域内容可读
PROT_WRITE: 区域内容可写
PROT_EXEC: 区域内容可执行
// flag的取值:
MAP_PRIVATE: 私有映射,区域内内容发生的变更对使用同一映射的其他进程不可见,对文件映射,变更不会反应在底层文件
MAP_SHARED: 共享映射,区域内内容发生的变更对使用同一映射的其他进程可见,对文件映射,变更会反应在底层文件
// fd和offset用于文件映射,标识被映射的文件和偏移(是系统分页大小的倍数),映射整个文件,将offset指定为0且len为文件大小
解除映射区域
从调用进程虚拟地址空间删除一个映射:
int munmap(void *addr, size_t len);
// 若成功,返回0,若出错,返回-1
// addr是待解除映射的起始地址
// len是待解除映射区域大小,可以解除一个映射中的部分映射,原来的映射要么收缩,要么被分成两部分
// addr和len指定的地址范围如果不存在映射,则调用不起作用,但返回0
解除映射期间,内核会删除进程持有的指定地址空间范围内的所有内存锁(mlock或mlockall建立)
进程终止或执行了exec之后进程中所有的映射会自动被解除
为确保一个共享文件映射的内容被写入到底层文件,使用munmap解除映射之前需要调用msync
文件映射
私有文件映射
常见用途:
- 允许多个执行同一程序或使用同一共享库的进程共享同样的文本段,它从底层可执行文件或库文件的相应部分映射而来
映射一个可执行文件或共享库的初始化数据段,会被处理成私有使得对映射数据段内容的变更不会发生在底层文件
共享文件映射
两个用途:内存映射IO:通过访问内存的字节执行文件IO,依靠内核确保对内存的变更被传递到映射文件上,是使用read和write进行IO的替代方案
优势:
- 简化一些逻辑
- 某些情况下,可以提升性能,原因:read/write需要两次传输,一次是文件和内核高速缓冲区之间,一次是高速缓冲区和用户缓冲区之间,使用mmap是直接将内存数据写到文件中,这种性能提升在大型文件重复随机访问时最有可能体现出来
劣势:
- 对于小数量IO,内存映射IO的开销(映射、分页故障、解除映射即更新硬件内存管理单元的超前转换缓冲器)比简单的read/write要大
- IPC:与System V共享内存对象之间的区别是区域上内容的变更会反应到底层的映射文件上,这种特性对于那些需要共享内存内容在应用程序或系统重启时能够持久化的应用程序非常有用
边界情况
试图访问映射结尾之外的字节导致SIGSEGV信号
创建一个大小超过底层文件大小的映射可能是无意义的,但通过扩展文件的方法(write或ftruncate)可以使得这种映射之前不可访问的部分变得可用
同步映射区域:msync
内核自动将发生在MAP_SHARED映射内容的变更写入底层文件,但默认内核不会保证同步何时发生,可手动同步:
int msync(void *addr, size_t len, int flag);
// 若成功,返回0,若出错,返回-1
// addr和len指定了需同步的起始地址和大小
// flag的取值:
MS_SYNC: 同步写入,阻塞直到所有修改被写入磁盘
MS_ASYNC: 异步写入,仅与内核高速缓冲区同步,会在将来某个时刻写入磁盘(由pdflush内核线程执行)
MS_INVALIDTE: 使得映射数据的缓存副本失效
mmap的其他标记
除了MAP_PRIVATE和MAP_SHARED之外,还有其他标记,SUSv3之规定了MAP_FIXED
- MAP_ANONYMOUS:创建 一个匿名映射
- MAP_FIXED:
- MAP_HUGETLB:创建一个使用巨页的映射
- MAP_LOCKED:将映射分页锁进内存
- MAP_NORRESERVE:是否提前为映射的交换空间执行预留
- MAP_POPULATE:填充一个映射的分页,对文件映射而言,将执行超前读取
- MAP_UNINITIALIZED:不清楚匿名映射
匿名映射
Linux有两种创建匿名映射的方法:
- flag指定为MAP_ANONYMOUS并将fd指定为-1(其实会忽略fd)
- 打开/dev/zero设备文件(从中读取的数据总是0,写入的数据总被丢弃)并将得到的文件描述符传递给mmap
不管哪种方法,映射中的字节总会被初始化为0,offset被忽略
私有匿名映射
用来分配进程私有的内存块并将其中的内容初始化为0
共享匿名映射
允许相关进程共享一块内存区域而无需一个对应的映射文件
重新映射一个区域:mremap
#defein _GNU_SOURCE
#include <sys/mmap.h>
void mremap(void *old_addr, size_t old_size, size_t new_size, int flag, ...);
// old_addr和old_size指定了需要扩展或收缩的既有映射的位置和大小,新大小通过new_size指定
在重新映射过程中内核可能为映射在进程的虚拟空间重新指定一个位置,具体由flag控制:
- MREMAP_MAYMOVE:内核可能重新指定位置,没有指定此标记且无足够空间扩展,返回ENOMEM错误
- MREMAP_FIXED:只能与MREMAP_MAYMOVE一起使用,且需指定一个额外的 *new_addr的参数,映射将迁移至此,之前由new_addr和new_size范围确定的映射都会被解除
由于调用成功返回的地址可能与之前不同,使用mremap的应用程序在引用映射区的地址时应使用偏移量而不是绝对指针
Linux上,remalloc使用mremap高效的为malloc使用mmap MAP_ANONYMOUS分配的大内存重新指定位置
MAP_NORRESERVE和过度利用交换空间
懒交换预留:内核只在用到映射分页的时候(即当程序访问访问时)为它们预留交换空间,优点是程序总共利用的虚拟内存量能够超过RAM+交换空间的总量,这种方式可以很好工作,只要所有进程都不试图访问整个映射,否则RAM和交换空间就会被耗尽,此时内核会杀死系统中一个多多个进程降低内存压力
overcommit_memory 指定了MAP_NORRESERVE 未指定
0 允许过度利用 拒绝明显的过度利用
1 允许过度利用 允许过度利用
2 严格的过度利用 严格的过度利用
Linux特有的/proc/sys/vm/overcommit_memory的值控制着内核对交换空间过度利用的处理,当其值大于等于2时,内核在所有mmap分配上执行严格的记账并将系统分配的总量控制在小于等于:
[swap_size] + [RAM_size] overcommit_ratio * / 100
Linux特有的/proc/sys/vm/overcommit_ratio是一个整数百分比,默认50,表示内核最多可分配的空间为系统RAM总量的50%,只适用于私有可写映射和共享匿名映射
OOM杀手一般不会杀死这些进程:
- 特权进程,因为他们可能在执行重要的任务
- 正在访问裸设备(没有分区格式化,无法被读取)的进程,杀死他们可能导致设备处于不可用状态
- 已经运行很长时间或已经消耗大量CPU的进程,杀死他们可能导致丢失很多“工作”
Linux特有的/proc/PID/oom_score代表的权重越大,被杀死的可能性越大,而Linux特有的/proc/PID/oom_adj可以影响oom_score的值,其值在-16到+15之间,负数减小oom_score,正数增加oom_score,特殊值-17完全将进程从OOM杀手名单删除
MAP_FIXED标记
指定了MAP_FIXED,那么addr必须分页对齐,可移植程序不该指定此标记并将addr置为NULL,存在需要指定此标记的情况是,它可以将一个文件的多个部分映射到一块连续的内存区域:
- 使用mmap创建一个匿名映射,mmap调用中将addr指定为NULL并不指定MAP_FIXED
- 使用一系列指定了MAP_FIXED的mmap调用将文件区域映射(重叠覆盖)到上步创建的映射的不同部分
使用remap_file_pages可以取得同样的效果,它是Linux特有的
非线性映射:remap_file_pages
使用mmap创建的文件映射是连续的,映射文件的分页与内存区域的分页存在一个顺序的,一对一的对应关系
使用MAP_FIXED的mmap调用创建非线性映射的伸缩性不好,每个mmap调用都会创建一个独立的内核虚拟内存区域(VMA)数据结构,大量的VMA会降低虚拟内存管理器的性能,/proc/PID/maps的一行代表一个VMA
使用remap_file_pages创建非线性映射的步骤:
int remap_file_pages(void *addr, size_t size, int prot, size_t pgoff, int flag); // 若成功,返回0,若出错,返回-1 // addr标识分页需调整的既有映射,必须是位于之前创建的映射区域的地址 // size指定了文件区域的长度,单位是字节 // pgoff指定了文件区域的起始位置,单位是系统分页代销 // prot会被忽略,其值必须是0 // flag未使用 // 目前仅适用于MAP_SHARED映射
```
// 系统分页大小
ps = sysconf(_SC_PAGESIZE);
// 创建包含三个系统分页的共享文件映射
addr = mmap(0, 3*ps, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 将文件分页0映射到内存区域的分页2
remap_file_pages(addr, ps, 0, 2, 0);
// 将文件分页2映射到内存区域的分页0
remap_file_pages(addr + 2*ps, ps, 0, 0, 0);