linux 源码学习 - 内存管理
linux 版本为 v6.18
结构体定义
主要涉及 include/linux/mm_types.h,定义了内存管理中所需的基本数据结构
空间优化
在内核代码的结构体定义中,有一种广泛使用的设计是这样的:
1 | struct Foo { |
也就是利用匿名结构体和匿名联合体的结合,这样写不仅可以利用顶层 struct 的变量直接访问底层变量,还能够充分发挥 union 的空间优势
以内存页元数据结构体为例,通常情况下物理页的大小是固定的 4KB,但同一个物理页可以有不同的用途,例如文件缓存、匿名内存(进程堆栈)、DMA 缓冲区、swap 缓存、网络栈页面池等,不同用途下页面所需要的元数据也是不同的,因此一种直接的思路是为每一种用途设计一个对应的结构体,例如 struct file_cache_page,struct anomyous_page 等等
然而,在内核开发中,时间和空间复杂度的优先级是高于代码简洁性与易读性的,如果按照上面的思路来实现,那就无法用一个数组来存储所有页的元数据,从 pfn 映射到元数据的开销会有所上升;并且当有一个物理页的用途发生变化的时候,就需要有结构体的析构和构造,这种开销在内核中是无法接受的
linux 采用的这种实现实际上是用 C 语言实现了一种特殊的“多态”,对于内存空间进行了极致压缩,在同一块内存空间中存储逻辑上互斥访问的多条数据
page 和 folio
struct page 是 linux 内核最基础的内存描述单元,一个 page 对应一个物理页帧(大小通常为 4KB)
现如今,由于内存变得越来越大,使用 4KB 作为页大小很多时候会导致性能问题,例如内核或用户进程需要 2MB 的物理内存时,4KB 的页会导致至少要 512 次 TLB miss 和缺页异常才能够完全建立页表
为了解决这个问题,内核引入了 compound_page 设计,一个 compound_page 代表一组连续的物理页面,由一个 head_page 和若干 tail_page 组成,其元数据由 head_page 对应的 struct page 管理,具体的实现方法仍然是在 struct page 中添加 union,这就导致了 struct page 语义严重过载,在使用的时候往往要增加一系列繁杂的判断
因此,在 linux 5.16 中引入了 struct folio,它代表一个高阶连续物理内存单元(大小为 个页),将传统上分散在多个 page 结构体中的逻辑聚合为一个统一对象
下面来看二者的具体实现:
page 实现
1 | struct page { |
只保留了一些很基础的字段,例如第一个 union 中的若干个匿名 struct,实际上就表示了这一个 page 的不同用途,在不通过用途下按照不同的方式来解释数据,比如如果访问了 page.buddy_list,就表示了这一个 page 已经不再被使用,并且位于伙伴系统;而访问 page.lru 则代表它正在被使用,并且使用 LRU 算法决定要不要被置换;当访问 page.compound_head 的时候就说明这一页是隶属于某个复合页,可以通过这个变量访问对应的 head_page
mapping 这个字段可以用于区分页缓存和匿名页,这里 mapping 的最低位如果是 1 的话,就代表这是一个匿名页,mapping 指向的地址是一个 struct anon_vma,由于内核中的结构体至少需要按照字节对齐,因此结构体指针最后的几位 bit 通常情况下并未被使用,因此用来存储这个信息是安全的
结构体中有两个易混的变量,_mapcount 和 _refcount,可以看出 _refcount 并不在 union 中,因此所有页面都有这个属性,它代表的是内核中有多少个地方正在引用这个物理页(例如一页刚被 alloc 的时候就会增加这个值);而 _mapcount 代表的是一个数据页被多少个进程映射了,非数据页(例如页表页、伙伴系统空闲页)则不需要这个字段,而是被 page_type 代替
最后是 virtual 字段,这个字段是为了解决 32 位机器上内核虚拟地址无法和物理地址一一映射的问题。在 32 位机器上,虚拟地址空间共有 4GB,通常会分配 1GB 给内核地址空间,因此如果物理内存大于 1GB,则无法在内核启动时就建立好一一映射的静态页表,因此 linux 将物理内存分为了两个部分,第一部分是低内存(通常 896MB),这部分物理地址和内核虚拟地址之间是线性映射的关系;第二部分则是高内存 highmem,当内核想要访问高内存的时候,就需要找到一个空闲的虚拟地址,并且建立该虚拟地址和要访问的物理页之间的临时映射,这个虚拟地址就存在 virtual 里面。由于 64 位机器的虚拟地址空间高达 16 EB,因此不需要考虑这个问题了
folio 实现
1 | struct folio { |
struct folio 是设计用于解决复合页和大页的开销,简单来看它的内存布局似乎是 4 个 struct page 的组合,并且在后续利用了大量的 assert 来保证内存结构的一致性,这样保证的是在出现 struct folio 的时候,它一定对应着 head page,因此可以显著减少分支判断,而内存布局的一致性则保证了可以将 struct page* 和 struct folio* 之间灵活的转换,转换代码在 include/linux/page-flags.h 中,具体来说:
1 |
其中利用了泛型语法以保持常量属性
此外还有 struct ptdesc,这是将 struct page 描述页表页的功能独立出来,这里就不说了
mm_struct
struct mm_struct 是描述进程整个内存布局的结构体