在 Linux 用户空间,虚拟内存可以通过两种映射方式与物理内存建立映射关系,第一种是 File-Mapping 文件映射,其可以将文件内容映射到用户空间,虚拟内存和磁盘文件中间通过 Page CACHE 进行数据中转,因此可以像普通虚拟内存一样访问文件; 另外一种是 Anonymous-Mapping 匿名映射, 用于将用户空间虚拟内存映射到物理内存上,以满足进程对内存的需求,例如堆(Heap)内存、堆栈(Stack)内存、以及 MMAP 内存等。两种映射方式的不同点在于是否有后端文件,匿名内存 可以通过 malloc/brk/mmap 进行分配
另外 Linux 还支持共享内存,其可以通过 mmap 函数进行分配,分配时采用分配标志: MAP_SHARED 与 MAP_ANONYMOUS. 而匿名内存同样可以使用 mmap 进行分配,其分配标志是: MAP_PRIVATE 与 MAP_ANONYMOUS。虽然两者都采用了 MAP_ANONYMOUS, 但是两者的处理逻辑是不同的,虽然共享内存没有明确指明后端文件,当内核会为每个共享文件关联一个 tmpfs 文件系统的文件,而匿名内存 Linux 是不会关联任何文件的,因此从这个角度来看,采用 MAP_PRIVATE 与 MAP_ANONYMOUS 的内存才能称为匿名内存.
分配匿名内存的函数很多,其中最常用的是 mmap,由于其提供了多种配置参数,可以分配更灵活的匿名内存。当使用 mmap 系统调用分配匿名内存时,各参数的含义如下:
- ARG0: MAP_VADDR 可以设置匿名内存的虚拟地址,该值为 NULL 是代表分配一个随机的虚拟地址,当该值为指定值时代表从指定的地址分配虚拟内存,该地址可能已经被分配出去,使用时调用者需要确保虚拟地址被覆盖.
- ARG1: MAP_SIZE 指明了需要分配匿名内存的大小,长度为 PAGE_SIZE 的倍数,不能为 0.
- ARG2: 指明了匿名内存的访问权限,其值可以是这些标志的合集: PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行) 和 PROT_NONE(不可访问)
- ARG3: 指明映射类型,该字段必须包含是 MAP_PRIVATE 和 MAP_ANONYMOUS 标志,否则无法分配到匿名内存.
- ARG4: 映射文件描述符,对于匿名内存由于其没有后端文件,因此这里必须为 -1.
- ARG5: 指明映射的物理区域,由于匿名内存映射的随机物理内存,因此该字段必须为 0.
在匿名内存架构里,VMA 通过页表正向映射到匿名页上,并且每个 VMA 维护一个 STRUCT anon_vma 的数据结构(简称为 AV),该数据结构里存在一颗红黑树,该红黑树的节点为 STRUCT anon_vma_chain(简称为 AVC), 每个 AVC 都指向另外一个 VMA,那么每个 AVC 可以表示映射到该匿名页的 VMA,最后将匿名页的 mapping 指向 AV。有了上面的关系之后,如>果获得一个匿名页之后,想通过逆向映射知道哪些 VMA 映射了该匿名页,那么可以通过匿名页的 mapping 获得 AV,然后遍历 AV 对应的红黑树获得每一个 AVC,在通过 AVC 获得 VMA 即可.
在 Linux 系统里,匿名内存的数量可以通过 ‘/proc/meminfo’ 文件提供的信息获得,其中 AnonPages 字段表示匿名内存的大小; 同理 ‘/proc/vmstat’ 文件也提供了信息,其中 nr_anon_pages 字段表示匿名页的数量.
开发者可能在其他地方看到这些术语: 匿名内存、匿名映射、匿名页等,那么具体指的是什么:
- 匿名映射: 指的是进程分配一段虚拟内存直接映射到系统物理内存的方式,匿名映射是相对与文件映射而言
- 匿名页: 进程通过匿名映射的物理页,每个物理页都用对应的 struct page 数据结构体,那么可以通过 PageAnon(page) 判断物理页是否为匿名页, 另外如果该物理页只被一个 VMA 映射,那么该物理页也称为独占匿名页(Exclusive Anonymous Page), 同理可以使用 PageAnonExclusive(page) 进行区分
- 匿名内存: 一般将进程通过匿名映射的内存称为匿名内存,其既包括虚拟内存部分,也包括物理内存部分, 其对应的 VMA 通过 vma_is_anonymous() 函数进行判断. 匿名内存可以通过 mmap(MAP_PRIVATE | MAP_ANONYMOUS) 方式获得,也可以通过 malloc/brk 从堆上分配,亦或者从堆栈(Stack) 上获得.
当调用 mmap 函数分配匿名内存时,其在 mmap 系统调用处理了逻辑如上图,与文件映射不同的地方是 pgoff 设置为虚拟地址的页号,并且调用 vma_set_anonymous 将 VMA 标记为匿名映射,那么 VMA 对应的 vm_ops 为空,因此可以用该条件判断 VMA 是否采用了匿名映射. 其余操作与文件映射差异不大,经过 mmap 系统调用之后,进程分配到一段虚拟内存,但这段虚拟内存是没有映射物理内存的,那么接下来进程一旦访问这段虚拟内存,那么就会触发缺页异常. 匿名内存在 Linux 里使用非常广泛,以下是匿名内存的使用场景介绍:
PreALLOC 方式分配匿名内存
PreALLOC 也支持对匿名内存的分配,匿名内存在用户进程里使用很广,例如堆、栈、MMAP 映射区内存等,PreALLOC 的特点可以让进程更快的访问虚拟内存。那么接下来通过一个实践案例介绍如何使用 PreALLOC 分配匿名内存,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] USER PREALLOC: Anonymous Memory --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-VM-PREALLOC-ANON-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-VM-PREALLOC-ANON-default Source Code on Gitee
实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配虚拟内存,由于其使用了 MAP_PRIVATE 和 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-ANON-default/
# 编译内核
make kernel
# 编程程序
make build
BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了运行实践案例所需的全部命令,可以看到应用程序运行之后,系统并没有打印缺页相关的信息,为了更好证明实践案例的可靠性,开发者可以将 mmap 函数的 MAP_POPULATE 标志去掉之后再实践:
通过对比实践可以看出,当 mmap 里去掉 MAP_POPULATE 标志之后,访问虚拟内存缺失引起了缺页异常,因此再次证明实践案例确实在分配虚拟内存的同时也分配了物理内存,并建立页表映射. 以上便是 PreALLOC 的一种使用场景.
LazyALLOC 方式分配匿名内存
LazyALLOC 也支持对匿名内存的分配,匿名内存在用户进程里使用很广,例如堆、栈、MMAP 映射区内存等,LazyALLOC 的特点是当进程访问虚拟内存时才会去分配物理内存。那么接下来通过一个实践案例介绍如何使用 LazyALLOC 分配匿名内存,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] USER LAZYALLOC: Anonymous Memory --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-VM-LAZYALLOC-ANON-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-VM-LAZYALLOC-ANON-default Source Code on Gitee
实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配虚拟内存,由于其使用了 MAP_PRIVATE 和 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-ANON-default/
# 编译内核
make kernel
# 编程程序
make build
BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了运行实践案例所需的全部命令,可以看到应用程序运行之后,系统并打印缺页相关的信息,说明进程在分配虚拟内存的时候仅仅分配了虚拟内存. 以上便是 LazyAlloc 的一种使用场景.