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

BiscuitOS 内存管理之分页大专题订阅入口

目录

  • 共享内存(Shmem) 分类

  • 共享内存缺页场景汇总

  • 共享内存缺页场景

    • 可读可写共享内存(R/W Shmem) 缺页场景

    • 只读共享内存(RO Shmem) 缺页场景

    • 共享内存 Fork 缺页场景

    • 共享内存换入换出(SWAP) 缺页场景

    • 共享内存内存压缩(ZSWAP) 缺页场景

    • 共享内存发生 MCE(UE) 缺页场景

    • 共享内存 SYSV 缺页场景

    • 共享内存 POSIX 缺页场景

    • 共享内存 UNIX-SOCK 缺页场景

    • 共享内存 MEMFD 缺页场景

    • 共享内存 DEV-SHM 缺页场景

    • 共享内存 LOCAL 缺页场景

    • 共享内存 HugeShmem(THP) 缺页场景

    • 共享内存 Protection Key 缺页场景

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


共享内存(Shmem) 与 PageFault

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

在开启分页(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 里,共享内存的数据架构如上图,用户进程的虚拟区域(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 字段描述了当前系统共享内存占用物理页数量,即共享内存页的数量.

共享内存缺页场景汇总

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

共享内存发生缺页的场景是: 当 CPU 访问了未建立页表的共享内存,MMU 检查到物理内存不存在即触发缺页异常. 但在 Linux 里还有很多场景会导致共享内存发生缺页,总结包括如下场景:

  • 可读可写共享内存场景: 用户进程访问一段没有建立页表的共享内存,MMU 检查到物理内存不存在而触发缺页异常,经过缺页异常处理函数分配物理内存建立页表之后,进程可以再次正常访问共享内存.
  • 只读共享内存场景: 用户进程读取一段未建立页表的共享内存,MMU 检查到物理内存不存在并触发缺页异常,经过缺页异常处理函数分配物理内存建立页表之后,进程可以再次读取共享内存。当进程对只读共享内存发起写操作,MMU 检查到权限异常并触发缺页异常,缺页异常处理函数发现是权限异常,发送 SIG_BUS 让程序异常退出(SegmentFault).
  • System V(SYSV) 共享内存场景: 当两个进程使用 SYSV 机制共享内存时,其中一个进程访问共享内存,此时共享内存并没有建立页表,因此 MMU 检查到物理内存不存在而触发缺页,缺页异常处理函数分配物理内存并建立页表之后,进程可以正常访问。其他进程访问共享内存时,由于虚拟内存还未与共享内存建立页表,于是 MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数找到共享内存并建立页表,那么进程后续可以正常访问共享内存.
  • MEMFD 共享内存场景: 当进程分配了一段匿名共享内存,其通过 MEMFD 获得匿名内存对应的伪文件描述符,然后与 UNIX-SOCK 其他进程实现共享这段匿名共享内存. 由于这段共享内存还没有建立页表,那么进程 A 访问这段共享内存时,MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数分配物理内存并建立页表,那么进程 A 可以继续访问这段共享内存. 其他进程访问这段共享内存时,同样 MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数找到对应的物理内存并建立页表,那么进程可以访问这段共享内存.
  • POSIX 共享内存场景: 进程之间通过 POSIX 机制访问同名文件共享内存,POSIX 机制会在 “/dev/shm” 目录下创建同名伪文件,进程 A 访问这段共享内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数分配物理内存并建立页表映射,那么进程 A 可以正常访问共享内存,而其他进程由于没有与共享内存建立页表,因此首次访问时 MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数找到共享内存并建立页表,那么其他进程可以访问共享内存.
  • SWAP 场景: 进程分配一段虚拟内存并与物理内存建立页表,那么进程可以正常访问共享内存,但是由于系统内存资源紧缺,系统将不常使用的共享内存交换到 SWAP Space,随即将共享内存对应页表清空. 当进程再次访问共享内存时,MMU 发现物理内存不存在并触发缺页异常,缺页异常处理函数将共享内存换入到系统物理内存并更新页表,那么进程可以继续访问共享内存.
  • ZSWAP 场景: 进程分配一段虚拟内存并与物理内存建立页表,那么进程可以正常访问共享内存,但是由于系统内存资源紧缺,系统将不常使用的共享内存进行压缩,随即将共享内存对应页表清空. 当进程再次访问共享内存时,MMU 发现物理内存不存在并触发缺页异常,缺页异常处理函数将共享内存解压到系统物理内存并更新页表,那么进程可以继续访问共享内存.
  • FORK 场景: 父进程分配一段共享内存并建立页表,父进程可以正常访问共享内存,父进程调用 fork 生成子进程时,并没有将共享内存对应的页表复制到子进程,于是父进程可以继续访问共享内存,而子进程访问共享内存时,由于共享内存没有建立页表,那么 MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数找到共享内存并建立页表,那么子进程可以正常访问共享内存.
  • 同名文件共享场景: 多个进程将同一个文件映射到各自的地址空间,那么多个进程可以通过共享 PAGECACHE 实现文件共享,此时 PAGECACHE 就是共享内存. 当进程 A 首次访问共享内存,MMU 发现物理内存不存在而触发缺页,缺页异常处理函数分配物理内存并建立页表,那么进程 A 可以访问共享内存; 当其他进程首次访问共享内存时,MMU 同样发现物理内存不存在而触发缺页异常,缺页异常处理函数找到共享内存并建立页表,那么其他进程可以正常访问共享内存.
  • Unix-SOCK 共享场景: UNIX-SOCK 机制运行进程之间共享文件描述符,那么对于进程打开的文件描述符可以和其他进程实现共享,进程将文件映射到地址空间,那么可以实现共享内存,这里的共享内存可以是匿名共享内存,也可以是 PAGECACHE. 当进程 A 访问共享内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数分配物理页并建立页表,那么进程 A 可以访问共享内存; 同理其他进程首次访问共享内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数找到共享内存并建立页表, 那么其他进程可以访问共享内存.
  • TMPFS 共享内存场景: tmpfs 基于内存的文件系统,进程可以采用本地的 tmpfs 文件系统使用共享内存,进程通过同名文件方式将文件映射到进程地址空间形成共享内存,此时共享内存和 PAGECACHE 等价。进程 A 首次访问共享内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数分配物理内存并建立页表,进程 A 可以继续访问共享内存; 同理其他进程访问首次访问共享内存时,MMU 发现物理内存并不存在而触发缺页异常,缺页异常处理函数找到共享内存并建立页表,最后其他进程可以访问共享内存.
  • HugeTmpfs 共享内存场景: huge-tmpfs 也是基于内存的文件系统,只是共享内存的大小为 2MiB,其行为与 tmpfs 类似。当进程通过同名文件方式将文件映射到地址空间,进程 A 首次访问共享内存,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数分配 2MiB 物理页并建立 PMD 页表,然后进程可以正常访问共享内存; 同理其他进程访问共享内存时,MMU 发现共享内存并不存在而触发缺页,缺页异常处理函数找到共享内存并建立页表,那么其他进程可以访问共享内存.
  • MCE(UE) 场景: 当进程分配一块共享内存,并可以正常对共享内存访问,此时内存发生硬件故障导致内存 UE 产生,MCE 异常处理函数将共享内存标记为有毒并对其页表进行有毒标记. 当进程再次访问共享内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数根据有毒页表内容知道 UE 发生,于是向 SIG_BUS 让进程异常退出.
  • DEVSHM 场景: 系统 “/dev/shm” 目录下为共享内存池子,里面的每个伪文件对应一块共享内存,可以将伪文件映射到进程地址空间实现共享,当进程 A 首次访问共享内存时,MMU 发现物理内存不存在而触发缺页异常,缺页异常处理函数分配物理内存并建立页表,进程 A 可以正常访问共享内存; 其他进程首次访问共享内存时,MMU 同样发现物理内存不存在而触发缺页异常,缺页异常处理函数找到共享内存并建立页表,那么其他进程可以访问共享内存.
  • Protection Key 场景: 进程分配了一段共享内存,并可以正常访问共享内存。由于进程采用 Protection Key 机制管理共享内存权限访问,此时将进程修改为无权访问共享内存,那么当进程再次访问共享内存时,MMU 检查到权限异常而触发缺页异常,缺页异常处理函数检查到因为 Protection Key 缺页,那么发送 SIG_BUS 导致进程异常退出.

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

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

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

当访问共享内存发生缺页时,缺页异常处理函数流程如上图,handle_pte_fault 函数对于共享内存选择 do_fault 函数,但在 do_fault 函数内部,由于触发缺页的原因不同会有不同的分支,例如 FP_READ 即异常原因包括读操作,那么选择 do_read_fault 函数处理剩下的逻辑; 反之 PF_WRITE,即异常原因包括写操作,那么选择 do_shared_fault 函数处理剩下的逻辑; do_shared_fault 和 do_read_fault 的处理逻辑差不多,只是 do_shared_fault 多了一个将页表 Write 标志位置位的操作. 在 do_shared_fault/do_read_fault 函数里,函数首先调用 vma->vm_ops->fault 指向的回调函数,对于共享内存回调函数为 shmem_fault,shmem_fault 函数的主要任务从 SHMEM 中分配一个物理页,接着调用 finish_fault,该函数主要目的是构造页表内容并更新页表,然后更新物理页的逆向映射和系统对共享内存的统计. 通过上面的分析,可以知道共享内存缺页存在两种场景:

  • 可读可写共享内存(R/W Shmem) 缺页场景

  • 只读共享内存(RO Shmem) 缺页场景

  • 共享内存 Fork 缺页场景

  • 共享内存换入换出(SWAP) 缺页场景

  • 共享内存内存压缩(ZSWAP) 缺页场景

  • 共享内存发生 MCE(UE) 缺页场景

  • 共享内存 SYSV 缺页场景

  • 共享内存 POSIX 缺页场景

  • 共享内存 UNIX-SOCK 缺页场景

  • 共享内存 MEMFD 缺页场景

  • 共享内存 DEV-SHM 缺页场景

  • 共享内存 LOCAL 缺页场景

  • 共享内存 HugeShmem(THP) 缺页场景

  • 共享内存 Protection Key 缺页场景

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


共享内存 POSIX 缺页场景

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

在 Linux 中,POSIX 共享内存是一种用于实现进程间通信(IPC)的机制,允许多个进程共享一个内存区域,从而实现数据共享。与 SystemV 共享内存不同,POSIX 共享内存是基于 POSIX 标准定义的,它具有一些与 SystemV 共享内存不同的特性和优点:

  • 基于文件系统路径: 与 SystemV 共享内存使用键值标识不同,POSIX 共享内存使用文件系统路径来标识共享内存对象。这意味着进程可以通过文件路径来访问和共享数据,而不需要预先协商一个键值
  • 动态创建和销毁: POSIX 共享内存对象可以在运行时动态创建和销毁,而无需在启动时预先创建, 这使得它更加灵活,不需要在多个进程之间协调共享内存的创建
  • 内存映射: POSIX 共享内存通常使用 mmap 函数将共享内存对象映射到进程的地址空间中,从而使进程可以直接访问共享内存中的数据。这与 SystemV 共享内存使用 shmat 函数连接到内存的方式不同
  • 文件权限控制: 由于 POSIX 共享内存对象是基于文件系统路径的,因此可以使用标准的文件权限控制机制(如文件权限位和访问控制列表)来控制对共享内存的访问
  • 无需专用 IPC 函数: POSIX 共享内存不需要专门的 IPC 函数来创建、连接或删除共享内存对象. 相反,它使用标准的文件操作和内存映射函数来实现这些操作
  • 跨平台性: POSIX 共享内存是跨平台的,因为它是 POSIX 标准的一部分,可以在支持 POSIX 标准的各种 UNIX 和 UNIX-like 操作系统上使用

这个进程在使用 POSIX 共享内存时,只需使用 shm_open 函数打开指定的 POSIX 共享文件名字即可,POSIX 机制会在 “/dev/shm” 目录下创建该文件,然后进程使用 mmap 函数将 POSIX 共享文件映射到进程的地址空间,无需使用 IPC 函数即可实现多个进程之间使用共享内存. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:

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

BiscuitOS-PAGING-PF-SHMEM-POSIX-default Source Code on Gitee

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

实践案例由两个应用程序组成,上图为 SERVER 端的应用程序,其在 25 行调用 shm_open 函数打开共享内存文件 “BiscuitOS.men”,然后在 32 行将共享内存文件大小设置为 MAP_SIZE, 接着在 35 行调用 mmap 函数将共享内存映射到进程的地址空间,程序在 45 行对共享内存写操作,并在 47 行对共享内存进行读操作,最后在释放共享内存.

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

上图为 CLIENT 端的应用程序,其在 25 行调用 shm_open 函数打开共享内存文件 “BiscuitOS.men”,接着在 31 行调用 mmap 函数将共享内存映射到进程的地址空间,程序在 41 行对共享内存读操作,最后在释放共享内存. 为了可以看到内存在缺页异常里的流动,在 SERVER 程序 45 行前后加上 BS_DEBUG 开关:

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

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

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-POSIX-default/
# 编译内核
make kernel
# 编译实践案例
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “POSIX SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 POSIX 的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.

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

对于可读可写 POSIX 共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.

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

do_shared_fault 函数作为可读可写 POSIX 共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 Xarray 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.

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

BiscuitOS 内存管理之分页大专题订阅入口

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