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

在 Linux 系统中,MAP_SHARED 是用于 mmap 系统调用的一个标志, 用于创建一个内存映射区域,该区域与其他所有映射了同一文件或设备的进程共享。使用 MAP_SHARED 创建的映射具有以下特点和用途:

  • 共享内存映射: 当多个进程映射同一个文件或设备,并指定 MAP_SHARED 标志时,它们可以共享同一物理内存区域, 任何一个进程对映射区域的修改都会反映到其他所有共享这一区域的进程中.
  • 写回文件: 对于 MAP_SHARED 映射的文件,当内存被修改后,变更通常会被写回(或最终一致地写回)到原始文件中. 这意味着这种类型的映射不仅在进程间共享,并且其修改会持久化到磁盘文件中
  • 进程间通信: MAP_SHARED 映射可以用作进程间通信的机制, 由于映射区域在所有映射了相同文件或设备的进程间共享,因此它提供了一种共享数据和协作的方式
  • 数据库和文件系统: 许多需要高效访问大型数据集的应用程序,如数据库和文件系统,可能会利用 MAP_SHARED 映射来提高性能和数据共享的效率
  • 与 MAP_PRIVATE 的对比: 与 MAP_SHARED 相对的是 MAP_PRIVATE,后者创建的映射是私有的,进程对映射的修改不会影响到其他进程,也不会写回到底层文件中, MAP_SHARED 和 MAP_PRIVATE 的选择取决于你希望如何共享和持久化映射数据

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

为了全面研究 MAP_SHARED 标志对内存的影响,文章将从文件映射和匿名映射的角度分别对 MAP_SHARED 标志进行详细分析。SYS_MMAP 系统调用的 ARG3 带有 MAP_SHARED 标志映射的区域可以和其他进程共享,其特点是: 进程只要对共享内存写操作,那么其他共享的进程就可以读到修改之后的数据. MAP_SHARED 共享映射和 MAP_PRIVATE 私有映射相对立的,因此两个标志不能同时出现,当 SYS_MMAP 系统调用的 ARG3 没有显示设置 MAP_PRIVATE/MAP_SHARED 标志时,系统调用默认使用 MAP_PRIVATE. 那么接下来分别从匿名映射和文件映射角度来分析 MAP_SHARED.

MAP_SHARED with 匿名映射

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

匿名映射是没有关联任何文件的映射方式,这里的需要和匿名内存进行区分,MAP_SHARED 和 MAP_ANONYMOUS 标志共同使用时,需要采用的匿名映射,但这里并不是真正意义上的匿名映射,因此这是分配共享内存的一种方式. 虽然应用程序通过 SYS_MMAP 系统调用显示的使用了 MAP_ANONYMOUS 标志,但内核识别到其还包含 MAP_SHARED 标志时,认为是通过匿名方式分配共享内存,因此也会为虚拟内存分配一个伪文件(Pseudo File), 并与虚拟内存关联在一起,以此分配共享内存,这样多个进程可以共享使用.

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

MAP_SHARED 和 MAP_ANONYMOUS 标志配合使用可以分配共享内存,并且内核为其分配一个伪文件,那么这段虚拟内存在内核里的数据架构就和文件映射的一致,那么回到之前我为什么说使用 MAP_ANONYMOUS 匿名映射的虚拟内存只有配合 MAP_PRIVATE 标志时才是真正的匿名映射,其余的虚拟内存都是文件映射内存.

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

当使用 SYS_MMAP 系统调用建立匿名共享内存时,那么可以在 SYS_MMAP 的 ARG3 给定 MAP_SHARED 和 MAP_ANONYMOUS 参数,那么可以分配共享内存. 当分配了共享内存之后,不同的操作会导致不同的内存行为出现,那么可能会出现以下几种:

  • 首次读/写操作: 当分配共享内存之后,由于只有分配了虚拟内存部分,那么用户进程对虚拟内存首次发起读或者写操作时,MMU 会因为没有映射物理页而触发缺页,缺页异常处理会为共享内存建立文件映射架构,并分配 SHM 里分配物理内存,最后建立页表使虚拟内存映射到新分配的物理内存上,那么进程可以正常读写共享内存.
  • FORK 多进程共享: 当进程分配一段匿名的共享内存之后,其使用 FORK 创建子进程之后,子进程会与父进程共享这段内存,那么任一进程的写操作都会被其他共享的进程看到.

综合来看,MAP_SHARED 标识的核心作用是保证数据在多个进程之间共享. 另外 MAP_SHARED 标志只有在多进程共享的场景才能最大限度发挥作用,另外对于共享内存,其也需要一定的逆向映射知道其被哪些进程共享,这样在处理类似 SWAP、MIGRATE 操作时才能更好的处理复杂的映射问题.

MAP_SHARED(匿名映射) 创建

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

