目录
内核维护进程虚拟内存区域
进程地址空间 VMA 链表
进程地址空间红黑树/区间树
VMCACHE
进程地址空间分配虚拟内存
MAP FLAGS TO VMA FLAGS
MAP FLAGS TO PAGETABLE ATTRIBUTE
ANONYMOUS MAPPING: 匿名映射
FILE MAPPING: 共享映射
逆向映射区间树之匿名映射
逆向映射区间树之文件映射
MEMORY With MMAP
ANONYMOUS MEMORY: 匿名内存
SHARED MEMORY: 共享内存
FILE-MAPPING MEMORY: 文件映射内存
HUGETLB MEMORY
THP(Transparent Huge Page): 透明大页内存
PFNMAP-MAPPING MEMORY: PFNMAP 映射内存
VMA 区域合并: MERGE
VMA 区域拆分: SPLIT
VMA 数据结构
STRUCT VM_AREA_STRUCT
STRUCT VM_OPERATIONS_STRUCT
VM_OPS: FAULT
VM_OPS: HUGE-FAULT
VM_OPS: OPEN
VM_OPS: CLOSE
VMA FLAGS
- VM_FLAGS: VM_PFNMAP
MAP FLAGS
MAP_PRIVATE
MAP_SHARED
MAP_ANONYMOUS
MAP_FIXED
MMAP 进阶研究
MMAP 机制导论
在 Linux 里,虚拟内存是一块平坦连续的地址空间,虚拟内存被换分成两段,其中低地址部分是由进程使用的用户空间(Userspace), 而高地址部分是由内核使用的内核空间(Kernel Space). 由于分页机制的存在,用户进程认为整个虚拟地址空间只有自己和内核两部分,那么用户进程可以自由使用自己的用户空间。用户进程的虚拟地址空间按用途划分成不同的区域,如上图所示(区域具体含义可以参考《内存大图之 Userspace Memory Map》). 用户进程并不是直接就可以访问用户空间的虚拟内存,而是需要使用时进行申请,申请成功之后就可以使用,使用完毕之后再释放会系统,这样才能保证进程长期的稳定运行.
Linux 提供了很多接口用于用户进程分配虚拟内存和回收虚拟内存,比较常见的接口如 mmap/munmap 和 malloc/free 函数组,这些接口都可以从进程的虚拟地址空间里分配到可用的虚拟内存,但差异点是可能两个接口分配的虚拟内存可能来自不同的功能区,例如有时 malloc 分配的虚拟内存可能来自堆(Heap)或者MMAP 区域, mmap 接口分配的虚拟内存来自 MMAP 区域,并且 mmap 函数提供了更灵活的参数用户分配不同需求的虚拟内存,例如分配只读或者可执行的虚拟内存等. mmap 属于 Linux 的系统调用 sys_mmap, 其不仅可以分配虚拟内存,而且还可以将文件或者设备的一部分映射到进程的虚拟地址空间,这使得进程可以通过指针直接读写这部分内存,而不是通过读写文件的系统调用, 另外 sys_mmap 还包括一下作用:
- 文件映射: 将一个文件或其一部分内容映射到进程的地址空间,以便快速访问文件内容, 这在数据库和需要快速文件访问的应用程序中非常常见
- 匿名映射: 创建不与任何文件关联的内存区域,通常用于动态内存分配,如实现堆
- 共享内存: 允许不同进程共享内存区域,为进程间通信提供一种高效的方式
- 执行映射: 将可执行文件或其一部分映射到内存,用于动态库的加载或程序的执行
通过上面的分析,sys_mmap 系统调用不仅提供了灵活的内存分配方式,还提供了更多的内存映射功能,访问进程完成对不同功能虚拟内存的分配,那么接下来的章节带各位开发者一同了解 sys_mmap 机制的组成、实践以及工程应用.
SYS_MMAP 系统在用户空间提供了 mmap 函数,该函数包含了 6 个参数,每个参数包含多种可选参数,以便满足不同的虚拟内存分配需求,参数的具体函数如下:
- ARG0: 该参数可以设置从进程虚拟地址空间指定位置分配虚拟内存,该值也可以为 NULL,那么系统调用分配一块任意地址空闲的虚拟内存.
- ARG1: 该参数设置分配虚拟内存的大小,虚拟内存的大小必须按 PAGE_SIZE 进行对齐, 如果设置了非 PAGE_SIZE 对齐的值,mmap 系统调用这会按 PAGE_SIZE 对齐进行分配.
- ARG2: 该参数设置虚拟区域的属性,可选项包括: PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)、PROT_NONE(无权限),这些选项可以单独使用,也可以组合起来使用,例如分配一段可读可写的虚拟区域,那么该字段可以使用 PROT_READ 和 PROT_WRITE.
- ARG3: 该参数用于设置映射标志,可选项包括: MAP_PRIVATE(私有映射)、MAP_ANONYMOUS(匿名映射)、MAP_SHARED(共享映射) 等标志,这些标志排列组合分配不同类型的内存,例如 MAP_PRIVATE 和 MAP_ANONYMOUS 组合是分配匿名内存的前提.
- ARG4: 该参数指明映射的文件,mmap 系统调用可以将文件直接映射到进程地址空间,因此需要建立文件映射时,该参数需要指向打开的文件. 如果需要建立匿名映射,那么该参数需要设置为 -1.
- ARG5: 该参数可以设置映射的物理区域. 当使用 mmap 映射外设的物理区域(RSVDMEM/MMIO) 时,可以将需要映射的物理区域的起始物理地址填充到该字段. 另外当 mmap 映射文件时,该字段也用于表示映射文件的偏移.
上图提供了 mmap 系统调用使用案例,案例在 21 行调用 mmap 函数从 MAP_VADDR 处分配长度为 MAP_SIZE 的虚拟内存,这段虚拟内存是可读可写(PROT_READ 和 PROT_WRITE), 并且虚拟内存是一段匿名内存。分配完毕之后将虚拟内存的起始地址存储在 mem 变量里,如果 26 行检查 mem 不等于 MAP_FAILED, 那么说明分配成功. 接着就是匿名内存的使用和回收.
内核维护进程虚拟内存区域
在 Linux 内核里,每个用户进程都使用一个 STRUCT task_struct 数据结构进行描述,其中进程的虚拟地址空间则通过 STRUCT mm_struct 数据结构进行描述,其由进程 STRUCT task_struct 数据结构的 mm 成员进行绑定. 进程的虚拟地址空间虽然是连续的地址空间,但由于其根据功能被划分成不同的虚拟区域,这些区域就形成了独立且不连续的孤岛,例如进程的 MMAP 映射区用于满足 mmap 系统调用分配的虚拟内存,放大该区域之后就可以看到其内部存在很多已经分配的区域(ALLOC)和很多未分配的区域(FREE). 内核为了管理已经分配的区域,引入了 STRUCT vm_struct_area 数据结构.
STRUCT vm_area_struct 数据结构可以描述用户进程地址空间一段已经分配的虚拟内存区域,将进程地址虚拟地址空间放大可以看到其有多个虚拟区域组成,可以将一个独立的虚拟区域称为 VMA, 这是因为内核使用 STRUCT vm_area_struct 数据结构进行描述,那么一个独立的 VMA 具有一定的属性和映射方式等,那么可以推断相邻的两个 VMA 是属性或者映射方式不同的两段虚拟内存.
进程虚拟地址空间中,已经分配的虚拟区域为 VMA,那么两个 VMA 之间的区域可以是可分配的区域(FREE), 或者是系统预留的区域(HOLE), 也可以称为空隙(GAP), 总之这些区域属于未分配区域,进程只能访问已经分配的区域,否则会引起不可预测的错误. 当进程需要分配一段虚拟内存,那么内核就会从未分配的区域找到一块空闲的区域进行分配.
进程地址空间 VMA 链表
内核使用 VMA 来描述用户进程虚拟地址空间里已经分配的虚拟内存,每个 VMA 的范围是 [vm_start, vm_end), 那么内核需要一个管理进程所有的 VMA,并且可以满足内核对 VMA 进行查找. 内核首先使用双链表将进程的所有 VMA 按起始虚拟地址从低到高进行排列,那么内核当获得一个 VMA 之后,就可以知道其前一个 VMA 和后一个 VMA 的地址范围,以此可以推算出 VMA 之间的 GAP(空隙/空闲区域大小).
内核使用 STRUCT mm_struct 维护用户进程的地址空间,并将用户进程所有 VMA 都维护在双链表 mmap 上,这样做的好处是既有个途径将进程所有的 VMA 管理起来,另外链表上的 VMA 都是按区域起始虚拟地址从低到高进行排列,那么可以通过双链表的属性知道 VMA 前一个 VMA(PREV) 和下一个 VMA(NEXT), 这样可以快速知道 PREV、VMA、以及 NEXT 之间的 GAP 大小,那么内核需要为进行分配一块可用的虚拟区域,只需遍历链表并找到两个 VMA 之间 GAP 满足分配大小的区域即可。一切看似完美但其也继承了双链表的缺点,那就是一旦双链表的 VMA 数量变多,那么会导致内核在链表里找到一块 GAP 的时间变长且不确定,那么为了解决这个问题,内核在使用双链表管理 VMA 的同时引入了红黑树进行管理.
进程地址空间红黑树/区间树
红黑树可能对大家来说不是那么熟悉,这里可以简化理解为红黑树是将 VMA 按起始虚拟地址插入到树里,形成了 Parent 节点对应的 VMA 的起始地址大于 Left 孩子节点对应的 VMA 的起始地址,且大于 Right 孩子节点对应的 VMA 的起始地址. 红黑树可以确保查询 VMA 在明确的时间内完成,那么这样就解决了链表查找时间不确定问题. 当需要通过虚拟地址查找 VMA 时,只需从 ROOT 根节点开始查找,如果虚拟地址大于当前节点 VMA 的起始地址,那么向右孩子节点继续查找,同理如果虚拟地址小于当前节点 VMA 的起始虚拟地址,那么向左孩子节点继续查找, 以此类推,直到找到虚拟地址对应的 VMA 为止.
红黑树虽然很好的解决了快速查找 VMA 的效率问题,但同时也会引入一个新的问题,例如插入红黑树的两个 VMA 之间存在重叠区域,那么通过 VMA 起始虚拟地址进行查找时会出现找到多个 VMA 的情况,为了解决这个问题引入了区间树,区间树是一个加强版的红黑树,它的特点可以简化为在插入一个 VMA 到红黑树时,需要根据 VMA 的起始虚拟地址和虚拟地址进行插入,这样可以确保区间树里面不会存在重叠的区域, 这样也确保了 VMA 之间不会存在重叠的情况.
在区间树里,为了加速内核在进程虚拟地址空间里找到一块空闲的区域,其引入了 rb_subtree_gap 统计量,该统计量属于 VMA 的一个成员,用于记录 VMA 在区间树里其子节点里最大 GAP 的值,可以简化为内核需要分配指定长度的虚拟区域时,只需从区间树的根节点触发,以此查找节点对应的 VMA 的 rb_subtree_gap 统计量,就可以快速知道某块区域是否还存在满足需求的空闲区域,如果满足那么内核会继续遍历区间树,直到遍历到叶子节点,这样就可以找到一块空闲的虚拟区域,分配完毕之后在更新区间树的 GAP 信息. 通过这样的优化,区间树可以实现高效的虚拟内存分配和 VMA 查找.
进程地址空间分配虚拟内存
SYS_MMAP 的核心任务是为用户进程分配所需的虚拟内存,由于其灵活性和多变性导致 SYS_MMAP 的逻辑需要满足不同的内存场景,不同场景之间细节存在一定的差异,但整体大逻辑上遵守上图的流程,分别是:
- 参数检测: 该阶段主要任务是检查用户进程传递的参数,对不符合要求的和无法满足的请求进行报错,并且对一些参数进行修正,例如对齐修正,该阶段也会根据用户进程的请求添加相关的标志等.
- 分配虚拟内存: 该阶段主要任务是从用户进程的地址空间对应区间树上,找到空闲的区域,这个过程就是所谓的分配虚拟内存.
- MAP FLAGS TO VMA FLAGS: 该阶段是将用户进程请求的 MAP 相关的 FLAGS 转换成 VMA 使用的 VMA FLAGS,这样利于内核对 VMA 属性的统一管理.
- PROT TO PAGETABLE FLAGS: 该阶段是将用户进程请求时的 PROT_READ/PROTWRITE 等属性转换成页表属性,这样有利于为虚拟内存建立不同属性的区域.
- MERGE VMA: 如果新分配的虚拟内存区域与已经存在的 VMA 相邻,并且虚拟内存的属性和映射方式都相同,那么内核会尽可能将其合并,这里可以新分配的区域将原先孤立的两个 VMA 合并成一个 VMA. MERGE VMA 的另外一层函数是透明大页的合并.
- ALLOC VMA: 如果新分配的虚拟区域与周围的 VMA 属性和映射方式不相同,那么无法进行合并,这时会新分配一个 VMA 描述新分配的虚拟区域,并且将新分配的 VMA 添加到区间树和双链表上.
- EXPAND VMA: 如果新分配的虚拟区域与一个已经存在的 VMA 区域相邻,且能合并,那么进行合并,从另外一个角度来看类似于原来的虚拟区域进行了扩展.
- UNMAP VMA: 如果新分配的虚拟区域与原有的 VMA 区域重叠,并且用户进程支持虚拟区域覆盖,那么内核需要将被覆盖的 VMA 区域进行释放,然后腾挪给新的 VMA.
以上便是 SYS_MMAP 分配虚拟内存的整个流程,如果从分配虚拟内存环节来看,内核所做的操作就是: 从用户进程对应的地址空间区间树中找到指定长度的 GAP,当在不同的架构其实分配虚拟逻辑上还是存在一些差异,例如有的架构优先从用户虚拟地址空间的高地址向下开始查找 GAP,有的架构则是优先从用户进程地址空间的低地址开始向上查找 GAP,针对这两种方式对应的区间树过程如下:
当内核采用自顶向下(TOP TO DOWN)的方式分配新的虚拟内存时,其会从区间树的根节点出发,优先查找右孩子子树,直到找到叶子节点有满足要求的 GAP 为止,具体流程如下:
- A: 内核首先从进程地址空间的区间树开始进行查找,直接检查根节点 VMA 的 rb_subtree_gap 统计值,如果需要分配的新内存大小大于 rb_subtree_gap, 那么说明现在进程地址空间已经没有足够的空闲虚拟内存; 反之则表示进程地址空间还有空闲虚拟内存可以分配,那么其优先查找右子树.
- B: 同理内核先查看 B 节点处 VMA 的 rb_subtree_gap 是否满足分配长度需求,如果不满足则返回上一级进入 A 节点的左孩子子树; 反之如果满足分配长度需求,那么继续进入 B 对应的右孩子子树. 依次类推
- C: 进入到 C 节点之后,与 B 节点的处理逻辑一致,继续查找, 以此类推.
- D: 当遍历到 D 节点时,由于 D 节点是叶子节点,此时内核会继续查看 D 节点对应 VMA 的 rb_subtree_gap, 此时只要该统计值满足分配需求,那么 D 节点就是新分配节点的父节点; 反之则进入到 E 节点,并将 E 节点作为新分配节点的父节点.
整个过程都是基于自顶向下(TOP TO DOWN)的方式分配新内存,这样分配的虚拟内存的起始地址都是很大,并且尽可能填充满高地址区域,那么最高可以分配的虚拟地址则维护在 STRUCT mm_struct 数据结构的 highest_vm_end 成员里,这样也可以解释 D 节点的 GAP 是如何计算的了.
当内核采用自底向上的方式分配新的虚拟内存时,其会从区间树的根节点出发,优先查找左孩子子树,直到找到叶子节点有满足要求的 GAP 为止,具体流程如下:
- A: 内核首先从进程地址空间的区间树开始进行查找,直接检查根节点 VMA 的 rb_subtree_gap 统计值,如果需要分配的新内存大小大于 rb_subtree_gap, 那么说明现在进程地址空间已经没有足够的空闲虚拟内存; 反之则表示进程地址空间还有空闲虚拟内存可以分配,那么其优先查找左子树.
- B: 同理内核先查看 B 节点处 VMA 的 rb_subtree_gap 是否满足分配长度需求,如果不满足则返回上一级进入 A 节点的左孩子子树; 反之如果满足分配长度需求,那么继续进入 B 对应的左孩子子树. 依次类推
- C: 进入到 C 节点之后,查看 C 节点对应 VMA 的 rb_subtree_gap 时发现无法满足分配需求,那么则进入 B 的右子树进行查找
- D: 进入到 D 节点之后,查看 D 节点对应 VMA 的 rb_subtree_gap 是发现满足分配需求,那么优先进入 D 的左子树进行查找.
- E: 当遍历到 E 节点时,由于 E 节点是叶子节点,此时内核会继续查看 E 节点对应 VMA 的 rb_subtree_gap, 此时只要该统计值满足分配需求,那么 E 节点就是新分配节点的父节点; 反之则进入到 D 节点右子树 F 节点,并将 F 节点作为新分配节点的父节点.
整个过程都是基于自底向上(DOWN TO TOP)的方式分配新内存,这样分配的虚拟内存的起始地址都是很小,并且尽可能填充满低地址区域,那么最低可以分配的虚拟地址则维护在 STRUCT mm_struct 数据结构的 mmap_base/mmap_legacy_base 成员里.
MAP FLAGS TO VMA FLAGS
应用程序可以使用 mmap 函数进行虚拟内存的分配,其 ARG3 参数可以通过使用不同的 MAP FLAGS 来设置新虚拟内存的特性,这个参数可以支持多种标志,内核在接受到这些标志之后将其转换成 VMA FLAGS,以此分配特殊功能的虚拟内存,接下来将 MAP FLAGS 到 VMA FLAGS 的转换关系总结如下:
- MAP_SHARED: 建立共享映射,其转换成 VM_SHARED 和 VM_MAYSHARE,那么表示 VMA 是采用共享映射以及可以和其他进程进行共享. 当 fork 发生时,其与子进程共享一个物理页.
- MAP_PRIVATE: 建立私有映射,当 fork 发生时,其不会与子进程共享同一个物理页,其不会转换成对应的 VMA FLAGS,但其会让 VMA FLAGS 不包含 VM_SHARED 和 VM_MAYSHARE.
- MAP_ANONYMOUS: 建立匿名映射,进程无需显示的提供映射的文件就可以分配虚拟内存,其不会转换成对应的 VMA FLAGS
PROT FLAGS TO PGTABLE ATTRIBUTE
匿名映射(Anonymous Mapping)
开发者可能在其他地方看到这些术语: 匿名内存、匿名映射、匿名页等,那么具体指的是什么:
- 匿名映射: 指的是进程分配一段虚拟内存直接映射到系统物理内存的方式,匿名映射是相对与文件映射而言, 其不需要指明映射的文件
- 匿名页: 进程通过匿名映射的物理页,每个物理页都用对应的 STRUCT page 数据结构体,那么可以通过 PageAnon(page) 判断物理页是否为匿名页, 另外如果该物理页只被一个 VMA 映射,那么该物理页也称为独占匿名页(Exclusive Anonymous Page), 同理可以使用 PageAnonExclusive(page) 进行区分
- 匿名内存: 一般将进程通过匿名映射的内存称为匿名内存,其既包括虚拟内存部分,也包括物理内存部分, 其对应的 VMA 通过 vma_is_anonymous() 函数进行判断. 匿名内存可以通过 mmap 方式获得,也可以通过 malloc/brk 从堆上分配,亦或者从堆栈(Stack) 上获得.
指的注意的是,采用匿名映射的虚拟内存不一定都是匿名内存,其也可以是共享内存,或者是 HugeTLB 大页, 当采用采用匿名映射分配匿名内存时,可以采用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志,当采用匿名映射分配共享内存时,可以采用 MAP_SHARED 和 MAP_ANONYMOUS 标志. 严格来说,匿名映射的作用是让用户进程不需要映射文件直接分配虚拟内存,但这里的比较容易搞混淆的是,匿名方式的共享内存虽然没有提供映射的文件,当内核会为该匿名共享内存创建一个伪文件,因此其还是文件映射的一种. 那么匿名映射的作用是什么?
严格意义上来讲,匿名映射针对的是匿名内存,当使用匿名映射之后,VMA 正向映射维护了虚拟内存到物理内存的页表映射,并且其采用 STRUCT anon_vma(简称 AV) 和 STRUCT anon_v-ma_chain(简称 AVC) 与区间树一共维护了匿名页的逆向映射. 因此 AV 和 AVC 是匿名映射的重要标志,因为匿名内存并没有后端文件,因此除了页表无法通过 STRUCT file 或者 STRUCT inode 获得正向映射的关系,因此逆向映射的物理页需要采用与文件映射不同的方式维护逆向映射. 综上所述,逆向映射可以为用户进程直接提供物理内存,并且采用 AV-AVC 区间树维护逆向映射.
文件映射(File Mapping)
相对于匿名映射,文件映射是将文件映射到进程的虚拟地址空间,其中物理内存在其中充当 PAGECACHE 的作用,当虚拟内存与文件之间的 PAGECAHE 映射建立完毕之后,内核会根据一定的策略将 PAGECAHCE 与文件进行同步,那么实现用户进程可以像访问普通虚拟内存一样访问文件.
通常情况下,使用 mmap 系统调用实现文件映射需要提供文件描述符即可,但对于特殊文件映射则不需要提供文件映射,例如匿名共享内存和匿名 HugeTLB 大页等. 两种方法在使用时值得注意的是普通文件映射时不能使用 MAP_ANONYMOUS, 并提供文件描述符. 而对于特殊的文件映射需要使用 MAP_ANONYMOUS, 但不需要提供文件描述符.
除了匿名内存之外的其他类型的虚拟内存,均采用文件映射方式进行构建. VMA 会通过 vm_file 对应一个打开文件 STRUCT file, 而打开的文件指向唯一的 STRUCT inode, 其成员 mapping(STRUCT address_space) 维护了文件相关的正向映射和逆向映射,所谓正向映射除了虚拟内存通过页表映射到物理内存,还可以通过虚拟内存和 XARRAY 数组获得与 PAGECACHE 的映射关系,另外 mapping 基于区间树维护了 PAGECACHE 的逆向映射. 对于共享内存或者 HUGETLB 大页,内核在为进程分配虚拟内存时会为其动态创建伪文件,伪文件位于内存里,因此不需要向真正的文件映射一样定期进行同步.
逆向映射之匿名映射
逆向映射(Reverse Mapping)指的是通过物理地址知道哪些 VMA(虚拟内存) 映射到了该物理页上,对于匿名映射来说,其使用 AV(STRUCT anon_vma) 和 AVC(STRUCT anon_vma_chain) 维护这个关系。逆向映射的核心要解决三个问题: 谁是我的子进程、我是谁的子进程、不破不立.
谁是我的子进程: 正如上图,匿名映射建立的时候会为每个 VMA 创建一个 AV 和一个 AVC,并将 AVC 与 VMA 进行绑定. AV 里维护了一颗红黑树,那么此时匿名映射将 AVC 加入到该红黑树里, 最后将 VMA 指向 AV. 每当父进程调用 FORK 创建子进程时,FORK 会为匿名映射的 VMA 创建一个新的 AVC,然后将新的 AVC 与子进程 VMA 进行绑定,绑定之后再将 AVC 插入到父进程 AV 的红黑树里。此时父进程想看哪些子进程映射了匿名映射 VMA,那么只需遍历 AV 里的红黑树即可.
我是谁的子进程: 正如上图, 每个 VMA 还维护一个 anon_vma_chain 链表,该链表上维护了很多 AVC,通过遍历该链表可以知道该 VMA 映射了哪些父进程的 VMA. 这是怎么做到呢? 当父进程 FORK 一个新进程时,父进程会遍历其 VMA 对应的 anon_vma_chain 链表,通过该链表可以知道父进程共享哪些父父进程的 VMA,每次遍历一个 AVC 时,子进程就创建一个 AVC 将其插入到遍历的父进程 AV 红黑树里,并将该 AVC 也加入到自己的 anon_vma_chain 链表里,那么子进程的 VMA 只要遍历 anon_vma_chain 就可以知道其共享了哪些父进程的 VMA.
不破不立: 正如上图,当子进程对匿名映射的内存发起写操作时,会触发缺页异常并执行 COW 机制,即新分配一个物理页,将父进程物理页内容拷贝到新的物理页里,最后再将页表更新到新的物理页上,这样正向映射就得到更新. 对于逆向映射子进程无需在维护 anon_vma_chain 链表,因为匿名页是全新的物理页,因此子进程只需维护自己的 AV 即可,此时将物理页指向 AV 即可获得逆向映射相关的信息.
逆向映射之文件映射
文件映射里的逆向映射相对来说简单很多,因为内核维护了一套 VMA 到 STRUCT inode 的唯一路径,因此无论多少进程共享或者打开这个文件映射内存,都可以找到唯一的 STRUCT inode 数据结构,那么内核干脆在 STRUCT inode 上维护了 STRUCT address_space 数据结构,该数据结构用于维护虚拟内存与 PAGECACHE 之间的关系,这里的关系详细解读就是正向映射关系和逆向映射关系.
当父进程调用 FORK 系统调用创建子进程时,FORK 系统调用会将子进程的 VMA 加入到 STRUCT address_space 维护的区间树里,然后将 PAGECACHE 指向 STRUCT address_space 即可,那么物理页只要找到 STRUCT address_space,然后提供虚拟地址范围遍历区间树,即可知道哪些 VMA 映射到该 PAGECACHE 上.
MEMORY With MMAP
SYS_MMAP 系统调用提供了灵活的参数,以满足不同的内存分配需求,调用者可以根据实际需求分配不同类型的虚拟内存,那么接下来本节重点介绍如何使用 mmap 函数分配不同类型的虚拟内存.
匿名内存
分配匿名内存的函数很多,其中最常用的是 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.
共享内存
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 里可以使用 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_PRIVATE(私有映射),当不能包含 MAP_ANONYMOUS 标志,否则变成匿名内存.
- ARG4: 映射文件描述符,该这段必须是需要映射文件的描述符.
- ARG5: 指明映射文件相对文件开头的偏移.
HugeTLB 大页内存
在 Linux 里可以使用 mmap 系统调用分配 HugeTLB 大页虚拟内存,mmap 提供了很多参数可以灵活设置文件映射内存属性,具体如下:
- ARG0: MAP_VADDR 可以指定 HugeTLB 大页内存的虚拟地址,该值为 NULL 是代表分配一个随机的虚拟地址,当该值为指定值时代表从指定的地址分配虚拟内存,该地 址可能已经被分配出去,使用时调用者需要确保虚拟地址被覆盖.
- ARG1: MAP_SIZE 指明了需要分配 HugeTLB 大页内存的大小,长度为 PMD_SIZE/PUD_SIZE 的倍数,不能为 0.
- ARG2: 指明了 HugeTLB 内存的访问权限,其值可以是这些标志的合集: PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行) 和 PROT_NONE(不可访问)
- ARG3: 指明映射类型,当采用匿名映射的 HugeTLB 大页内存,该字段必须包含是 MAP_HUGETLB 和 MAP_ANONYMOUS 标志. 如果采用文件映射的 HugeTLB 大页内存,该字段只能包括 MAP_SHARED, 不能包括 MAP_ANONYMOUS 标志,否则无法映射.
- ARG4: 映射文件描述符,当采用文件映射的 HugeTLB 大页内存时需要提供大页文件的描述符; 采用匿名映射的 HugeTLB 大页内存则填充 -1 即可.
- ARG5: 指明映射文件相对文件开头的偏移.
透明大页内存
分配匿名 THP 内存的函数与分配匿名内存一样,代码层面无法感知,其中最常用的是 mmap,由于其提供了多种配置参数,可以分配更灵活的匿名 THP 内存。当使用 mmap 系统调用分配匿名 THP 内存时,各参数的含义如下:
- ARG0: MAP_VADDR 可以设置匿名 THP 内存的虚拟地址,该值为 NULL 是代表分配一个随机的虚拟地址,当该值为指定值时代表从指定的地址分配虚拟内存,该地址可能已经被分配出去,使用时调用者需要确保虚拟地址被覆盖.
- ARG1: MAP_SIZE 指明了需要分配匿名 THP 内存的大小,长度为 PMD_SIZE 的倍数,不能为 0.
- ARG2: 指明了匿名 THP 内存的访问权限,其值可以是这些标志的合集: PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行) 和 PROT_NONE(不可访问)
- ARG3: 指明映射类型,该字段必须包含是 MAP_PRIVATE 和 MAP_ANONYMOUS 标志,否则无法分配到匿名 THP 内存.
- ARG4: 映射文件描述符,对于匿名 THP 内存由于其没有后端文件,因此这里必须为 -1.
- ARG5: 指明映射的物理区域,由于匿名内存映射的随机物理内存,因此该字段必须为 0.
PFN/MMIO 内存
SYS_MMAP 系统调用可以将外设的 MMIO 或者 RSVDMEM,但这里依赖系统提供 “/dev/mem” 文件,然后采用文件映射方式进行分配,当使用 mmap 系统调用分配 PFN/MMIO 内存时,各参数的含义如下:
- ARG0: MAP_VADDR 可以设置 PFN/MMIO 映射的虚拟地址,该值为 NULL 是代表分配一个随机的虚拟地址,当该值为指定值时代表从指定的地址分配虚拟内存,该地址可能已经被分配出去,使用时调用者需要确保虚拟地址被覆盖.
- ARG1: MAP_SIZE 指明了需要分配虚拟内存的大小,长度为 PAGE_SIZE 的倍数,不能为 0.
- ARG2: 指明了映射内存的访问权限,其值可以是这些标志的合集: PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行) 和 PROT_NONE(不可访问)
- ARG3: 指明映射类型,该字段可以是 MAP_PRIVATE 或者是 MAP_SHARED.
- ARG4: 映射文件描述符,必须是 “/dev/mem” 的文件描述符.
- ARG5: 指明映射的物理区域,调用者需提前准备好映射的 PFN 和 MMIO.