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

在开启分页(Paging) 之后,用户进程只能看到自己的虚拟地址空间和内核空间,并且用户进程认为系统只有自己和内核线程在运行,无法看到其他进程,这样就形成了进程之间的资源隔离。但在有的场景,需要进程之间进行通行,其中一种方式就是通过共享内存实现.

共享内存(Shmem) 分类

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

在 Linux 中,共享内存是进程间通信(IPC) 的一种机制,允许多个进程访问和操作同一块物理内存区域。这种机制可以实现高效的数据交换和通信,因为数据不需要在进程之间进行复制,只需要直接在共享内存区域中进行读写. 在 Linux 中共享内存有以下几种实现方式:

  • System V 共享内存: 该方法基于早期 UNIX 系统的 IPC 机制, 使用 shmget、shmat、shmdt 等函数来创建、附加和操作共享内存
  • POSIX 共享内存: 比 System V 更现代的方法,基于 POSIX 标准。它使用 shm_open、mmap、munmap 等函数,与 System V 共享内存不同,POSIX 共享内存是基于文件的,通常会在 “/dev/shm” 目录中看到这些共享内存的文件表.
  • 同名共享内存: 通过 mmap 系统调用映射文件时,可以使用 MAP_SHARED 标志来创建一个可以被多个进程共享的内存映射。虽然这通常用于文件映射,但也可以用于进程间的共享内存.
  • 匿名共享内存: 通过 mmap 系统调用分配一段内存,内存带有 MAP_SHARED 和 MAP_ANONYMOUS 属性,该方法从系统公共的内存池子中分配一段内存充当共享内存.
  • Tmpfs/HugeTmpfs: tmpfs 是一个基于内存的文件系统,通常挂载在 “/dev/shm” 目录。实际上 tmpfs 文件系统是通过 shmem 子系统实现的,这也是一个共享内存机制。通过在 tmpfs 上创建文件,进程可以使用 mmap() 来映射这些文件并在进程之间共享数据.
  • DMA-BUF 共享内存: 外设之间共享 DMA 内存的机制,可以指向多个设置之间直接共享 DMA 内存而无需 CPU 介入.

虽然共享内存的实现方式不同,但它们的共同点都是可以在不同进程之间通过共享内存交换数据. 共享内存的优点是高效(无需数据复制)和低延迟。但它也带来了挑战,例如需要同步和协调进程以避免数据竞争和不一致,通常使用互斥锁、信号量或其他同步原语来实现。

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

相比匿名内存(Anonymous Memory)只能进程私有化使用,而共享内存(Shmem)通过将多个进程的虚拟内存映射到同一个物理内存上,并通过特定的同步手段实现资源的共享。匿名内存匿名共享内存虽然名字中带有匿名,但两种是不同类型的内存,匿名内存使用带 MAP_PRIVATE 和 MAP_ANONYMOUS 的 mmap 函数分配, 其通过匿名映射到一块物理内存上; 而匿名共享内存则使用带有 MAP_SHARED 和 MAP_ANONYMOUS 的 mmap 函数分配,且其通过文件映射到共享内存的伪文件上.

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

Linux 里分配共享内存的函数提供,其中以 mmap 系统调用最为常见,mmap 提供了很多参数可以灵活设置共享内核,具体如下:

  • ARG0: MAP_VADDR 可以设置共享内存的虚拟地址,该值为 NULL 是代表分配一个随机的虚拟地址,当该值为指定值时代表从指定的地址分配虚拟内存,该地址可能已经被分配出去,使用时调用者需要确保虚拟地址被覆盖.
  • ARG1: MAP_SIZE 指明了需要分配共享内存的大小,长度为 PAGE_SIZE 的倍数,不能为 0.
  • ARG2: 指明了共享内存的访问权限,其值可以是这些标志的合集: PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行) 和 PROT_NONE(不可访问)
  • ARG3: 指明映射类型,但采用匿名映射的共享内存,该字段必须包含是 MAP_SHARED 和 MAP_ANONYMOUS 标志. 如果采用文件映射的共享内存,该字段只能包括 MAP_SHARED, 不能包括 MAP_ANONYMOUS 标志,否则变成匿名内存.
  • ARG4: 映射文件描述符,对于匿名映射的共享内存该这段必须为 -1. 对于文件映射的共享内存该字段必须是文件描述符 fd.
  • ARG5: 指明映射的物理区域,由于共享内存映射的随机物理内存,因此该字段必须为 0.

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

在 Linux 里,共享内存的数据架构如上图,用户进程的虚拟区域(VMA) 设置为共享内存之后,其 vm_ops 设置为 shmem_vm_ops, 因此可以使用 vma_is_shmem() 判断是否为共享内存. 每个共享内存 VMA 的 vm_file 指向一个有名或者匿名的虚拟文件,虚拟文件对应的 struct inode,该 inode 是共享内存与文件系统和物理内存的桥梁,通过 i_sb 可以获得虚拟文件挂载点和 VFS 相关的信息,所有的虚拟内存都挂载 shm_mnt 下; 通过通过 mapping 获得虚拟文件与物理内存的映射关系,其使用 struct address_space 数据结构维护,其通过 i_pages 维护的 XARRAY 用于映射文件偏移与共享内存(PAGECACHE) 之间的关系,并通过 i_mmap 维护一颗区间树(特殊功能的红黑树),可以知道有多少进程的 VMA 映射到该虚拟文件. 另外每个共享物理内存的 mapping 指向了 MAPPING(struct address_space), 这样可以通过逆向映射知道共享物理内存上有哪些进程 VMA 映射.

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