当用户进程调用 SYS_MMAP 系统调用分配共享内存时,其使用 MAP_SHARED 和 MAP_ANONYMOUS 两个标志,SYS_MMAP 系统调用在处理这个场景时的代码流程如上图,其中 VM-SHARED FLAGS 处根据 MAP_SHARED 标志为 VMA 添加 VM_SHARED 和 VM_MAYSHARE 标志,然后在 ALLOC PSEUDO-FILE 处根据 MAP_SHARED 标志为虚拟内存分配并关联伪文件, 最后在 REVERSE-TREE 处建立共享内存的逆向映射. SYS_MMAP 系统调用对 MAP_SHARED 调用并没有太多特殊处理, 接下来查看具体的代码细节:

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

在 do_mmap 函数的 VM-SHARED FLAGS 处存在如上逻辑,其判断映射不是文件映射情况下,并在 1508 行检查到用户进程传入了 MAP_SHARED 标志之后,进入 1509 行分支,此时如果检查到 VMA FLAGS 里包含了 VM_GROWSDOWN 或者 VM_GROWSUP 标志,那么说明共享内存不允许具有堆栈向上或者向下的属性,否则直接返回 EINVAL. 函数在 1514 行将 pgoff 设置为 0,因此推测 vm_pgoff 标志在共享内存里并不重要. 函数在 1515 行向 VMA FLAGS 里添加了 VM_SHARED 和 VM_MAYSHARE 标志.

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

在 mmap_region 函数的 ALLOC PSEUDO-FILE 处识别到 VMA 没有关联文件,但 VMA 包含 VM_SHARED 标志,那么此时识别出虚拟内存为共享内存,因此调用 shmem_zero_setup 函数为其创建一个伪文件. 函数首先在 4229 行计算出文件映射虚拟内存大大小,接着调用 shmem_kernel_file_setup 函数从 TMPFS 文件系统里创建一个名为 “dev/zero” 的伪文件,这里值得注意是文件名不是 “/dev/zero”, 以免混淆. 文件创建完毕与 VMA 进行关联,然后将 VMA 的 vm_ops 设置为 shmem_vm_ops, 里面重点提供了 fault 的实现,即虚拟内存发生缺页时,缺页异常处理函数最终会调用 fault 对应的回调函数.

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

在 mmap_region 函数里创建完 VMA 之后,接下来将 VMA 加入到进程的地址空间区间树和双向链表里,以及在 REVERSE-TREE 处调用 __vma_link_file 函数设置共享内存的逆向映射,函数在 633 行调用 vma_interval_tree_insert 函数将 VMA 添加到 vm_file 对应的 ADDRESS_SPACE 逆向映射区间树. 以上便是 SYS_MMAP 对标记为 MAP_SHARED 的逆向映射虚拟内存的处理,可以看到 SYS_MMAP 系统调用将其识别为共享内存,并以文件映射的方式架构该虚拟内存在内核里的逻辑.

MAP_SHARED(匿名映射) 缺页

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

当用户进程首次访问匿名共享内存时(此时 SYS_MMAP 系统调用已经将匿名共享内存识别为共享内存),由于其并未映射具体的物理内存,那么会触发缺页异常,在缺页异常处理函数里,匿名共享内存的处理逻辑如上图,在 DETECT ANON-MEMORY 处调用 vma_is_anonymous 函数识别出缺页的虚拟内存不是匿名内存,然后进入 do_fault 进行处理,此时通过 VM_SHARED 标志识别出是匿名共享内存,那么进入 do_shared_fault 函数进行处理. 此时虚拟内存类型已经识别出来,那么内核接下来只需完成两个事情,即分配物理内存和建立页表,于是缺页异常处理函数在 ALLOC-PAGE 处调用 shmem_fault 函数从系统共享内存池子里分配共享内存,然后在 SET-PAGETABLE 处调用 finish_fault 函数完成页表的填充构建和映射操作,那么处理完毕之后,进程可以正常读写共享内存.

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

在 do_fault 函数时已经识别处虚拟内存不是匿名内存,那么函数在 4652 行通过 VMF flags 里包含了 FAULT_FLAG_WRITE,因此此时是因为写发生的缺页,因此这里条件不满足,那么在 4654 行由于 VMA 在 SYS_MMAP 系统调用里已经包含了 VM_SHARED 标志,因此该条件也不满足,最后匿名共享内存进入 do_shared_fault 行分支进行处理.

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

在 __do_fault 函数里,函数的主要作用是分配物理内存,那么函数在 4173 行调用虚拟内存关联文件提供的 fault 回调函数,在 SYS_MMAP 系统调用里,内核为匿名共享内存提供了 shmem_vm_ops VMOPS,其中 fault 回调对应的是 shmem_fault,该函数从系统共享内存池子里分配一个物理页,并将 vmf->page 指向新分配的物理页.

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

