在 Linux 内存管理中,MAP_LOCKED 是传递给 mmap 系统调用的一个标志,用于控制内存映射的行为, 当内存使用了该标志,那么不会被换成到 SWAP Space 里。具体来说,使用 MAP_LOCKED 标志会有以下影响:

  • 锁定内存: 当使用 MAP_LOCKED 标志时,映射创建后,相关的内存页会立即锁定在物理内存中,而不会被交换到交换空间(SWAP space)中, 这意味着这部分内存始终保留在 RAM 中,不会因为内存压力而被交换出去,从而确保了对这些内存区域的快速访问
  • 防止页面交换: 通常操作系统为了管理内存,可能会将不活跃的页面移动到交换区。但是锁定的内存不会被交换出去,这对于需要快速、确定性访问内存的应用(如实时系统或需要快速响应的高性能应用)来说很重要
  • 权限要求: 由于锁定内存可能会导致系统资源被长时间占用,普通用户在默认情况下可能没有权限使用 MAP_LOCKED。在很多系统上,使用这个标志可能需要特权(如 root 用户)或相应的限额(ulimit)设置
  • 慎用: 虽然 MAP_LOCKED 可以保证快速访问和防止交换,但滥用它可能会对系统的整体性能和稳定性产生负面影响。如果大量内存被锁定,其他应用或系统进程可能因为可用内存不足而性能下降或无法正常工作
  • 与 mlock 和 mlockall 的关系: MAP_LOCKED 的作用与 mlock 和 mlockall 系统调用类似,这些调用用于锁定特定内存区域或进程的所有内存。使用 MAP_LOCKED 可以在映射创建的同时立即锁定内存,而 mlock 和 mlockall 是在内存映射或分配后分别锁定内存的操作

图片无法显示,请右键点击新窗口打开图片

MAP_LOCKED 标志直接在 SYS_MMAP 系统调用即可,那么应用程序在分配虚拟内存时直接分配物理内存,并将物理内存锁在 RAM 里,不让其 SWAP OUT 到 SWAP Space,这种也属于预分配的一种. 无论是匿名内存或者文件映射内存,SYS_MMAP 系统调用 ARG3 带上 MAP_LOCKED 标志即可,那么系统可以直接锁住内存. 接下来分析 MAP_LOCKED 标志对内存行为的影响:

图片无法显示,请右键点击新窗口打开图片

在 SYS_MMAP 系统调用里使用 MAP_LOCKED 标志时,其代码逻辑如上图,内核在 TRANS VMA FLAGS 处将 MAP_LOCKED 标志转换成 VM_LOCKED, 那么 VMA 此时具有 VM_LOCKED 属性,接着内核在 CHECK LOCK-CAP 处调用 can_do_mlock 检测系统是否支持锁内存的功能,如果不支持那么进行报错. 检查完毕之后在 RLIMIT-CHECK 处检测系统是还有足够内存进行锁定,如果没有直接报错,如果有则继续执行分配, 最后在 POPULATE 处检测到 VM_LOCKED 标志,那么会通过 GUP 机制预分配物理内存. 其余环节与 MAP_LOCKED 标志无关,那么具体查看这几个代码细节:

图片无法显示,请右键点击新窗口打开图片

在 do_mmap 函数的 TRANS VMA FLAGS 处调用 calc_vm_flag_bits 函数,可以看到该函数直接将 MAP_LOCKED 标志转换成 VM_LOCKED 标志,那么这块虚拟内存具有 VM_LOCKED 属性. VM_LOCKED 属性会让其他内存管理行为识别,并做出相应的处理.

图片无法显示,请右键点击新窗口打开图片

在 do_mmap 函数的 CHECK LOCK-CAP 处调用 can_do_mlock 函数检测系统是否支持锁内存的功能,这里有两个开关用于控制锁内存的能力,首先是 42 行的 RLIMIT_MEMLOCK 方式控制锁内存行为,另外是 44 行的 CAP_IPC_LOCK 方式控制锁内存行为,如果两个方式都不支持锁内存功能,那么 SYS_MMAP 系统调用将失败.

# 查看当前 RLIMIT_MEMLOCK 的值
ulimit -l
# 设置 RLIMIT_MEMLOCK 的值
#   NR_PAGE: 按 PAGE_SIZE 的粒度进行设置
ulimit -l ${NR_PAGE}

# 开启 CAP_IPC_LOCK 能力
sudo setcap 'cap_ipc_lock=ep' /path/to/program

# 移除 CAP_IPC_LOCK 能力
sudo setcap -r  /path/to/program

以上两个操作可以控制系统是否支持锁内存的能力,另外 ulimit 还可以设置锁内存的数量,那么这么细粒度的内存管控,开发者可以根据实际需求进行管理,默认情况下系统是开启两个能力的.

图片无法显示,请右键点击新窗口打开图片

在 do_mmap 函数的 RLIMIT-CHECK 处调用 mlock_future_check 函数检测, 从代码逻辑可以知道进程已经锁主内存的数量存储在 mm->locked_vm 里,并且是按页进行统计的,那么 1327 行将这次要锁住的内存和进程已经锁住的内存相加之后存储在 locked 变量里,接着在 1328 行从 RLIMIT_MEMLOCK 读取系统运行的最大锁住内存的值,并存储在 lock_limit 里,如果此时要锁住的内存大于系统运行锁住的最大内存,且系统不支持 CAP_IPC_LOCK 功能,那么系统判定出不支持进程锁住这么多的内存,于是直接返回 EAGAIN,让 SYS_MMAP 系统调用进行多次尝试等待其他进程释放锁住的内存. 如果函数检测通过,那么说明内核支持进程锁住这么多的内存在 RAM 里. 以上便是 MAP_LOCKED 对 SYS_MMAP 的影响,那么接下来通过一个实践案例了解其使用,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Memory Mapping Mechanism  --->
          [*] Memory Mapping: FLAGS MAP_LOCKED --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-MMAP-MAP-LOCKED-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-MEMORY-MMAP-MAP-LOCKED-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,程序在 21 行调用 mmap 函数分配一段虚拟内存,并在 23 行使用了 MAP_LOCKED 标志,那么会预分配物理内存,并将物理内存锁定在 RAM 里,防止其被 SWAP OUT 到 SWAP Space. 函数接着在 30 行对虚拟内存进行访问,此时由于已经分配物理内存建立页表,因此不会触发缺页。函数接着在 34 行调用 madvise 函数发起 MADV_PAGEOUT 请求,请求将虚拟内存对应的物理页交换到 SWAP Space,这里可以验证是否可以锁住内存,如果锁住那么程序将打印 35 行的内容,最后就是回收内存.以上便是完整的实践案例,开发者可以使用内存流动工具查看整个内存流动的过程,例如在 21-25 前后加上内存流动开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在分配内存锁内存流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 can_do_mlock 函数的 42 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-MMAP-MAP-LOCKED-default/
# 编译内核
make kernel
# 运行实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本可以看到运行实践案例,可以看到应用程序成功的执行 mmap 分配到了虚拟内存,并且可以访问虚拟内存,但当通过 madvise 换出物理内存到 SWAP Space 时失败了,说明了锁住的内存无法被交换到 SWAP Space,最后看到内核打印的信息显示内存流动到 can_do_mlock 函数. 以上便是 MAP_LOCKED 标志对内存行为的影响,内核还有很多场景会被该标志影响,开发者可以根据上面讲述的方法,去发现更多的场景.

图片无法显示,请右键点击新窗口打开图片