目录
共享内存缺页场景
共享内存(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) 缺页场景
当使用 mmap 分配一段带 PROT_READ 和 PROT_WRITE 的共享内存时,共享内存便是可读可写的,并且当进程首次对可读可写共享内存进行读或者写操作时,MMU 都会硬件检查不到物理内存而直接触发缺页异常. 在缺页异常处理函数里,由于共享内存底层都是基于 tmpfs 文件系统构建,那么每个共享内存 VMA 都对应一个伪文件,如下图:
缺页异常处理函数首先找到 VMA 对应的 vm_file, 此时文件是一个没有后端映射的伪文件,那么通过伪文件可以找到对应的 inode, inode 里面有一个关键的成员 address_space, 该数据结构维护的文件与共享内存之间的映射关系。缺页异常处理函数首先分配一个新的物理页,然后将其添加到 address_space 对应的 XARRAY 里,形成文件偏移与物理内存一一映射关系,另外 address_space 对应的区间树 i_mmap, 在 mmap 阶段就将 VMA 插入到该区间树,形成文件偏移与 VMA 的逆向映射关系,那么将每个新分配的物理页 mapping 指向 address_space, 这样结合区间树就构建里物理页的逆向映射,知道有哪些进程的 VMA 映射到该物理页上,最后缺页异常处理函数填充页表映射到最新的物理页上,待缺页异常处理函数返回之后,进程可以正常访问共享内存. 无论是读还是写流程一致. 那么接下来通过一个实践案例了解这种异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 和 PROT_WRITE 属性实现可读可写的共享内存,然后在 32 行对共享内存进行写操作,并在 34 行再次对共享内存进行读操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 32 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 32 行前后加上 BS_DEBUG 开关:
接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “R/W Share Memory on do_shared_fault 0x6000000000”, 那么说明实践案例分配了可读可写共享内存,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对于可读可写共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.
do_shared_fault 函数作为可读可写共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 XARRY 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.
只读共享内存(RO Shmem) 缺页场景
当使用 mmap 分配一段仅带 PROT_READ 的只读共享内存时,并且当进程首次对只读共享内存进行读操作时,MMU 都会硬件检查不到物理内存而直接触发缺页异常. 在缺页异常处理函数会为其分配新的物理页,并建立页表映射到新的物理页上,那么进程可以正常读共享内存。但是如果进程对只读共享内存发起写操作,无论只读共享内存是否已经建立页表,MMU 都会因为权限异常而再次触发缺页异常,此时 MMU 检查到对只读内存发起写操作,于是发送 SIG_BUS 让进程异常退出. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on RO(Read-Only) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-RO-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 属性实现只读的共享内存,然后在 32 行对共享内存进行读操作,并在 34 行再次对共享内存进行读操作,接下来在 38 行对只读共享内存进行写操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 32 行读操作和 38 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 38 行前后加上 BS_DEBUG 开关:
接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_user_addr_fault 函数的 1381 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-RO-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “RO Shared Memory on do_user_addr_fault 0x6000000000”, 那么说明实践案例对只读共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对只读共享内存进行写操作引起的缺页,缺页异常处理流程如上图,其在找到对应的 VMA 之后,调用 access_error 函数进行权限检查,此时检查到缺页权限异常,那么直接调用 bad_area_access_error 发送 SIG_BUS,导致程序异常退出.
access_error 权限检查的逻辑如上图,1086 行如果检查到是因为 Protection Key 权限不足的缺页直接 SIG_BUS 终结进程; 1098 行检查到缺页原因是 X86_PF_SGX 之后也直接退出; 1110 行检查到缺页原因包括 PF_WRITE,即缺页的时候有写操作,但是接着检查到 VMA 没有写权限,那么直接返回 1,致使发送 SIG_BUS 导致程序异常退出.
共享内存 Fork 缺页场景
当父进程使用 mmap 分配了一段可读可写的共享内存,并且共享内存已经建立页表,因此父进程可以正常访问共享内存。当父进程使用 fork 创建子进程时,fork 系统调用会父进程地址空间的 VMA 拷贝到子进程的地址空间,但在拷贝时只拷贝 VMA,并未拷贝页表,这样做的目的是加速子进程创建的速度.
从 fork 系统调用可以看到,当 fork 拷贝父进程地址空间的 VMA 到子进程的地址空间时,共享内存并不像匿名内存一样调用 copy_page_range 拷贝父进程的页表内容,共享内存直接不拷贝父进程的页表,这样做的好处是可以加速子进程的创建速度,缺点是子进程由于没有映射共享内存的页表,因此需要缺页一次才能映射共享内存,那么子进程首次访问共享内存是会触发缺页的. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on Fork --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-FORK-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,父进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 和 PROT_WRITE 属性实现可读可写的共享内存,然后在 32 行对共享内存进行写操作,并在 34 行再次对共享内存进行读操作,接下来在 37 行调用 fork 函数创建子进程,然后子进程在 39 行对共享内存进行写操作,而父进程在 43 行对共享内存也执行写操作,只是时间上落后于子进程,最后父子进程在 47 行再次对共享内存进行读操作. 以上便是一个最基础的实践案例,可以知道 32 行读操作和 39 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 39 行前后加上 BS_DEBUG 开关:
接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-FORK-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “FORK Shared Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 FORK 之后的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
子进程首次对共享内存执行写操作引起的缺页,去也异常处理流程如上图,其调用 do_shared_fault 函数进行共享内存写操作处理,其核心是调用 __filemap_get_folio 从父进程共享内存对应文件找到对应的物理页,与可读可写共享内存缺页场景对比,其是分配一个新的物理内存,而子进程场景只是找到父进程的共享内存对应的物理页即可,无需分配新的物理内存. 最后就是填充子进程对应的页表,那么进程就可以对共享内存进行访问.
对于查找共享内存对应物理页的逻辑简单归结为: 父子进程的 VMA 对应同一个文件(伪文件), 进而可以找到唯一的 inode, inode 维护了 address_space,该数据结构维护的 XARRAY 可以通过文件偏移找到对应的物理页,因此子进程只要提供虚拟地址,缺页异常处理函数就可以基于这个逻辑将虚拟地址转换成文件偏移,即可在 XARRAY 里找到对应的物理页,那么接下来只要更新一下页表,子进程即可访问共享内存.
共享内存 Fork 缺页场景
当父进程使用 mmap 分配了一段可读可写的共享内存,并且共享内存已经建立页表,因此父进程可以正常访问共享内存。当父进程使用 fork 创建子进程时,fork 系统调用会父进程地址空间的 VMA 拷贝到子进程的地址空间,但在拷贝时只拷贝 VMA,并未拷贝页表,这样做的目的是加速子进程创建的速度.
从 fork 系统调用可以看到,当 fork 拷贝父进程地址空间的 VMA 到子进程的地址空间时,共享内存并不像匿名内存一样调用 copy_page_range 拷贝父进程的页表内容,共享内存直接不拷贝父进程的页表,这样做的好处是可以加速子进程的创建速度,缺点是子进程由于没有映射共享内存的页表,因此需要缺页一次才能映射共享内存,那么子进程首次访问共享内存是会触发缺页的. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on Fork --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-FORK-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,父进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 和 PROT_WRITE 属性实现可读可写的共享内存,然后在 32 行对共享内存进行写操作,并在 34 行再次对共享内存进行读操作,接下来在 37 行调用 fork 函数创建子进程,然后子进程在 39 行对共享内存进行写操作,而父进程在 43 行对共享内存也执行写操作,只是时间上落后于子进程,最后父子进程在 47 行再次对共享内存进行读操作. 以上便是一个最基础的实践案例,可以知道 32 行读操作和 39 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 39 行前后加上 BS_DEBUG 开关:
接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-FORK-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “FORK Shared Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 FORK 之后的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
子进程首次对共享内存执行写操作引起的缺页,去也异常处理流程如上图,其调用 do_shared_fault 函数进行共享内存写操作处理,其核心是调用 __filemap_get_folio 从父进程共享内存对应文件找到对应的物理页,与可读可写共享内存缺页场景对比,其是分配一个新的物理内存,而子进程场景只是找到父进程的共享内存对应的物理页即可,无需分配新的物理内存. 最后就是填充子进程对应的页表,那么进程就可以对共享内存进行访问.
对于查找共享内存对应物理页的逻辑简单归结为: 父子进程的 VMA 对应同一个文件(伪文件), 进而可以找到唯一的 inode, inode 维护了 address_space,该数据结构维护的 XARRAY 可以通过文件偏移找到对应的物理页,因此子进程只要提供虚拟地址,缺页异常处理函数就可以基于这个逻辑将虚拟地址转换成文件偏移,即可在 XARRAY 里找到对应的物理页,那么接下来只要更新一下页表,子进程即可访问共享内存.
共享内存换入换出(SWAP) 缺页场景
当进程分配一段可读可写的共享内存之后,并可以对共享内存进行读写访问,但由于系统内存资源紧缺,那么系统会将部分共享内存交换到 SWAP Space 上,以释放物理内存供其他进程使用,一旦系统内存压力解决,被换成的共享内存页可以再次被加载(SWAP IN) 到物理内存里。当共享内存被交换到 SWAP Space 之后,进程访问共享内存会再次触发缺页,缺页异常处理函数会将共享内存页加载(SWAP IN)到新的物理页上,并更新页表指向新的物理页,那么进程可以继续访问共享内存.
与匿名内存(Anonymous Memory) SWAP OUT 存在一些差异,匿名内存被交换出去的时候会将 SWAP Entry 的信息填充到 PTE Entry 里,以便缺页时异常处理函数根据该信息从 SWAP Space 里加载(SWAP IN) 内容到物理内存. 反观共享内存被 SWAP OUT 之后,PTE Entry 的内容是被清空,由于其物理内存信息维护在 struct address_space 的 XARRAY 数组里,因此交换到 SWAP Space 信息维护在 XARRAY Entry 里,那么此时共享内存缺页时,异常处理函数发现共享内存的 PTE Entry 为空,那么到 XARRAY Entry 里加载共享内存的内容. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on SWAP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-SWAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,父进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 和 PROT_WRITE 属性实现可读可写的共享内存,然后在 32 行对共享内存进行写操作,并在 34 行再次对共享内存进行读操作,接下来在 37 行调用 madvise 函数发送 MADV_PAGEOUT 请求,让系统将该共享内存交换到 SWAP Space 上,然后子进程在 40 行对共享内存进行写操作,此时会再次发生缺页将共享内存的内容从 SWAP Space 加载到物理内存上. 以上便是一个最基础的实践案例,可以知道 32 行读操作和 40 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 40 行前后加上 BS_DEBUG 开关:
接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-SWAP-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “SWAP Shmem Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 SWAP OUT 之后的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
进程对交换出去的共享内存进行访问,其会再次触发缺页异常,此时缺页异常处理函数的逻辑如上,大体上与可读可写共享内存第一次缺页流程类似,唯一不同的是在 do_shared_fault 函数里,处理函数从 XARRAY 获得是 SWAP Entry 的信息,而不是物理页的信息,于是调用 shmem_swapin_folio 函数将共享内存的内容加载到系统物理内存。其余处理逻辑并无差异.
shmem_getpage_gfp 函数用于分配新的共享内存或加载共享内存,在该场景下,函数在 1861 行调用 __filemap_get_folio 函数从 XARRAY 中获得对应的物理内存信息,此时获得的 folio 既非空也不包含物理页信息,而是包含了 SWAP Entry 的信息,那么函数在 1871 行调用 xa_is_value 函数检查到 folio 里面记录了 SWAP Entry 的信息,于是进入 1872 行调用 shmem_swapin_folio 函数加载共享内存到信息的共享内存上, 并直接返回.
共享内存内存压缩(ZSWAP) 缺页场景
当进程分配一段可读可写的共享内存之后,并可以对共享内存进行读写访问,但由于系统内存资源紧缺,那么系统会将部分共享内存进行压缩,以释放物理内存供其他进程使用,一旦系统内存压力解决,被压缩的内存会被解压到物理内存里。当共享内存被压缩之后,进程访问共享内存会再次触发缺页,缺页异常处理函数会将共享内存页解压到新的物理页上,并更新页表指向新的物理页,那么进程可以继续访问共享内存.
与匿名内存(Anonymous Memory) SWAP OUT 存在一些差异,匿名内存被交换出去的时候会将 SWAP Entry 的信息填充到 PTE Entry 里,以便缺页时异常处理函数根据该信息从 SWAP Space 里加载(SWAP IN) 内容到物理内存. 反观共享内存被压缩之后,PTE Entry 的内容是被清空,由于其物理内存信息维护在 struct address_space 的 XARRAY 数组里,因此内存压缩的信息维护在 XARRAY Entry 里,那么此时共享内存缺页时,异常处理函数发现共享内存的 PTE Entry 为空,那么到 XARRAY Entry 里加载共享内存的内容. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
(zswap.enabled=1) CMDLINE on Kernel
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on ZSWAP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-ZSWAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-SHMEM-ZSWAP-default Source Code on Gitee
实践案例由一个应用程序构成,父进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 和 PROT_WRITE 属性实现可读可写的共享内存,然后在 34 行对共享内存进行写操作,并在 36 行再次对共享内存进行读操作,接下来在 39 行调用 madvise 函数发送 MADV_PAGEOUT 请求,让系统将该共享内存进行压缩,然后子进程在 42 行对共享内存进行写操作,此时会再次发生缺页将共享内存的内容解压到物理内存上. 以上便是一个最基础的实践案例,可以知道 34 行读操作和 42 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 42 行前后加上 BS_DEBUG 开关:
接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-ZSWAP-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “ZSWAP SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对内存压缩之后的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
进程对交换出去的共享内存进行访问,其会再次触发缺页异常,此时缺页异常处理函数的逻辑如上,大体上与可读可写共享内存第一次缺页流程类似,唯一不同的是在 do_shared_fault 函数里,处理函数从 XARRAY 获得是 ZSWAP Entry 的信息,而不是物理页的信息,于是调用 shmem_swapin_folio 函数将共享内存的内容解压到系统物理内存。其余处理逻辑并无差异.
shmem_getpage_gfp 函数用于分配新的共享内存或解压共享内存,在该场景下,函数在 1861 行调用 __filemap_get_folio 函数从 XARRAY 中获得对应的物理内存信息,此时获得的 folio 既非空也不包含物理页信息,而是包含了 ZSWAP Entry 的信息,那么函数在 1871 行调用 xa_is_value 函数检查到 folio 里面记录了 ZSWAP Entry 的信息,于是进入 1872 行调用 shmem_swapin_folio 函数解压共享内存到信息的共享内存上, 并直接返回.
共享内存发生 MCE(UE) 缺页场景
在 Linux 中,MCE 代表 Machine Check Exception(机器检查异常),它用于处理硬件错误和异常情况。MCE 可以分为 CE(Corrected Error,已纠正的错误) 和 UE(Uncorrected Error, 未纠正的错误) 两种类型。MCE 是 Linux 内核的一部分,用于监视和响应这些硬件错误,以提高系统的可靠性和稳定性。当发生 MCE 时,通常会生成系统日志(例如 /var/log/mcelog),以帮助管理员诊断和解决硬件问题。在服务器和关键应用中,监控和处理 MCE 错误非常重要,以确保系统的可靠性和可用性. CE 和 UE 的区别如下:
- CE(Corrected Error,已纠正的错误): CE 是指硬件错误,但系统可以自动纠正这些错误,而不会导致系统崩溃或严重问题。通常这些错误是由于内存或其他硬件组件的临时问题引起的,例如内存中的单个位翻转。内核会记录这些 CE 错误,但不会采取进一步的行动,因为它们被认为是暂时性的,不需要干预
- UE(Uncorrected Error,未纠正的错误): UE 是指硬件错误,但系统无法自动纠正这些错误,因此可能会导致系统崩溃或数据损坏。UE 错误通常是更严重的硬件问题,例如内存模块中的多个位错误或其他硬件组件的不可恢复故障。内核会尝试采取适当的措施来处理 UE 错误,例如记录错误信息并尝试停机以防止进一步损坏
在 Linux 里,当硬件检测到硬件错误或者异常,这些硬件错误可能包括内存位翻转、缓存错误、总线错误、内存单元故障等,那么硬件会触发 MCE 异常,MCE 异常处理函数能处理很多硬件错误,其中对于内存 UE 的处理逻辑如上图, 其核心是调用 memory_failure() 函数进行处理,该函数主要完成一下几个任务:
- 标记物理页: 将发生 UE 对于的物理页标记为 PG_hwpoison
- 解除映射: 通过 TTU 机制解除用户进程映射到物理页的映射
- 更新页表: 构造 UE Entry 并更新页表,以防止进程再次访问
MCE 异常处理函数构造的 UE PTE Entry 如上图,可以看到页表 Entry 的 _PAGE_PRESENT 标志位为 0,然后 MSB 5BIT 存储了 SWAP Entry 的类型,UE 场景下该类型是 SWP_HWPOIS-ON,_PAGE_PROTNONE 后面两个 BIT 的字段存储了物理页帧的反码. 因此当进程再次访问 UE 内存 时,MMU 检查到对应的物理内存不存在,于是触发缺页异常,缺页异常处理函数再根据 SWP_HWPOISON 可以判断是 UE Page Fault,那么接下来就是将对应的进程杀死. 接下来通过一个实践案例进一步了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核 CONFIG_MEMORY_FAILURE 宏):
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on MCE(UE) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-MCE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,其分作三部分,首先是 21 行调用 mmap 函数结合 MAP_SHARED 和 MAP_ANONYMOUS 分配一段可读可写的共享内存,并在 32 行对匿名内存执行写操作, 以及在 34 行再次对匿名内存执行读操作. 第二部分是利用 madvise 注入 UE,由于 UE 是硬件行为,所有只能借助软件来模拟 UE 的发生,于是函数在 37 行调用 madvise 函数发送 MADV_HWPOISON 请求. 最后是第三部分,程序在 40 行再次对共享内存执行写操作,此时会再次触发缺页并被识别为 UE Page Fault. 以上便是一个最基础的实践案例,可以知道 32 行读操作和 40 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 40 行前后加上 BS_DEBUG 开关:
接着在共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_swap_page 函数的 3732 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-MCE-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “MCE SHMEM Memory on do_swap_page 0x6000000000”, 说明缺页异常处理函数执行过 UE 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看共享内存在缺页异常处理流程里的流动.
对于 Memory UE 引起的缺页,缺页异常处理函数根据 UE SWP Entry,一旦确认是 Memory UE Page Fault, 直接退出到 handle_mm_fault 函数,然后执行 force_sig_mceerr 发送信号杀死进程.
对于 Memory UE 缺页核心处理函数是 do_swap_page 函数,由于 MCE 异常将页表修改成了 SWAP Entry,其特点就是 _PAGE_PRESENT 不存在,但在 SWAP TYPE 字段存储着 SWP_HWPOISON 信息,因此 do_swap_page 函数在 3735 行调用 pte_to_swp_entry 函数获得 SWAP Entry 之后,调用 non_swap_entry 函数检查到里面不是正常的 SWAP Entry,接着读取 SWAP TYPE 字段知道符合 is_hwpoison_entry() 函数对应的分支,因此函数在 3747 行将缺页异常返回原因设置为 VM_FAULT_HWPOISON.
在 do_user_addr_fault 函数处理异常处理返回值时,VM_FAULT_HWPOISON 返回值会进入 1454 行分支,并调用 do_sigbus 函数发起信号杀死进程。以上便是 Memory UE Page Fault 缺页处理的流程.
共享内存 SYSV 缺页场景
SystemV 共享内存是 Linux 中一种共享内存机制,用于不同进程之间共享数据。它是 SystemV IPC(Inter-Process Communication,进程间通信) 机制的一部分,与其他 IPC 机制如信号量和消息队列一起提供了进程间通信的方式, SystemV 共享内存的主要特点包括:
- 共享内存段: SystemV 共享内存允许多个进程创建或连接到共享内存段(shared memory segment),并在这些段中读取和写入数据
- 高效的共享: 与其他 IPC 机制相比,共享内存提供了一种高效的方式来在进程之间共享大量的数据, 这是因为它不涉及数据复制,而是直接在内存中共享数据
- 操作系统持久性: 共享内存段通常具有操作系统的持久性。这意味着它们可以在不同的进程之间保持数据的一致性,即使某个进程退出也不会导致共享内存段的销毁
- 内核支持: Linux 内核提供了对 SystemV 共享内存的支持,包括创建、连接、分离和删除共享内存段的系统调用,例如 shmget、shmat、shmdt 和 shmctl
SYSV 机制提供了 shmget 和 shmat 等多个接口,其中 shmget 接口用于创建或获取一个共享内存的标识符,其可以指定共享内存的属性,例如可读可写等. shmat 接口用于将与其他进程共享的共享内存映射到自己的虚拟地址空间,该函数也是一个系统调用,从流程可以看出其也是获得共享内存对应的文件,然后调用 do_mmap 函数分配共享内存,其对应的文件处理接口是 shm_file_operations,该接口的 mmap 对应 shmem_mmap 函数,这就是分配共享内存的接口,因此 do_mmap 函数分配的内存与普通的共享内存一致.
当进程 A 通过 shget 接口与进程 B 共享一块共享内存,并通过 shmat 接口获得该共享内存,然后进程 A 或者进程 B 访问这段共享内存时都会发生缺页,缺页异常处理函数会找到共享内存,然后将进程的页表映射到该共享内存上. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on SYSV --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-SYSV-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两个应用程序组成,上图为 SERVER 端的应用程序,其在 26 行调用 shmget 函数申请一段可读可写的共享内存,并获得 IPC ID,然后调用 shmat 接口将共享内存映射到进程的地址空间,程序接着在 40 行将共享内存清零,并在 41 行向共享内存写入字符串.
上图为 CLIENT 端的应用程序,其在 26 行调用 “ipcs -m” 命令查看当前 IPC 信息,然后在 29 行调用 shmget 接口共享 SHMEM_KEY 对应的共享内存,然后在 36 行调用 shmat 行将共享内存映射到进程的地址空间,并在 43 行对共享内存发起写操作,然后在 46 行对共享内存进行读操作,49 行和 55 行两个函数的目的是进程释放共享。以上便是一个最基础的实践案例,可以知道 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:
接着在共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-SYSV-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “SYSV SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 SYSV 的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对于可读可写 SYSV 共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.
do_shared_fault 函数作为可读可写 SYSV 共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 XARRY 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.
共享内存 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 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on POSIX --->
# 部署实践案例
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 函数无异.
共享内存 UNIX-SOCK 缺页场景
在 Linux 中,Unix Socket(也称为 Unix 域套接字或本地套接字) 是一种用于进程间通信(IPC)的通信机制。与传统的网络套接字不同,Unix Socket 是在本地主机上进行通信的方式,不涉及网络协议栈,因此具有低延迟和高性能的特点。Unix Socket 通常用于同一台计算机上运行的不同进程之间进行通信,通信方式可以是面向字节流或数据报. Unix Socket 通过文件系统路径来标识套接字。通常套接字是文件系统中的一个特殊文件,可以使用文件路径来访问它们, 这使得不同进程可以轻松地找到和连接到所需的套接字。POSIX 共享内存也使用文件系统路径来标识共享内存对象,这意味着 Unix-Socket 也可以实现多个进程之间的共享内存,其方法如下:
- 创建共享内存: 首先,在一个进程中创建一段共享内存,这段共享内存包含要共享的数据和任何必要的同步机制,例如互斥锁或信号量
- 创建 Unix Socket 服务器: 在一个进程中创建 Unix Socket 服务器,该服务器将用于接收其他进程的请求,以便访问共享内存
- 将共享内存数据发送到客户端: 其他进程作为 Unix Socket 客户端连接到服务器,并发送请求来获取或修改共享内存中的数据。服务器接收到请求后,将共享内存中的数据发送回客户端
- 处理客户端请求: 服务器进程负责处理客户端请求,包括读取或写入共享内存中的数据。在执行这些操作时,服务器需要确保访问共享内存的线程安全性,可能需要使用同步机制
- 关闭连接和清理: 当客户端完成对共享内存的访问后,服务器可以关闭连接。在共享内存不再需要时,可以清理和释放共享内存
UNIX-SOCKET 共享内存是 POSIX 共享内存的一种应用,可以基于文件系统路径标识共享内存对象,但与 POSIX 共享内存不同,UNIX-SOCKET 需要提供一个独立的文件作为共享文件标识,而 POSIX 共享文件标识均来自 ‘/dev/shm’ 目录下,而 UNIX-SOCKET 共享文件标识可以来自任何文件系统的文件. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on UNIX-SOCK --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-USOCK-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-SHMEM-USOCK-default Source Code on Gitee
实践案例由两个应用程序组成,上图为 SERVER 端的应用程序,看似代码很长实则逻辑很简单,其由三个部分组成,首先是 send_fd 函数用于通过 Unix-Socket 套接字向 CLIENT 发送共享文件标识符,第二部分则由 socket_setup 函数组成,其主要目的就是构造 UNIX-Socket 通道。最后一部分就是 91-121 的逻辑,其在 91 行调用 open 函数创建共享文件标识,并将共享文件大小修改为 MAP_SIZE, 接着在 99 行调用 mmap 函数分配共享内存,并在 108 行向共享内存写入字符串 “Bello BiscuitOS” 字符串。程序接下来进行 While 循环等待 CLIENT 的请求,在 112 行调用 accept() 函数监听请求,当有新的请求到来时,程序在 119 行调用 send_fd() 函数将共享文件标识发送给 CLIENT 端,并关闭 afd 完成该次请求。
上图为 CLIENT 端的应用程序,程序代码也很长但逻辑很简单,其也是由两部分组成,首先是 sock_setup 函数用于构建 Unix-Socket 套接字通道,并向 SERVER 端发起请求,以便获得共享内存文件标识符. 第二部分是 85-105 行的逻辑,当 85 行成功获得共享内存表示符之后,函数在 88 行调用 mmap 函数将共享内存映射到自己的地址空间,然后在 99 行对共享内存进行写操作,并在 101 行对共享内存进行读操作,最后释放内存和关闭共享文件. 以上便是最简单的实践案例,可以知道程序在 99 行对共享内存的写操作会触发缺页异常,为了可以看到内存在缺页异常里的流动,在 SERVER 程序 99 行前后加上 BS_DEBUG 开关:
接着在共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-USOCK-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “UNIX-SOCKET SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 UNIX-SOCKET 的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对于可读可写 UNIX-Socket 共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.
do_shared_fault 函数作为可读可写 UNIX-SOCKET 共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 Xarray 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.
共享内存 MEMFD 缺页场景
在 Linux 中,Unix Socket(也称为 Unix 域套接字或本地套接字) 是一种用于进程间通信(IPC)的通信机制, 其通过共享文件描述符实现多个进程之间共享内存,因此共享文件描述符称为了关键所在,那么对于采用 MAP_SHARED 与 MAP_ANONYMOUS 分配的共享内存,其如果要实现在不同进程的之间共享,那么也需要为这段共享内存创建一个共享描述符. MEMFD(内存文件描述符) 机制是一种用于创建和操作匿名内存映射区域的方法,它允许将内存块视为文件,并通过文件描述符进行访问。MEMFD 是Linux 3.17 内核引入的新特性,它的主要目的是为了提供一种灵活、轻量级的内存共享和 IPC(进程间通信) 机制,而无需使用传统的文件或共享内存. 以下是 MEMFD 机制的一些关键特点:
- 无需文件系统路径: MEMFD 创建的内存映射区域不依赖于文件系统路径,因此不需要在文件系统中创建文件或目录。这使得它非常灵活,可以用于临时存储、临时文件、内存缓冲区等
- 文件描述符访问: MEMFD 创建的内存区域通过文件描述符进行访问,类似于普通文件。您可以使用标准的文件 I/O 函数(如 read 和 write) 来读取和写入内存区域的数据
- 轻量级: MEMFD 机制非常轻量,不需要额外的文件系统操作或共享内存管理。它适用于需要快速创建临时内存区域的情况
- 可以用于 IPC: MEMFD 的文件描述符可以用于进程间通信,允许多个进程共享内存块。这可以用于实现共享数据、消息传递等
- 内存清理: 当所有持有 MEMFD 文件描述符的进程都关闭它时,关联的内存区域将被自动释放,不会导致内存泄漏
MEMFD 机制与 UNIX-SOCKET 机制结合实现的共享内存是 POSIX 共享内存的一种应用,可以基于文件系统路径标识共享内存对象,但与普通的 UNIX-SOCKET 共享内存不同,MEMFD 创建的共享文件标识是动态随机创建的,其无需关系创建共享文件的名字和路径. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on MEMFD --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-MEMFD-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-SHMEM-MEMFD-default Source Code on Gitee
实践案例由两个应用程序组成,上图为 SERVER 端的应用程序,看似代码很长实则逻辑很简单,其由三个部分组成,首先是 send_fd 函数用于通过 Unix-Socket 套接字向 CLIENT 发送共享文件标识符,第二部分则由 socket_setup 函数组成,其主要目的就是构造 UNIX-Socket 通道。最后一部分就是 91-121 的逻辑,其在 91 行调用 memfd_create 函数为匿名的共享内存创建共享文件标识,并将共享文件大小修改为 MAP_SIZE, 接着在 99 行调用 mmap 函数分配共享内存,并在 108 行向共享内存写入字符串 “Bello BiscuitOS” 字符串。程序接下来进行 While 循环等待 CLIENT 的请求,在 112 行调用 accept() 函数监听请求,当有新的请求到来时,程序在 119 行调用 send_fd() 函数将共享文件标识发送给 CLIENT 端,并关闭 afd 完成该次请求。
上图为 CLIENT 端的应用程序,程序代码也很长但逻辑很简单,其也是由两部分组成,首先是 sock_setup 函数用于构建 Unix-Socket 套接字通道,并向 SERVER 端发起请求,以便获得共享内存文件标识符. 第二部分是 85-105 行的逻辑,当 85 行成功获得共享内存表示符之后,函数在 88 行调用 mmap 函数将共享内存映射到自己的地址空间,然后在 99 行对共享内存进行写操作,并在 101 行对共享内存进行读操作,最后释放内存和关闭共享文件. 以上便是最简单的实践案例,可以知道程序在 99 行对共享内存的写操作会触发缺页异常,为了可以看到内存在缺页异常里的流动,在 SERVER 程序 99 行前后加上 BS_DEBUG 开关:
接着在共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-MEMFD-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “MEMFD SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 MEMFD/UNIX-SOCKET 的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对于可读可写 MEMFD/UNIX-Socket 共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.
do_shared_fault 函数作为可读可写 MEMFD/UNIX-SOCKET 共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 Xarray 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.
共享内存 DEV-SHM 缺页场景
在 Linux 中,POSIX 共享内存是一种用于实现进程间通信(IPC)的机制,允许多个进程共享一个内存区域,从而实现数据共享。POSIX 共享内存使用文件系统路径来标识共享内存对象。这意味着进程可以通过文件路径来访问和共享数据,而不需要预先协商一个键值。在 Linux 系统中,“/dev/shm” 目录是一个特殊的目录,它通常用于实现共享内存(shared memory) 和临时文件系统(tmpfs)。这个目录的特别之处在于它允许用户在内存中创建文件系统,用于临时数据存储和进程间通信,而不是存储在硬盘上的持久性文件. 以下是 “/dev/shm” 目录的一些特别之处:
- 临时文件系统: “/dev/shm” 实际上是一个临时文件系统,通常挂载在内存中,而不是硬盘上。这意味着所有的数据和文件都存储在 RAM 中,访问速度非常快
- 用于共享内存: “/dev/shm” 目录通常用于实现共享内存机制,允许多个进程在内存中共享数据。这对于高效的进程间通信非常有用,因为不需要通过硬盘来存储和读取数据
- 临时文件存储: “/dev/shm” 目录也用于存储临时文件,这些文件通常在进程执行期间使用,并在进程结束时自动删除。这可以减少硬盘I/O,提高性能
- 内存限制: 由于共享内存和临时文件系统存储在 RAM 中,因此它们受到系统内存限制的约束。如果过多的数据存储在 “/dev/shm” 中,可能会导致系统内存不足,因此需要谨慎使用
- 权限控制: “/dev/shm” 目录的权限通常受到系统设置的影响,可以通过文件系统权限进行控制,以确保只有授权的进程可以访问其中的数据
“/dev/shm” 目录的特点决定了其与共享内存有不可分割的连续,首先 POSIX 共享内存创建的共享文件位于该目录下,另外该目录存储的文件又位于 RAM 之上,那么文件消耗的正是共享内存. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on /dev/shm --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-DEVSHM-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-SHMEM-DEVSHM-default Source Code on Gitee
实践案例很简单,多个进程通过打开 “/dev/shm” 目录下同一个文件,然后将文件映射到进程地址空间即可实现内存共享. 程序首先在 25 行调用 open 打开 “/dev/shm” 目录下的共享文件,然后在 32 行调用 mmap 将共享文件映射到进程地址空间,那么函数在 45 行可以对共享内存进行写操作,并且在 47 行对共享内存执行读操作,最后为了调试效果在 49 行永久睡眠. 以上便是最简单的实践案例,可以知道程序在 45 行对共享内存的写操作会触发缺页异常,为了可以看到内存在缺页异常里的流动,在 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-DEVSHM-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “DEVSHM SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 DEVSHM 的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。另外可以看到新创建的文件大小只是 16 字节, 但程序使用了 4K 的共享内存,开发者可以查看 POSIX 共享内存时,其创建共享文件大小为 4K,占用 4K 共享内存. 最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对于可读可写 DEVSHM 共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.
do_shared_fault 函数作为可读可写 DEVSHM 共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 Xarray 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.
共享内存 LOCAL 缺页场景
在 Linux 中,POSIX 共享内存是一种用于实现进程间通信(IPC)的机制,允许多个进程共享一个内存区域,从而实现数据共享。POSIX 共享内存使用文件系统路径来标识共享内存对象。这意味着进程可以通过文件路径来访问和共享数据,而不需要预先协商一个键值。在 Linux 中,tmpfs 是一种临时文件系统,它将文件和目录存储在内存中,而不是在硬盘上。tmpfs 允许您在 RAM 中创建临时文件系统,通常用于存储临时文件、进程间通信(IPC)、共享内存和其他需要高速读写操作的场景。以下是一些关于 tmpfs 文件系统的关键特点和用途:
- 存储在 RAM 中: tmpfs 文件系统的文件和目录完全存储在系统内存中,因此读取和写入操作非常快。这使得 tmpfs 非常适合用于临时数据存储,如临时文件、进程间通信和缓存
- 动态分配: tmpfs 文件系统动态分配内存,根据需要分配内存块。这意味着它不会占用过多的内存,只会使用实际需要的内存
- 文件系统权限: 与硬盘上的标准文件系统一样,tmpfs 文件系统支持文件和目录的权限控制,可以使用标准的文件权限位来控制访问
- 自动清理: tmpfs 文件系统中的文件和目录通常在系统重启时被删除,因此它们是临时的。这对于存储临时数据非常有用,因为不需要手动清理
tmpfs 文件系统可以使用 IPC,那么多个进程可以通过在 tmpfs 目录使用同一个文件,以此实现共享内存。另外可以使用私有的 tmpfs 文件系统实现多个进程的内存共享. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on LOCAL --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-LOCAL-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-SHMEM-LOCAL-default Source Code on Gitee
实践案例由两个应用程序构成,以此构造多个进程共享内存的场景。上图为 SERVER 端的应用程序,其首先在 25 行调用 open 函数打开指定的共享文件,然后在 32 行调用 mmap 函数将共享内存映射到地址空间,并在 42 行对共享内存进行写操作,然后在 44 行对共享内存进行读操作,最后在释放内存关闭文件.
实践案例的另外部分如上图,其逻辑和 SERVER 端一致,首先在 24 行打开共享文件,然后在 31 行调用 mmap 函数将共享内存映射到进程地址空间,并在 41 行对共享内存进行读操作。以上便是最简单的实践案例,可以知道 SERVER 端程序在 42 行对共享内存的写操作会触发缺页异常,为了可以看到内存在缺页异常里的流动,在 SERVER 程序 42 行前后加上 BS_DEBUG 开关:
接着在共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-DEVSHM-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “LOCAL SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 LOCAL 的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动. 最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对于可读可写 LOCAL 共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.
do_shared_fault 函数作为可读可写 LOCAL 共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 Xarray 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.
共享内存 HugeShmem(THP) 缺页场景
在 Linux 中,POSIX 共享内存是一种用于实现进程间通信(IPC)的机制,允许多个进程共享一个内存区域,从而实现数据共享。POSIX 共享内存使用文件系统路径来标识共享内存对象。这意味着进程可以通过文件路径来访问和共享数据,而不需要预先协商一个键值。在 Linux 中,Huge-tmpfs 是一种临时文件系统,它将文件和目录存储在内存中,而不是在硬盘上。Huge-tmpfs 与 tmpfs 一样允许 RAM 中创建临时文件系统,通常用于存储临时文件、进程间通信(IPC)、共享内存和其他需要高速读写操作的场景,不同的是 Huge-Tmpfs 创建的文件占用 2MiB 的共享内存。以下是一些关于 Huge-tmpfs 文件系统的关键特点和用途:
- 巨大页面: HugeShmem 使用巨大页面来分配共享内存。巨大页面比标准的 4KB 页面更大,通常为 2MB 或更大。这些大页面可以减少页表的大小和 TLB(Translation Lookaside Buffer) 缺失率,从而提高内存访问的效率
- 透明使用: 类似于 Hugetlbfs(巨大透明大页文件系统),HugeShmem 是”透明的”,这意味着应用程序不需要特殊的 API 来使用巨大页面。应用程序可以使用标准的内存分配函数(如 malloc) 来分配共享内存,而无需进行大幅度的代码更改
- 性能提升: HugeShmem 通常用于需要处理大量数据或需要高性能的应用程序,如数据库系统、科学计算、虚拟机等。通过使用巨大页面,可以降低内存管理的开销,并加速内存访问,从而提高应用程序的性能
- 配置和管理: 配置和管理 HugeShmem 通常需要管理员特权,因为它涉及系统级资源的分配和管理。管理员可以配置系统以启用巨大页面支持,并为进程分配巨大页面内存
Huge-tmpfs 文件系统可以使用 IPC,那么多个进程可以通过在 Hueg-tmpfs 目录使用同一个文件,以此实现共享内存。另外可以使用私有的 Huge-tmpfs 文件系统实现多个进程的内存共享. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Pseudo Filesystem: Huge TMPFS
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on HugeShmem(THP) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-THP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,程序首先在 23 行调用 open 函数打开 HugeTmpfs 目录下的文件 BiscuitOS.txt,然后在 30 行调用 mmap 函数将共享内存映射到进程的地址空间,接着在 41 行对共享内存进行写操作,然后在 43 行对共享内存执行读操作,最后就是释放共享内存. 以上便是最简单的实践案例,可以知道应用程序在 41 行对共享内存的写操作会触发缺页异常,为了可以看到内存在缺页异常里的流动,在应用程序 41 行前后加上 BS_DEBUG 开关:
接着在共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-THP-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HugeTmpfs SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 HugeTmpfs 的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动. 最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
对于可读可写 HugeTmpfs 共享内存引起的缺页,缺页异常处理函数根据 vma_is_anonymous 和匿名内存进行区分,一旦缺页就根据缺页行为进行判断,如果缺页时是写操作,那么调用 do_shared_fault 函数进行处理,该函数完成两个基本动作,分配物理内存和建立页表.
do_shared_fault 函数作为可读可写 Hugetmpfs 共享内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 Xarray 等映射这里不放开讲。当分配物理内存之后,Shmem 没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异.
共享内存 Protection Key 缺页场景
在基于页表的权限管理里,如果用户进程需要修改页表的权限,那么首先需要通过 mprotect 系统调用修改对应的页表权限,然后进行 TLB 刷新,这是因为 TLB 机制会缓存虚拟地址到最后一级页表的内容,CPU 访问虚拟内存时,硬件首先在 TLB 查找映射关系,如果映射关系存在,那么硬件直接使用映射关系实现内存访问; 反之 TLB 映射关系不存在,那么 MMU 才会去页表里查找映射关系。如果只修改虚拟区域对应页表的权限,那么 TLB 存在映射关系且 MMU 硬件使用旧的权限进行访问,因此修改完页表权限之后需要更新 TLB。上图是 TLB Entry 存储的信息,可以看到 TLB 缓存了最后一级页表的多个权限信息.
Memory Protection Keys for Userspace(PKU) 是 Linux 中的一种内存保护机制,旨在改善用户空间进程的内存访问权限控制和安全性。PKU 引入了一种新的硬件特性,允许用户空间进程定义和管理内存保护密钥,从而实现细粒度的内存保护。PKU 的主要目标是提供以下功能:
- 细粒度内存访问权限控制: PKU 允许用户空间进程定义多个内存保护密钥,每个密钥可以与特定的内存区域相关联。这意味着进程可以为不同的内存区域设置不同的保护级别,从而提供更细粒度的内存访问权限控制
- 减少内核介入: 传统的内存权限更改通常需要涉及内核,并且可能涉及昂贵的系统调用。PKU 允许用户进程在不涉及内核的情况下更改内存保护密钥,从而减少了内核介入的次数,提高了性能
- 提高安全性: 通过允许用户空间进程定义和管理内存保护密钥,PKU 可以帮助防止内存访问错误和缓冲区溢出攻击。只有具有正确密钥的进程才能访问受保护的内存区域
PKU 的实现依赖于特定的硬件支持,因此需要支持 PKU 的处理器。此外操作系统内核也需要对 PKU 进行支持,以便用户空间进程可以利用这一特性. 在匿名内存场景下,如果使用 PKU 对用户空间共享内存设置权限,如果进程对共享内存没有 PKU 权限,那么会导致权限异常而触发缺页. PKU 缺页的场景可以通过一个实践案例进行了解,其在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Support Host CPU Feature Passthrough
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on Protection Key --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-PK-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,应用程序有点长被分作三部分,第一部分是 46-61 行调用 mmap 分配一段可读可写共享内存,并对共享内存进行写操作,然后是第二部分 64-84 行,其主要目的是使用 PKU 机制将共享内存设置为不可读不可写。第三部分是 90 行对 PKU 修改过后的区域进行读操作,此时由于 PKU 权限不够引起缺页异常. 最后 93-97 行就是回收测试资源. 以上便是一个最基础的实践案例,可以知道 90 行读操作会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 90 行前后加上 BS_DEBUG 开关:
接着在匿名内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_user_addr_fault 函数的 1381 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-PK-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “PKU SHMEM Memory on do_user_addr_fault 0x6000000000”, 说明缺页异常处理函数执行过 PKU 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
对于 PKU 缺页场景,其异常处理流程如上图,缺页异常处理找到共享内存对应的 VMA 之后,检查地址合法之后跳转到 good_area 出,此时调用 access_error 函数进行权限检查,对于 PKU 引起的缺页直接返回 1,因此调用 bad_area_access_error 函数发送 SIG_BUS 终结程序.
对于 PKU 权限引起的缺页,缺页异常原因中会包含 PF_PK, 因此在 access_error 函数里检查 PF_PK 之后直接返回 true. 以上便是 Protection Key 缺页场景.