在 Linux 内存管理中,MAP_NORESERVE 是传递给 mmap 系统调用的一个标志,它主要影响操作系统如何处理虚拟内存和交换空间(SWAP Space)的预留. 具体来说,MAP_NORESERVE 的作用和特点包括:
- 减少交换空间预留: 当不使用 MAP_NORESERVE 标志时,操作系统为 mmap 创建的映射预留足够的交换空间,以确保映射的内存能够在需要时被交换出去。这意味着系统需要确保有足够的交换空间来备份所有可能被写入的内存页; 使用 MAP_NORESERVE 后,系统不会为映射预留交换空间。这意味着在内存紧张时,如果需要将这些页面交换出去,而又没有足够的交换空间,进程可能会收到内存不足的错误
- 大量内存映射的应用场景: 一些应用可能会映射大量的内存,但只使用其中的一小部分。在这些情况下,使用 MAP_NORESERVE 可以避免不必要地预留大量的交换空间, 这种做法对于某些类型的应用程序来说是有益的,尤其是那些知道自己不太可能使用所有映射内存的应用程序
- 风险和考虑: 使用 MAP_NORESERVE 可能会增加进程在内存压力下被操作系统杀死的风险(如 Linux 的 OOM Killer),因为它减少了系统为进程保留的资源, 开发者在使用 MAP_NORESERVE 时需要非常谨慎,确保应用程序能够正确处理潜在的内存不足情况
- 与 overcommit_memory 设置相关联: 系统的内存过度提交(overcommit)设置也会影响 MAP_NORESERVE 的行为. 在某些 overcommit 设置下,即使没有 MAP_NORESERVE,系统也可能允许分配超过物理内存和交换空间总和的内存
MAP_NORESERVE 标志的主要目的还是欺骗应用程序,内核本来没法保证一定能提供这么多内存,但可以先让程序运行起来,等内存不够用了在通过回收或者换出方式腾挪出一点内存出来,运气好的话可以提供足量内存,运气不好的话进程直接因为内存不足直接挂掉。因此开发者可以结合自己的场景,如果大量的虚拟内存,但不一定真正使用那么多虚拟内存时,可以使用该标志让程序先运行起来.
MAP_NORESERVE 标志直接在 SYS_MMAP 系统调用即可,那么应用程序可以分配超过当前系统可用物理内存还多的内存,这种分配不适用与预分配. 无论是匿名内存或者文件映射内存,SYS_MMAP 系统调用 ARG3 带上 MAP_NORESERVE 标志即可,那么系统直接分配超量的内存. 接下来分析 MAP_NORESERVE 标志对内存行为的影响:
在 SYS_MMAP 系统调用里使用 MAP_NORESERVE 标志时,其代码逻辑如上图,可以看到在 ADD NORESERVE 处如果检查到 MAP FLAGS 里包含了 MAP_NORESERVE 标志,那么说明不需要检查系统是否有足够的内存,可以直接为进程分配足量的虚拟内存,因此向 VMA 添加了 VM_NORESERVE 标志. 其余步骤就不涉及 MAP_NORESERVE 标志.
在 do_mmap 函数了,函数在 1532 行检查 MAP FLAGS 里是否存在 MAP_NORESERVE,如果存在那么进入 1533 行分支进行处理,函数首先检测 sysctl_overcommit_memory 是否为 OVERCOMMIT_NEVER,如果是就算使用了 MAP_NORESERVE 标志,内核也不会让应用程序超量的分配内存,此时不会添加 VM_NORESERVE 标志; 反之则添加 VM_NORESERVE 标志以此支持超量内存的分配。另外 1538 行检测到是 HugeTLB 大页的分配,那么直接添加 VM_NORESERVE, 这是因为采用惰性分配 HugeTLB 大页内存时,系统会从 HugeTLB 大页池子里提前预留足量的大页.
# 0: OVERCOMMIT_GUESS
# 1: OVERCOMMIT_ALWAYS
# 2: OVERCOMMIT_NEVER
# PROC
echo 0 > /proc/sys/vm/overcommit_memory
# SYSCTL
sudo sysctl -w vm.overcommit_memory=1
通过上面的分析,Linux 内核里使用 sysctl_overcommit_memory 作为总开关控制这内存超量分配,其支持 PROC 和 SYSCTL 两种接口,可以向接口写入三个值:
- OVERCOMMIT_GUESS: 0 表示内核会尝试猜测是否有足够的内存可供分配请求,这是默认设置.
- OVERCOMMIT_ALWAYS: 1 表示内核允许过度承诺内存,程序可以分配超过物理内存和交换空间总和的内存.
- OVERCOMMIT_NEVER: 2 表示内核不允许分配超过定义的内存和交换空间总量
通过上面的分析之后,在 sysctl_overcommit_memory 允许超量内存的情况下,可以使用 MAP_NORESERVE 标志让进程分配到没有保障的内存,因此开发者需要很谨慎使用该标志,以免偷鸡不成蚀把米。那么接下来通过一个实践案例了解 MAP_NORESERVE 标志的使用,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] Memory Mapping: FLAGS MAP_NORESERVE --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-MMAP-MAP-NORESERVE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-MEMORY-MMAP-MAP-NORESERVE-default Source Code on Gitee
实践案例由一个应用程序构成,程序在 21 行调用 mmap 分配一段 HugeTLB 大页内存,且在 24 行使用了 MAP_NORESERVE 标志,这里需要重点提醒一下,在分配 HugeTLB 大页时,无论使用惰性分配还是预分配,系统都会从 HugeTLB 大页池子里预留足够数量 HugeTLB 大页,就算用户进程不使用这段虚拟内存,内核也要为其预留,这样可能会造成 HugeTLB 大页内存的浪费. 回到应用程序,由于使用了 MAP_NORESERVE 的缘故导致系统不需要在 HugeTLB 大页池子里预留大页,那么这样也会出现一个问题,当进程访问这段虚拟内存时,MMU 发现物理内存不存在那么触发缺页,缺页异常处理函数从 HugeTLB 大页池子里为其分配内存,可以这个时候 HugeTLB 大页池子里没有足够的内存,那么导致缺页处理函数 OOM,最终导致进程段错误. 继续查看实践案例,其在 31 行对虚拟内存进行访问,此时会触发缺页,缺页异常处理完毕之后可以对虚拟内存读写访问,最后就是回收内存. 以上便是完整的实践过程,开发者可以使用内存流动工具查看整个流程,例如在 33 行查看内核分配 HugeTLB 大页失败的过程, 在 33 行前后加上内存流动开关:
接着在 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 4579 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-MMAP-MAP-NORESERVE-default/
# 编译内核
make kernel
# 运行实践案例
make build
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本可以看到运行实践案例,可以看到应用程序成功的执行 mmap 分配到了虚拟内存,可是当访问虚拟内存时,由于 HugeTLB 大页池子里没有可用的大页,因此导致分配大页失败,此时直接 SIGBUS 导致进程段错误. 通过这个实践案例说明了在使用 MAP_NORESERVE 标志时一定要小心,否则会导致进程段错误,那么开发者可以思考一下如何解决上面的段错误问题,以及如何防止 MAP_NORESERVE 带来的潜在危害.