在 Linux 系统里,共享内存的数量可以通过 free 命令等获得,例如上图当使用 free 命令时,shared 字段描述的正是当前系统使用共享内存的数量。另外 ‘/proc/meminfo’ 文件也提供了共享内存信息,其中 Shmem 字段描述系统正在使用的共享内存数量. 除了这些以外还有 ‘/proc/vmstat’ 文件也统计了共享内存页的信息,其中 nr_shmem 字段描述了当前系统共享内存占用物理页数量,即共享内存页的数量.

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

当调用 mmap 分配共享内存时,其在 mmap 系统调用处理逻辑如上图,与匿名映射不同的地方是 pgoff 设置为 0,在 mmap_region 函数内部,调用 shmem_kernel_file_setup() 函数为每个共享内存创建伪文件,虽然文件名都叫 “dev/zero”,但其 struct inode 都是 tmpfs 唯一提供的,另外还为共享内存的 VMA 实现了 vm_ops, 该接口提供了特定的缺页回调 shmem_fault, 也就是访问共享内存发送缺页之后,缺页异常处理函数最终会调用 shmem_fault 函数。mmap 系统调用最后几步和其他映射方式一样. 共享内存在 Linux 里有很多使用场景,比较经典的使用场景如下:

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


PreALLOC 方式分配共享内存

PreALLOC 也支持对共享内存的分配,共享内存在用户进程里使用很广,例如 IPC 和 SOCK-FD 共享等,PreALLOC 的特点可以让进程更快的访问虚拟内存。那么接下来通过一个实践案例介绍如何使用 PreALLOC 分配共享内存,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] USER PREALLOC: SHMEM Memory --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-VM-PREALLOC-SHMEM-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-VM-PREALLOC-SHMEM-default Source Code on Gitee

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

实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配虚拟内存,由于其使用了 MAP_SHARED 和 MAP_ANONYMOUS, 那么这段虚拟内存就是共享内存,另外还使用了 MAP_POPULATE 标志,那么意味着在分配虚拟内存的同时也会分配物理内存,并建立相应的页表映射. 函数接着在 31 行对虚拟内存进行写操作,但由于页表已经建立,因此此时不会触发缺页,最后测试完毕之后将资源进行回收,以上便是一个最简单的实践案案例,为了验证 31 行处没有发生缺页,可以在 31 行处添加 BS_DEBUG 开关:

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

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

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-VM-PREALLOC-SHMEM-default/
# 编译内核
make kernel
# 编程程序
make build

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

BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了运行实践案例所需的全部命令,可以看到应用程序运行之后,系统并没有打印缺页相关的信息,为了更好证明实践案例的可靠性,开发者可以将 mmap 函数的 MAP_POPULATE 标志去掉之后再实践:

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

通过对比实践可以看出,当 mmap 里去掉 MAP_POPULATE 标志之后,访问虚拟内存缺失引起了缺页异常,因此再次证明实践案例确实在分配虚拟内存的同时也分配了物理内存,并建立页表映射. 以上便是 PreALLOC 的一种使用场景.

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


LazyALLOC 方式分配共享内存

LazyALLOC 也支持对共享内存的分配,共享内存在用户进程里使用很广,例如 IPC 和 SOCK-FD 共享等,LazyALLOC 的特点是当进程访问虚拟内存时才会去分配物理内存。那么接下来通过一个实践案例介绍如何使用 LazyALLOC 分配共享内存,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] USER LAZYALLOC: SHMEM Memory --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-VM-LAZYALLOC-SHMEM-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-VM-LAZYALLOC-SHMEM-default Source Code on Gitee

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

实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配虚拟内存,由于其使用了 MAP_SHARED 和 MAP_ANONYMOUS, 那么这段虚拟内存就是共享内存,由于采用惰性分配,因此分配的时候只分配了虚拟内存. 函数接着在 31 行对虚拟内存进行写操作,但由于页表没有建立,因此此时会触发缺页,最后测试完毕之后将资源进行回收,以上便是一个最简单的实践案案例,为了验证 31 行处发生缺页,可以在 31 行处添加 BS_DEBUG 开关:

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

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

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-VM-LAZYALLOC-SHMEM-default/
# 编译内核
make kernel
# 编程程序
make build

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

BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了运行实践案例所需的全部命令,可以看到应用程序运行之后,系统并打印缺页相关的信息,说明进程在分配虚拟内存的时候仅仅分配了虚拟内存. 以上便是 LazyAlloc 的一种使用场景.

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