linux 源码学习 - 内存管理

linux 版本为 v6.18

结构体定义

主要涉及 include/linux/mm_types.h,定义了内存管理中所需的基本数据结构

空间优化

在内核代码的结构体定义中,有一种广泛使用的设计是这样的:

1
2
3
4
5
6
7
8
9
struct Foo {
...
union {
struct {...};
struct {...};
...
};
...
};

也就是利用匿名结构体和匿名联合体的结合,这样写不仅可以利用顶层 struct 的变量直接访问底层变量,还能够充分发挥 union 的空间优势

以内存页元数据结构体为例,通常情况下物理页的大小是固定的 4KB,但同一个物理页可以有不同的用途,例如文件缓存、匿名内存(进程堆栈)、DMA 缓冲区、swap 缓存、网络栈页面池等,不同用途下页面所需要的元数据也是不同的,因此一种直接的思路是为每一种用途设计一个对应的结构体,例如 struct file_cache_pagestruct 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,它代表一个高阶连续物理内存单元(大小为 2n2^n 个页),将传统上分散在多个 page 结构体中的逻辑聚合为一个统一对象

下面来看二者的具体实现:

page 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct page {
memdesc_flags_t flags;
/*
* WARNING: bit 0 of the first word is used for PageTail().
*/
union {
struct { /* Page cache and anonymous pages */
union {
struct list_head lru;
struct list_head buddy_list;
struct list_head pcp_list;
struct llist_node pcp_llist;
};
struct address_space *mapping;
...
};
struct { /* page_pool used by netstack */
...
};
struct { /* Tail pages of compound page */
unsigned long compound_head; /* Bit zero is set */
};
struct { /* ZONE_DEVICE pages */
...
};
/** @rcu_head: You can use this to free a page by RCU. */
struct rcu_head rcu_head;
};
union {
unsigned int page_type;
atomic_t _mapcount;
};

/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
atomic_t _refcount;
...
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif
...
} _struct_page_alignment;

只保留了一些很基础的字段,例如第一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct folio {
union {
struct {
memdesc_flags_t flags;
union {
struct list_head lru;
struct {
void *__filler;
unsigned int mlock_count;
};
struct dev_pagemap *pgmap;
};
struct address_space *mapping;
union {
pgoff_t index;
unsigned long share;
};
union {
void *private;
swp_entry_t swap;
};
atomic_t _mapcount;
atomic_t _refcount;
...
};
struct page page;
};
union {
struct {...};
struct page __page_1;
};
union {
struct {...};
struct page __page_2;
};
union {
struct {...};
struct page __page_3;
};
};

struct folio 是设计用于解决复合页和大页的开销,简单来看它的内存布局似乎是 4 个 struct page 的组合,并且在后续利用了大量的 assert 来保证内存结构的一致性,这样保证的是在出现 struct folio 的时候,它一定对应着 head page,因此可以显著减少分支判断,而内存布局的一致性则保证了可以将 struct page*struct folio* 之间灵活的转换,转换代码在 include/linux/page-flags.h 中,具体来说:

1
2
3
4
5
#define page_folio(p)		(_Generic((p),				\
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))

#define folio_page(folio, n) (&(folio)->page + (n))

其中利用了泛型语法以保持常量属性

此外还有 struct ptdesc,这是将 struct page 描述页表页的功能独立出来,这里就不说了

mm_struct

struct mm_struct 是描述进程整个内存布局的结构体


© 2024 本网站由 Ywang22 使用 Stellar主题 创建
总访问 次 | 本页访问
共发表 81 篇 Blog(s) · 总计 188k 字