当分配好物理页之后,最后的任务就是建立页表映射,在 SET-PAGETABLE 处调用 do_set_pte 函数建立页表,函数在 4311-4318 行对 MAP_SHARED 标志的内存进行区别处理,在 4311 行检测到 write 为真且 VMA FLAGS 里不包含 VM_SHARED, 那么说明是私有映射的情况,这里显然不满足,因为 VMA FLAGS 里早已经包含了 VM_SHARED, 其来自 MAP_SHARED, 因此函数进入 4316 行分支,此时调用 page_add_file_rmap 函数建立文件映射的逆向映射,因此可以再次证明 MAP_SHARED 和 MAP_ANONYMOUS 组合请求最终分配的是文件映射的匿名映射共享内存,这句话是不是很绕口,但可以知道 MAP_SHARED 标志分配的内存都是文件映射的,并且可以在多个进程之间共享,那么接下来继续分析如何实现多进程之间的共享.

MAP_SHARED(匿名映射) FORK

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

SYS_FORK 系统调用是最能体现 MAP_SHARED 的重要性,承接上面的分析,MAP_SHARED 和 MAP_ANONYMOUS 分配的虚拟内存被设置为匿名共享内存,既然是共享内存那么就可以在多个进程之间共享. 由于此时共享内存是匿名映射的,无法直接通过常用的进程间通行手段进行共享,但可以使用 FORK 系统调用实现父子进程之间的共享,FORK 系统调用在创建子进程时,会将父进程的虚拟内存拷贝到子进程地址空间里,对于父进程的共享内存,那么父子进程可以共同映射,实现数据的共享.

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

SYS_FORK 的实现逻辑如上图,其在 CREATE CHILD-VMA 处创建子进程的 VMA,然后在 ADD FILE-RMAP 处识别出需要复制的父进程 VMA 是共享内存,那么其属于文件映射的一种,因此在 FORK 的时候需要将子进程的 VMA 添加到文件对应的逆向映射区间树里,这样才好管理文件映射物理页的逆向映射关系. 由于文件映射有自己的管理逻辑,其不可以在页表映射之外维护一套正向映射的关系,因此子进程的 VMA 无需复制父进程的页表以及虚拟内存的内容,这样可以大大加快 FORK 的速度,然后子进程在真正访问该共享内存时才真正建立页表映射, 映射到父进程的共享内存上,实现父子进程之间的数据共享. 回到本小节的重点,这里将研究 MAP_PRIVATE 对 SYS_FORK 的影响,具体如下:

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

在 dup_mmap 函数里,函数在 661 行获得 VMA 关联的文件,此时对于匿名映射的共享内存来说文件是存在的,因此进入 663 行分支进行处理,并且在 671 行调用 vma_interval_tree_insert_after 函数将子进程的 VMA 添加到文件关联的逆向映射区间树里. 做了以上操作之后,FORK 系统调用没有做更多的操作,连页表都没有复制,那么对于子进程的共享内存来说,其虚拟内存的页表是空的,因此只能通过子进程访问共享内存引发缺页实现,然后缺页异常处理函数会根据文件映射找到父进程的共享内存,缺页部分的逻辑和前面分析的是一致的,只是少了分配物理页的环节,其余均一致. 处理完毕之后,父子进程将页表都映射到同一个物理页上了,那么接下来就是父子进程之间的数据共享。为了更好的说明上述过程,接下来将通过一个实践案例分析介绍匿名映射共享内存的使用,以及与其他进程之间共享内存的过程, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

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

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

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

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

实践案例由一个应用程序构成,程序在 22 行调用 mmap 函数分配一块虚拟内存,并在 24 行使用 MAP_SHARED 和 MAP_ANONYMOUS 标志,那么分配的虚拟内存是匿名的共享内存,然后在 31 行对匿名共享内存首次发起写操作,此时会触发缺页将虚拟内存映射到新的物理页上,那么进程的写操作在数据写入到共享内存上. 函数接着在 35 行使用 fork 系统调用,那么此时父子进程会共享共享内存,接着在 39 行子进程首先对共享内存发起写操作,此时由于会触发缺页异常,缺页异常处理函数会通过虚拟内存关联的文件找到父进程的共享内存,并将页表也映射到共享内存上,那么父子进程之间可以共享数据,因此 39 行的写入到父进程的共享内存里. 父进程在睡眠 1s 之后在 46 行对共享内存发起读操作,此时读到的数据是子进程刚刚写入的数据,那么可以证明父子进程之间实现了数据共享. 以上便是完整的实践过程,开发者可以使用内存流动工具查看整个流程,流入查看子进程虚拟内存建立页表的过程,在 39 >行前后加上 BS_DEBUG 开关:

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

接着在匿名共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4579 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

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

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

当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本可以看到运行实践案例,可以先打印匿名共享内存刚写入的字符 ‘B’, 当子进程写入 ‘D’ 之后,父进程读到 ‘D’ 字符,那么证明了父子进程之间实现了数据共享. 以上便是 MAP_SHARED 对内存行为的影响,其还在很多方面有影响,开发者可以更加之前的分析方法在更多的场景加深对 MAP_SHARED 标志的理解和学习.

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