【OS.0x02】Linux 内核内存管理I - 页、区、节点

本文最后更新于:2023年10月27日 晚上

无内鬼,来点内存条

0x00.一切开始之前

本系列文章将通过 Linux 5.11 的源代码简要分析 Linux 内核中的内存管理(memory management)部分,笔者选择采用自底向上的方式来逐层分析,本篇文章便从最基础的页框开始进行分析

Overview

这是一张十分经典的 _Overview_,自顶向下是

  • 节点(node,对应结构体 pgdata_list)
  • (zone,对应结构体 zone,图上展示了三种类型的 zone)
  • (page,对应结构体 page)

image.png

我们可以通过 cat /proc/buddyinfocat /proc/pagetypeinfo 查看页面相关信息:

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
arttnba3@ubuntu:~$ sudo cat /proc/buddyinfo 
Node 0, zone DMA 0 1 1 0 2 1 1 0 1 2 2
Node 0, zone DMA32 8706 1386 748 543 232 48 39 9 0 0 0
Node 0, zone Normal 15391 3317 877 826 221 77 14 2 0 0 0
arttnba3@ubuntu:~$ sudo cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 0, zone DMA, type Unmovable 0 1 1 0 2 1 1 0 1 1 0
Node 0, zone DMA, type Movable 0 0 0 0 0 0 0 0 0 1 2
Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Unmovable 254 166 54 33 18 8 11 1 0 0 0
Node 0, zone DMA32, type Movable 6762 740 535 278 43 3 3 2 0 0 0
Node 0, zone DMA32, type Reclaimable 1690 480 159 232 171 37 25 6 0 0 0
Node 0, zone DMA32, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 27 30 15 0 1 4 4 2 0 0 0
Node 0, zone Normal, type Movable 12963 3039 806 727 197 68 10 0 0 0 0
Node 0, zone Normal, type Reclaimable 1135 251 56 99 23 5 0 0 0 0 0
Node 0, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic Isolate
Node 0, zone DMA 3 5 0 0 0
Node 0, zone DMA32 60 1382 86 0 0
Node 0, zone Normal 245 4270 93 0 0

0x01.struct page:页

Linux kernel 中使用 page 结构体来表示一个物理页框,每个物理页框都有着一个对应的 page 结构体,为了节省内存空间,其定义中使用了大量的联合体

一个 page 结构体的大小为 64B,若是每个物理页框大小为 4KB,则仅需要牺牲 1.5625% 的空间存储 page 结构体

在这里给出一张 struct page 的overview

网上找的图,侵删

image.png

该结构体定义于内核源码 include/linux/mm_types.h 中,如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
struct page {
// flags 用以存储该 page 的标志位,每一个位表示一种状态,故一张页可以有 32 种状态,这些状态定义于 include/linux/page-flags.h 中
unsigned long flags; /* 原子变量 flag,也可能被异步更新 */
/*
* 该联合体中 5 个 word(32 位系统20字节/64位系统40字节)是可用的
* 警告:第一个 word 的 0 bit 供 PageTail()使用
* 这意味着其他用户使用该结构体时【禁止】使用该 bit
* 以避免碰撞和误判 PageTail().
*/
union {
struct { /* 页缓存与匿名页 */
/**
* @lru: Pageout 链表, 例如 active_list 便由
* lruvec->lru_lock 保护。
* 有时会被页所有者作为常规链表使用。
*/
struct list_head lru;
/* See page-flags.h for PAGE_MAPPING_FLAGS */
struct address_space *mapping;
pgoff_t index; /* 在映射的虚拟空间(vma_area)内的偏移 */
/**
* @private: 私有映射的非透明数据
* 在 PagePrivate 中通常用于 buffer_heads.
* 在 PageSwapCache 中用于 swp_entry_t
* 在 PageBuddy 中指定在 buddy system 中的次序
*/
unsigned long private;
};
struct { /* page_pool used by netstack */
/**
* @dma_addr: 在 32 位机器上仍可能需要 64 位的空间
*/
dma_addr_t dma_addr;
};
struct { /* 供 slab, slob and slub 使用 */
union {
struct list_head slab_list;
struct { /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* 剩余的页数量 */
int pobjects; /* 近似计数 */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* 不在 slob 中使用 */
/* 两个 word 的范围 */
void *freelist; /* 第一个空闲对象 */
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
struct { /* 复合页的尾页 */
// 复合页 (compound page)即为将多个物理连续页框视作一个大页
unsigned long compound_head; /* Bit zero is set */

/* First tail page only */
unsigned char compound_dtor;
unsigned char compound_order;
atomic_t compound_mapcount;
unsigned int compound_nr; /* 1 << compound_order */
};
struct { /* Second tail page of compound page */
unsigned long _compound_pad_1; /* compound_head */
atomic_t hpage_pinned_refcount;
/* For both global and memcg */
struct list_head deferred_list;
};
struct { /* 页表页面 */
unsigned long _pt_pad_1; /* compound_head */
pgtable_t pmd_huge_pte; /* protected by page->ptl */
unsigned long _pt_pad_2; /* mapping */
union {
struct mm_struct *pt_mm; /* 用于 x86 的全局目录表(pgd) */
atomic_t pt_frag_refcount; /* 用于 powerpc 架构 */
};
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
};
struct { /* ZONE_DEVICE pages */
/** @pgmap: Points to the hosting device page map. */
struct dev_pagemap *pgmap;
void *zone_device_data;
/*
* ZONE_DEVICE private pages are counted as being
* mapped so the next 3 words hold the mapping, index,
* and private fields from the source anonymous or
* page cache page while the page is migrated to device
* private memory.
* ZONE_DEVICE MEMORY_DEVICE_FS_DAX pages also
* use the mapping, index, and private fields when
* pmem backed DAX files are mapped.
*/
};

/** @rcu_head: 你可以通过该成员以通过 RCU 释放内存页 */
struct rcu_head rcu_head;
};

union { /* 这个联合体占用四个字节 */
/*
* 若是这个页被映射到用户空间, 记录该页被页表引用的次数
*/
// 每个进程有其独立的页表,故可以理解为该值记录了该页被多少个进程共享,初始值为 -1
atomic_t _mapcount;

/*
* 若是该页既不是 PageSlab 也没有被映射到用户空间,
* 则该值会帮助决定该页的作用。
* 该处的页面类型列表参见 page-flags.h
*/
unsigned int page_type;

unsigned int active; /* SLAB */
int units; /* SLOB */
};

/* 使用计数. 【不要直接使用】. 参见 page_ref.h */
atomic_t _refcount;

#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif

/*
* 当机器上的所有内存都被映射到内核空间时,
* 我们可以简单地计算其虚拟地址。
* 在有着【高端内存(大于896MB)】的机器上,有的内存被动态地映射到内核
* 虚拟空间中,因此我们需要一个地方来存储这个地址
* 在 x86 机器上这个域可能占 16 bit 的空间 ... ;)
*
* 乘法计算较慢的架构可以在 asm/page.h 中定义 WANT_PAGE_VIRTUAL
*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* 内核虚拟地址 (若非 kmapped 则为 NULL, 即高端内存) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
} _struct_page_alignment;

I.几个比较重要的字段

简单讲讲其中几个重要的成员

lru:LRU 链表节点

lru 即 Least Recently Used,在操作系统课程上我们已经学习了这个页面置换算法的概念,这里不再过多赘叙

在 Linux 内核中,page 结构体通过其 lru 字段组织成链表,如下图所示

image.png

lru 成员是一个 struct list_head 类型,这是内核中通用的双向链表节点结构

**slab相关结构体**

在 page 结构体中专门有着一个匿名结构体用于存放与 slab 相关的成员

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
struct {    /* 供 slab, slob and slub 使用 */
union {
struct list_head slab_list;
struct { /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* 剩余的页数量 */
int pobjects; /* 近似计数 */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* 不在 slob 中使用 */
/* 两个 word 的范围 */
void *freelist; /* 第一个空闲对象 */
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};

Linux kernel 中的 slab allocator 一共有三种:slab、slob、slub,其中比较常用的是 slub 分配器,关于 slab allocator 将在后续的文章中进行更为详细的叙述,下图是一张 slub 分配器的 overview

image.png

flags:标志位

即该页的标志位成员,用以表示该页所处在的状态,每一个位表示一种状态,故一张页可以有 32 种不同的状态,这些状态定义于 include/linux/page-flags.h 中,该字段与体系无关

我们可以通过该头文件中定义的枚举类型获取相应的值:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
enum pageflags {
PG_locked, /* Page is locked. Don't touch. */
PG_referenced,
PG_uptodate,
PG_dirty,
PG_lru,
PG_active,
PG_workingset,
PG_waiters, /* Page has waiters, check its waitqueue. Must be bit #7 and in the same byte as "PG_locked" */
PG_error,
PG_slab,
PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/
PG_arch_1,
PG_reserved,
PG_private, /* If pagecache, has fs-private data */
PG_private_2, /* If pagecache, has fs aux data */
PG_writeback, /* Page is under writeback */
PG_head, /* A head page */
PG_mappedtodisk, /* Has blocks allocated on-disk */
PG_reclaim, /* To be reclaimed asap */
PG_swapbacked, /* Page is backed by RAM/swap */
PG_unevictable, /* Page is "unevictable" */
#ifdef CONFIG_MMU
PG_mlocked, /* Page is vma mlocked */
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
PG_uncached, /* Page has been mapped as uncached */
#endif
#ifdef CONFIG_MEMORY_FAILURE
PG_hwpoison, /* hardware poisoned page. Don't touch */
#endif
#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT)
PG_young,
PG_idle,
#endif
#ifdef CONFIG_64BIT
PG_arch_2,
#endif
__NR_PAGEFLAGS,

/* Filesystems */
PG_checked = PG_owner_priv_1,

/* SwapBacked */
PG_swapcache = PG_owner_priv_1, /* Swap page: swp_entry_t in private */

/* Two page bits are conscripted by FS-Cache to maintain local caching
* state. These bits are set on pages belonging to the netfs's inodes
* when those inodes are being locally cached.
*/
PG_fscache = PG_private_2, /* page backed by cache */

/* XEN */
/* Pinned in Xen as a read-only pagetable page. */
PG_pinned = PG_owner_priv_1,
/* Pinned as part of domain save (see xen_mm_pin_all()). */
PG_savepinned = PG_dirty,
/* Has a grant mapping of another (foreign) domain's page. */
PG_foreign = PG_owner_priv_1,
/* Remapped by swiotlb-xen. */
PG_xen_remapped = PG_owner_priv_1,

/* SLOB */
PG_slob_free = PG_private,

/* Compound pages. Stored in first tail page's flags */
PG_double_map = PG_workingset,

/* non-lru isolated movable page */
PG_isolated = PG_reclaim,

/* Only valid for buddy pages. Used to track pages that are reported */
PG_reported = PG_uptodate,
};
  • PG_locked:该页已被上锁,说明此时该页正在被使用
  • PG_referenced:该页刚刚被访问过;该标志位与 PG_reclaim 标志位共同被用于匿名与文件备份缓存的页面回收
  • PG_uptodate:该页处在最新状态(up-to-date);当对该页完成一次读取时,该页便变更为 up-to-date 状态,除非发生了磁盘 IO 错误
  • PG_dirty:该页为脏页,即该页的内容已被修改,应当尽快将内容写回磁盘上
  • PG_lru:该页处在一个 LRU 链表上
  • PG_active:该页面位于活跃 lru 链表中
  • PG_workingset:该页位于某个进程的 working set(工作集,即一个进程同时使用的内存数量,例如一个进程可能分配了114514MB内存,但是在同一时刻只使用其中的1919MB,这就是工作集)中
  • PG_waiters:有进程在等待该页面
  • PG_error:该页在 I/O 过程中出现了差错
  • PG_slab:该页由 slab 使用
  • PG_owner_priv_1:该页由其所有者使用,若是作为 pagecache 页面,则可能是被文件系统使用
  • PG_arch_1:该标志位与体系结构相关联
  • PG_reserved:该页被保留,不能够被 swap out(内核会将不活跃的页交换到磁盘上)
  • PG_private && PG_private2:该页拥有私有数据(private 字段)
  • PG_writeback:该页正在被写到磁盘上
  • PG_head:在内核中有时需要将多个页组成一个 compound pages,而设置该状态时表明该页是 compound pages 的第一个页
  • PG_mappedtodisk:该页被映射到硬盘中
  • PG_reclaim:该页可以被回收
  • PG_swapbacked:该页的后备存储器为 swap/RAM
  • PG_unevictable:该页不可被回收(被锁),且会出现在 LRU_UNEVICTABLE 链表中
  • PG_mlocked:该页被对应的 vma 上锁(通常是系统调用 mlock)
  • PG_uncached:该页被设置为不可缓存
  • PG_hwpoison:硬件相关的标志位
  • PG_young
  • PG_idle
  • PG_arch_2:64位下的体系结构相关标志位

flags 内存复用

为了节省空间,flags 字段除了用作标志位外还给其他结构使用,其划分的形式其实与内核配置的内存模型有关,在 include\linux\page-flags-layout.h 文件中描述了五种划分形式(其实是三大种)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* page->flags layout:
*
* There are five possibilities for how page->flags get laid out. The first
* pair is for the normal case without sparsemem. The second pair is for
* sparsemem when there is plenty of space for node and section information.
* The last is when there is insufficient space in page->flags and a separate
* lookup is necessary.
*
* No sparsemem or sparsemem vmemmap: | NODE | ZONE | ... | FLAGS |
* " plus space for last_cpupid: | NODE | ZONE | LAST_CPUPID ... | FLAGS |
* classic sparse with space for node:| SECTION | NODE | ZONE | ... | FLAGS |
* " plus space for last_cpupid: | SECTION | NODE | ZONE | LAST_CPUPID ... | FLAGS |
* classic sparse no space for node: | SECTION | ZONE | ... | FLAGS |
*/
非 sparse 内存模式 / sparse vmemmap 内存模式

如下图所示,低位用作该 page 的 flag,高位分别标识其归属的 zone, node id(非 NUMA 系统中为0),中间剩余的位保留

image.png

这种形式中若是开启了 last_cpuid 则是下面这个样子:

image.png

sparse 内存模式

如下图所示,相比起第一种形式多了一个 SECTION 字段标识其归属的 mem_section

image.png

若是开启了 last_cpuid 则是下面这个样子

image.png

没有 Node 的 sparse 内存模式

主要是针对非 NUMA 设计的,在这种模式下取消了 Node 结构

image.png

_mapcount:映射计数

记录该页被页表映射的次数,每个进程有其独立的页表,故可以理解为该值记录了该页被多少个进程共享,其初始值为 -1

由于这是一个联合体,若是该页既不是 PageSlab 也没有被映射到用户空间,则为 page_type 字段,具体说明定义于 /include/linux/page-flags.h 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* For pages that are never mapped to userspace (and aren't PageSlab),
* page_type may be used. Because it is initialised to -1, we invert the
* sense of the bit, so __SetPageFoo *clears* the bit used for PageFoo, and
* __ClearPageFoo *sets* the bit used for PageFoo. We reserve a few high and
* low bits so that an underflow or overflow of page_mapcount() won't be
* mistaken for a page type value.
*/

#define PAGE_TYPE_BASE 0xf0000000
/* Reserve 0x0000007f to catch underflows of page_mapcount */
#define PAGE_MAPCOUNT_RESERVE -128
#define PG_buddy 0x00000080
#define PG_offline 0x00000100
#define PG_table 0x00000200
#define PG_guard 0x00000400

_refcount:引用计数

该字段用作该页面在内核中的引用计数器,初始时页面为空闲状态,该计数器为 0,每当该页面被分配引用时计数器会 + 1,被其他页面进行引用时也会 + 1

当引用计数器为 0 时表示该页面为空闲状态或即将要被释放,若大于 0 则表示正在被使用,暂时不会释放

内核中提供了两个函数 get_page()put_page() 来进行引用计数的增减,后者在引用计数器为 1 时会调用 __put_single_page() 释放该页面(1->0,该页面已空闲)

**virtual:虚拟地址**

该字段为该物理页框对应的的虚拟地址,那么这里又要放上这张经典的图:

image.png

每一个 struct page 对应一个物理页框,那么这个 virtual 字段其实就是上图的反向映射

II.不同内存模型下的 struct page 存储方式

Linux 提供了三种内存模型,定义于 include/asm-generic/memory_model.h 中,如下图所示(偷的图,侵删):

image.png

内存模型在编译期就会被确定下来,目前常用的是 Sparse Memory 模型,即离散内存模型

Flat Memory

平滑内存模型。物理内存地址连续,有一个全局变量 mem_map ——由一个大的 struct page 数组直接对应现有的物理内存

Discontiguous Memory

非连续性内存模型。主要针对内存中存在空洞的情况。

对于每一段连续的物理内存,都有一个 pglist_data 结构体进行对应,其成员 node_mem_map 为一个 struct page 指针,指向一个 page 结构体数组,由该结构体对应到该段连续物理内存

有一个全局变量 node_data 为一个 pglist_data 指针数组,其中存放着指向每一个 pglist_data 的指针,该数组的大小为 MAX_NUMNODES

Sparse Memory

离散内存模型。在一个 mem_section 结构体中存在一个 section_mem_map 成员指向一个 struct page 数组对应一段连续的物理内存,即将内存按照 section 为单位进行分段

存在一个全局指针数组 mem_section (与结构体同名)存放所有的 mem_section 指针,指向理论上支持的内存空间,每个 section 对应的物理内存不一定存在,若不存在则此时该 section 的指针为 NULL

这种模型支持内存的热拔插

图还是偷的,侵删

image.png

mem_section 结构体

该结构体定义于 /include/linux/mmzone.h 中,如下:

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
struct mem_section {
/*
* 逻辑上这指向一个 pages 结构体数组,
* 然而,他的存储还有一些别的魔力
* (参见 sparse.c::sparse_init_one_section())
*
* 此外,在引导的早期,我们对此处节区的位置的
* 节点的id进行编码,以指引分配。
* (参见 sparse.c::memory_present())
*
* 将之声明为一个 unsigned long,至少可以让人在
* 错误使用之前完成一次(类型)转换
*/
unsigned long section_mem_map;

struct mem_section_usage *usage;
#ifdef CONFIG_PAGE_EXTENSION
/*
* 若是 SPARSEMEM, pgdat 没有 page_ext 指针.
* 我们使用 section. (关于这个,参见 page_ext.h)
*/
struct page_ext *page_ext;
unsigned long pad;
#endif
/*
* 警告: mem_section 的大小必须是2的幂次方, 以便于
* 让计算与使用 SECTION_ROOT_MASK 有意义
*/
};

_CONFIG_SPARSEMEM_EXTREME_:动态分配 mem_section 数组

内核编译选项之一,若开启了则连 mem_section 数组的空间也是动态分配的,在 section 较多的情况下通常会开启这个编译选项

全局 mem_section 数组

该数组中存放着指向所有 mem_section 结构体的指针,定义于 /mm/sparse.c 中,如下:

1
2
3
4
5
6
#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
____cacheline_internodealigned_in_smp;
#endif

若未开启 CONFIG_SPARSEMEM_EXTREME 编译选项则 mem_section 为一个常规的二维数组,否则为一个二级指针,其所指向空间内存动态分配

对于后一种情况,其结构如下图所示:

自己画的图.png

PFN 与 page 结构体间的转换

kernel 中提供了两个用以在 PFN(Page Frame Numer) 与 page 结构体之间进行转换的宏,定义于 /include/asm-generic/memory_model.h 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#elif defined(CONFIG_SPARSEMEM)
/*
* Note: 节区的 mem_map 被编码以表示其 start_pfn.
* section[i].section_mem_map == mem_map's address - start_pfn;
*/
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
int __sec = page_to_section(__pg); \
(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \
})

#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#endif /* CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM */

在这里我们需要注意一点:mem_section 结构体的 section_mem_map 中存储的为 page 数组与 PFN 的差值

(1)page 结构体到 PFN:page 结构体地址减去对应 mem_section->section_mem_map

该宏首先会使用 page_to_section() 通过 page 结构体的 flags 字段获取该 page 所属的 section 标号,该函数定义于 /include/linux/mm.h 中,如下:

1
2
3
4
static inline unsigned long page_to_section(const struct page *page)
{
return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}

之后使用 __nr_to_section() 来获取对应的 mem_section 结构体的地址,该函数定义于 /include/linux/mmzone.h 中,如下:

1
2
3
4
5
6
7
8
9
10
static inline struct mem_section *__nr_to_section(unsigned long nr)
{
#ifdef CONFIG_SPARSEMEM_EXTREME
if (!mem_section)
return NULL;
#endif
if (!mem_section[SECTION_NR_TO_ROOT(nr)])
return NULL;
return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}

这里用到一个宏 SEECTION_NR_TO_ROOT,定义如下:

1
2
3
4
5
6
7
#ifdef CONFIG_SPARSEMEM_EXTREME
#define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section))
#else
#define SECTIONS_PER_ROOT 1
#endif

#define SECTION_NR_TO_ROOT(sec) ((sec) / SECTIONS_PER_ROOT)

我们默认开启 CONFIG_SPARSEMEM_EXTREME,此时 SECTION_PER_ROOT 意为一张页中 mem_section 结构体的数量,即宏 SEECTION_NR_TO_ROOT 得到的是对应的_页下标_,之后再通过 mem_section 标号与每页中 mem_section 数量 - 1(SECTION_ROOT_MASK)做与运算最终得到该 mem_section 在该页这一 mem_section 数组中的下标

之后通过 __section_mem_map_addr() 获取到 mem_section 结构体中的 section_mem_map 成员,该函数定义于 /include/linux/mmzone.h 中,如下:

1
2
3
4
5
6
static inline struct page *__section_mem_map_addr(struct mem_section *section)
{
unsigned long map = section->section_mem_map;
map &= SECTION_MAP_MASK;
return (struct page *)map;
}

最后与 page 结构体的地址做差运算便能获得其 PFN,需要注意的是在这里进行的是 page 结构体指针间的运算 而非简单的地址加减法,计算过程为:
$$
address_{struct\ page} - section_mem_map = address_{struct\ page} - (address_{mem_map} - start_PFN)\
=(address_{struct\ page} - address_{mem_map}) + start_PFN
\
=PFN
$$

(2)PFN 到 page 结构体:页框号加上对应 mem_section->section_mem_map

该宏首先使用 __pfn_section() 来获取到 PFN 所属的 mem_section,该函数定义于 /include/linux/mmzone.h 中,如下:

1
2
3
4
static inline struct mem_section *__pfn_to_section(unsigned long pfn)
{
return __nr_to_section(pfn_to_section_nr(pfn));
}

其中 pfn_to_section_nr() 定义如下,用以获取对应的 section 的索引:

1
2
3
4
static inline unsigned long pfn_to_section_nr(unsigned long pfn)
{
return pfn >> PFN_SECTION_SHIFT;
}

这里用到一个宏 PFN_SECTION_SHIFT,定义如下:

1
#define PFN_SECTION_SHIFT    (SECTION_SIZE_BITS - PAGE_SHIFT)

其中的 SECTION_SIZE_BIT 表示一个 section 的大小(恒定为2的幂次方)所占位数,而 PAGE_SHIFT 则为一个页的大小(通常为4096)所占位数,前者移位后者所得为_一个 section 中页的数量_

由页框号移位(本质为除法)单个 section 中页的数量便能得到其所属 section 标号

之后使用 __nr_to_section() 来获取对应的 mem_section 结构体的地址,最后使用 __section_mem_map_addr() 获取到 mem_section 结构体中的 section_mem_map 成员后再与页框号做 指针加法 便能获取到对应的 page 结构体数组,计算过程如下:
$$
PFN - section_mem_map = PFN - (address_{mem_map} - start_PFN)\
= (PFN - start_PFN )+ address_{mem_map}
\
=address_{struct\ page}
$$

*Sparse Memory virtual memmap

基于Sparse Memory 内存模型上引入了 vmemmap 的概念,是目前 Linux 最常用的内存模型之一

图依然是偷的,侵删

image.png

在开启了 vmemmap 之后,所有的 mem_section 中的 page 都抽象到一个虚拟数组 vmemmap 中,这样在进行struct page * 和 pfn 转换时,直接使用 vmemmap 数组即可

0x02.struct zone:区

在 Linux 下将一个节点内不同用途的内存区域划分为不同的 区(zone),对应结构体 struct zone,该结构体定义于 /include/linux/mmzone.h 中,如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
struct zone {
/* Read-mostly fields */

/* zone 的“水位线”, 使用宏 *_wmark_pages(zone) 进行访问 */
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;

unsigned long nr_reserved_highatomic;

/*
* 我们不知道我们将要分配的内存是否是可释放的 或/且 最终会被释放,
* 因此为了避免将整个的几个 GB 的 RAM浪费掉,
* 我们必须要保留一些 lower zone memory
* (否则我们将有在 lower zones 上耗尽所有内存(OOM)的风险,
* 尽管此时在 higher zones 仍有大量的 RAM).
* 若 sysctl_lowmem_reserve_ratio 系统控制项改变,
* 这个数组有可能在运行时被改变
*/
long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
/*
* the high and batch values are copied to individual pagesets for
* faster access
*/
int pageset_high;
int pageset_batch;

#ifndef CONFIG_SPARSEMEM
/*
* 单个 pageblock_nr_pages block 的标志位. 参见 pageblock-flags.h.
* 在 SPARSEMEM 中, 该 map 存放于 struct mem_section 中
*/
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;

/*
* spanned_pages 为该 zone 所包含的 pages 的范围, 包括空洞
* 计算方式如下:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages 为该 zone 中存在的物理页框数
* 计算方式如下:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* managed_pages 为现有的由 buddy system 管理的页面数量,
* 计算方式如下 (reserved_pages 包括由 bootmem allocator 分配的页面):
* managed_pages = present_pages - reserved_pages;
*
* present_pages 可能会被内存热拔插或内存电源管理逻辑
* 通过检查(present_pages - managed_pages)来算出未被管理的页面.
* managed_pages 应被页面分配器与 vm 扫描器用以计算所有的水位线与阈值
*
* 锁规则:
*
* zone_start_pfn 与 spanned_pages 由 span_seqlock 保护.
* 这是一个顺序锁(seqlock,译者补充:写优先锁)因为他得在 zone->lock 之外被读取,
* 在主分配器路径中完成.
* 但他确实不经常被写入。
*
* span_seq lock 随着 zone->lock 被定义,因为相较于 zone->lock,
* 他经常被读取. 让他们有个机会在同一条缓存线(cacheline)上一件好事
*
* 运行时 present_pages 应当由 mem_hotplug_begin/end() 进行保护.
* 任何无法忍受 present_pages 的应当使用 get_online_mems()来获得固定的值.
*/
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;

const char *name;

#ifdef CONFIG_MEMORY_ISOLATION
/*
* 独立的 pageblock 的数量. 用以解决由于对 pagelock
* 的 migratetype 的竞态检索导致的对 freepage 的错误计数.
* 由 zone->lock 保护
*/
unsigned long nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
/* 参见 spanned/present_pages 以获得更多描述 */
seqlock_t span_seqlock;
#endif

int initialized;

/* 供页分配器使用的写敏感字段 */
ZONE_PADDING(_pad1_)

/* 不同 sizes 的闲置区域 */
struct free_area free_area[MAX_ORDER];

/* zone 标志位 */
unsigned long flags;

/* 主要保护 free_area */
spinlock_t lock;

/* 供 compaction and vmstats 使用的写敏感字段. */
ZONE_PADDING(_pad2_)

/*
* 当闲置页在这一点下时, 在读取闲置页数量时会采取额外的步骤
* 以避免 per-cpu 计数器
* 漂移导致水位线被突破
*/
unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* pfn where compaction free scanner should start */
unsigned long compact_cached_free_pfn;
/* pfn where compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
#endif

#ifdef CONFIG_COMPACTION
/*
* On compaction failure, 1<<compact_defer_shift compactions
* are skipped before trying again. The number attempted since
* last failure is tracked with compact_considered.
* compact_order_failed is the minimum compaction failed order.
*/
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* Set to true when the PG_migrate_skip bits should be cleared */
bool compact_blockskip_flush;
#endif

bool contiguous;

ZONE_PADDING(_pad3_)
/* Zone 的统计数据 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

I.页面迁移机制

页面迁移主要用以解决内核空间中的碎片问题,在长期的运行之后内存当中空闲页面的分布可能是零散的,这便导致了内核有可能无法映射到足够大的连续内存,因此需要进行_页面迁移_——将旧的页面迁移到新的位置

从知乎偷的图.png

并非所有的页面都是能够随意迁移的,因此我们在 buddy system 当中还需要将页面按照迁移类型进行分类

迁移类型

迁移类型由一个枚举类型定义,定义于 /include/linux/mmzone.h 中,如下:

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
enum migratetype {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
/*
* MIGRATE_CMA migration type is designed to mimic the way
* ZONE_MOVABLE works. Only movable pages can be allocated
* from MIGRATE_CMA pageblocks and page allocator never
* implicitly change migration type of MIGRATE_CMA pageblock.
*
* The way to use it is to change migratetype of a range of
* pageblocks to MIGRATE_CMA which can be done by
* __free_pageblock_cma() function. What is important though
* is that a range of pageblocks must be aligned to
* MAX_ORDER_NR_PAGES should biggest page be bigger then
* a single pageblock.
*/
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* can't allocate from here */
#endif
MIGRATE_TYPES
};
  • MIGRATE_UNMOVABLE:这类型页面在内存当中有着固定的位置,不能移动
  • MIGRATE_MOVABLE:这类页面可以随意移动,例如用户空间的页面,我们只需要复制数据后改变页表映射即可
  • MIGRATE_RECLAIMABLE:这类页面不能直接移动,但是可以删除,例如映射自文件的页
  • MIGRATE_PCPTYPESper_cpu_pageset,即每 CPU 页帧缓存,其迁移仅限于同一节点内
  • MIGRATE_ISOLATE不能从该链表分配页面,该链表用于跨 NUMA 节点进行页面移动,将页面移动到使用该页面最为频繁的 CPU 所处节点
  • MIGRATE_TYPES_:表示迁移类型的数目,_并不存在这一链表

II.几个比较重要的字段

简单讲讲其中几个重要的成员

_watermark:“水位线”

每一个 zone 都有着其对应的三档“水位线”: WMARK_MINWMARK_LOWWMARK_HIGH,存放在 _watermark 数组中,在进行内存分配时,分配器(例如 buddy system)会根据当前 zone 中空余内存所处在的“水位线”来判断当前的内存状况,如下图所示:

图仍然是偷的,侵删

image.png

具体机制可以参见这里

lowmem_reserve:zone 自身的保留内存

在进行内存分配时,若当前的 zone 没有足够的内存了,则会向下一个 zone 索要内存,那么这就存在一个问题:来自 higher zones 的内存分配请求可能耗尽 lower zones 的内存,但这样分配的内存未必是可释放的(freeable),亦或者/且最终不一定会被释放,这有可能导致 lower zones 的内存提前耗尽,而 higher zones 却仍保留有大量的内存

为了避免这样的一种情况的发生,lowmem_reserve 字段用以声明为该 zone 保留的内存,这一块内存别的 zone 是不能动的

node:NUMA 中标识所属 node

这个字段只在 NUMA 系统中被启用,用以标识该 zone 所属的 node

可以参考下面的图

图还是偷的,侵删

image.png

image.png

zone_pgdat:zone 所属的 pglist_data 节点

该字段用以标识该 zone 所属的 pglist_data 节点

**pageset**:zone 为每个 CPU 划分一个独立的”页面仓库“

众所周知伴随着多 CPU 的引入,条件竞争就是一个不可忽视的问题,当多个 CPU 需要对一个 zone 进行操作时,频繁的加锁/解锁操作则毫无疑问会造成大量的开销,因此 zone 引入了 per_cpu_pageset 结构体成员,即为每一个 CPU 都准备一个单独的页面仓库,因此其实现方式是实现为一个 percpu 变量

在一开始时 buddy system 会将页面放置到各个 CPU 自己独立的页面仓库中,需要进行分配时 CPU 优先从自己的页面仓库中分配

该结构体定义于 /include/linux/mmzone.h 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct per_cpu_pages {
int count; /* 链表中页的数量 */
int high; /* 高水位线, 清空需要(笔者补:用以进行判断) */
int batch; /* chunk size for buddy add/remove */

/* 页面链表, 在 pcp-lists 上储存的独立的迁移类型 */
struct list_head lists[MIGRATE_PCPTYPES];
};

struct per_cpu_pageset {
struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
s8 expire;
u16 vm_numa_stat_diff[NR_VM_NUMA_STAT_ITEMS];
#endif
#ifdef CONFIG_SMP
s8 stat_threshold;
s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};

该结构体会被存放在每个 CPU 自己独立的 .data..percpu 段中,以 CPU0 为例,结构如下图所示

自己画的图.png

zone_start_pfn:zone 的起始物理PFN

该字段用以标识该 zone 的起始物理页帧编号(page frame number)

spanned_pages: zone 对应的内存区域中的 pages 总数(包括空洞)

该字段用以标识该 zone 对应的内存区域中的 pages 总数, 包括空洞

present_pages: zone 中存在的物理页框数

该字段用以标识 zone 中实际存在的物理页框数

managed_pages:zone 中 buddy system 管理的页面数量

该字段用以标识 zone 中 buddy system 管理的页面数量

**free_area**:buddy system 按照 order 管理的页面

该字段用以存储 buddy system 按照 order 管理的页面,为一个 free_area 结构体数组,该结构体定义如下:

1
2
3
4
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};

在 free_area 中存放的页面通过自身的相应字段连接成双向链表结构,这里放一张 overview

自己画的图.png

free_area 中并非只有一个双向链表,而是按照不同的“迁移类型”(migrate type)进行分开存放,这是由于_页面迁移_机制的存在

free_list[0] 作为例子,我们可以得到如下 overview:

自己画的图.png

vm_stat:统计数据

该数组用来进行数据统计,按照枚举类型 zone_stat_item 分为多个数组,以统计不同类型的数据(比如说 NR_FREE_PAGES 表示 zone 中的空闲页面1数量):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum zone_stat_item {
/* First 128 byte cacheline (assuming 64 bit words) */
NR_FREE_PAGES,
NR_ZONE_LRU_BASE, /* Used only for compaction and reclaim retry */
NR_ZONE_INACTIVE_ANON = NR_ZONE_LRU_BASE,
NR_ZONE_ACTIVE_ANON,
NR_ZONE_INACTIVE_FILE,
NR_ZONE_ACTIVE_FILE,
NR_ZONE_UNEVICTABLE,
NR_ZONE_WRITE_PENDING, /* Count of dirty, writeback and unstable pages */
NR_MLOCK, /* mlock()ed pages found and moved off LRU */
/* Second 128 byte cacheline */
NR_BOUNCE,
#if IS_ENABLED(CONFIG_ZSMALLOC)
NR_ZSPAGES, /* allocated in zsmalloc */
#endif
NR_FREE_CMA_PAGES,
NR_VM_ZONE_STAT_ITEMS };

flags:标志位

该 zone 的标志位,用以标识其所处的状态

II.zone 的分类

在 Linux kernel 当中,我们根据内存区段的不同用途,将其划分为不同的 zone,在 /include/linux/mmzone.h 中有着相应的枚举定义,如下:

摆烂了,可能某天想起来会补充翻译

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
enum zone_type {
/*
* ZONE_DMA and ZONE_DMA32 are used when there are peripherals not able
* to DMA to all of the addressable memory (ZONE_NORMAL).
* On architectures where this area covers the whole 32 bit address
* space ZONE_DMA32 is used. ZONE_DMA is left for the ones with smaller
* DMA addressing constraints. This distinction is important as a 32bit
* DMA mask is assumed when ZONE_DMA32 is defined. Some 64-bit
* platforms may need both zones as they support peripherals with
* different DMA addressing limitations.
*/
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
/*
* Normal addressable memory is in ZONE_NORMAL. DMA operations can be
* performed on pages in ZONE_NORMAL if the DMA devices support
* transfers to all addressable memory.
*/
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
/*
* A memory area that is only addressable by the kernel through
* mapping portions into its own address space. This is for example
* used by i386 to allow the kernel to address the memory beyond
* 900MB. The kernel will set up special mappings (page
* table entries on i386) for each page that the kernel needs to
* access.
*/
ZONE_HIGHMEM,
#endif
/*
* ZONE_MOVABLE is similar to ZONE_NORMAL, except that it contains
* movable pages with few exceptional cases described below. Main use
* cases for ZONE_MOVABLE are to make memory offlining/unplug more
* likely to succeed, and to locally limit unmovable allocations - e.g.,
* to increase the number of THP/huge pages. Notable special cases are:
*
* 1. Pinned pages: (long-term) pinning of movable pages might
* essentially turn such pages unmovable. Memory offlining might
* retry a long time.
* 2. memblock allocations: kernelcore/movablecore setups might create
* situations where ZONE_MOVABLE contains unmovable allocations
* after boot. Memory offlining and allocations fail early.
* 3. Memory holes: kernelcore/movablecore setups might create very rare
* situations where ZONE_MOVABLE contains memory holes after boot,
* for example, if we have sections that are only partially
* populated. Memory offlining and allocations fail early.
* 4. PG_hwpoison pages: while poisoned pages can be skipped during
* memory offlining, such pages cannot be allocated.
* 5. Unmovable PG_offline pages: in paravirtualized environments,
* hotplugged memory blocks might only partially be managed by the
* buddy (e.g., via XEN-balloon, Hyper-V balloon, virtio-mem). The
* parts not manged by the buddy are unmovable PG_offline pages. In
* some cases (virtio-mem), such pages can be skipped during
* memory offlining, however, cannot be moved/allocated. These
* techniques might use alloc_contig_range() to hide previously
* exposed pages from the buddy again (e.g., to implement some sort
* of memory unplug in virtio-mem).
*
* In general, no unmovable allocations that degrade memory offlining
* should end up in ZONE_MOVABLE. Allocators (like alloc_contig_range())
* have to expect that migrating pages in ZONE_MOVABLE can fail (even
* if has_unmovable_pages() states that there are no unmovable pages,
* there can be false negatives).
*/
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES

};

X86-32

如下表格所示(懒得找图了)

Type Start address End address
ZONE_DMA 0MB 16MB
ZONE_NORMAL 16MB 896MB
ZONE_HIGHMEM 896MB

通常我们简单地划分为两部分:

  • 线性映射区(前 896MB):这一块内存直接映射到物理内存地址 0 起始往后的总计 896MB,为线性映射
  • 高端内存(从 896MB 开始往后):这一块内存的映射是不连续的

X86-64

如下表格所示(懒得找图了)

Type Start address End address
ZONE_DMA 0MB 16MB
ZONE_DMA32 16MB 4GB
ZONE_NORMAL 4GB

在 64 位的 Linux kernel 中没有了“高端内存”这一概念

0x03.struct pglist_data:节点

zone 再向上一层便是节点——Linux 将_内存控制器(memory controller)_作为节点划分的依据,对于 UMA 架构而言只有一个节点,而对于 NUMA 架构而言通常有多个节点,对于同一个内存控制器下的 CPU 而言其对应的节点称之为_本地内存_,不同处理器之间通过总线进行进一步的连接。如下图所示,一个MC对应一个节点:

image.png

一个节点使用 pglist_data 结构进行描述,该结构定义于 /include/linux/mmzone.h 中,如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/*
* 在 NUMA 机器上, 每个 NUMA 节点都有一个 pg_data_t 用以描述其内存布局。
* 在 UMA 机器上则只有一个单独的 pglist_data 描述整个内存。
*
* 内存统计数据与页置换数据结构由一个 per-zone basis维持
*/
typedef struct pglist_data {
/*
* node_zones 字段包含该节点所拥有的 zones。 并非所有的 zone 都已被填充,但这是一个满的列表。
* 它被该节点的 node_zonelists 以及其他节点的 node_zonelists 所引用.
*
*/
struct zone node_zones[MAX_NR_ZONES];

/*
* node_zonelists 包含有对所有节点中所有区的引用。
* 通常第一个区将会作为该节点的 node_zones 的引用.
*/
struct zonelist node_zonelists[MAX_ZONELISTS];

int nr_zones; /* 该节点中被填充的 zone 的数量 */
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* 即 SPARSEMEM */
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
/*
* 若你期望 node_start_pfn, node_present_pages,
* node_spanned_pages 或 nr_zones 保持不变,
* 必须在任何时刻持有(这个锁)。
* 同时在 deferred page 初始化期间对 pgdat->first_deferred_pfn 进行同步。
*
* (内核)提供了 pgdat_resize_lock() 与 pgdat_resize_unlock()
* 以在没有对 CONFIG_MEMORY_HOTPLUG 或 CONFIG_DEFERRED_STRUCT_PAGE_INIT
* 进行检查的情况下操纵 node_size_lock
*
* 基于 zone->lock 与 zone->span_seqlock
*/
spinlock_t node_size_lock;
#endif
unsigned long node_start_pfn;
unsigned long node_present_pages; /* 所有物理页的数量 */
unsigned long node_spanned_pages; /* 所有物理页的大小,包括空洞 */

int node_id;
wait_queue_head_t kswapd_wait;
wait_queue_head_t pfmemalloc_wait;
struct task_struct *kswapd; /* 由 mem_hotplug_begin/end() 保护 */

int kswapd_order;
enum zone_type kswapd_highest_zoneidx;

int kswapd_failures; /* 进行了 'reclaimed == 0' 判断的次数 */

#ifdef CONFIG_COMPACTION
int kcompactd_max_order;
enum zone_type kcompactd_highest_zoneidx;
wait_queue_head_t kcompactd_wait;
struct task_struct *kcompactd;
#endif
/*
* 这是每个 node 保留的对用户空间分配不可用的页面
*/
unsigned long totalreserve_pages;

#ifdef CONFIG_NUMA
/*
* 若存在更多的未映射页面,则节点回收将会变得活跃
*/
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
#endif /* CONFIG_NUMA */

/* 页回收使用的写敏感字段 */
ZONE_PADDING(_pad1_)

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
/*
* 若在大机器上的内存初始化被推迟了,那么这是
* 第一个需要被初始化的 PFN
*/
unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
struct deferred_split deferred_split_queue;
#endif

/* 页回收扫描器通常访问的字段 */

/*
* NOTE: 若开启了 MEMCG 则其将不会被使用
*
* 使用 mem_cgroup_lruvec() 以查询 lruvecs.
*/
struct lruvec __lruvec;

unsigned long flags;

ZONE_PADDING(_pad2_)

/* Per-node vmstats */
struct per_cpu_nodestat __percpu *per_cpu_nodestats;
atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;

几个比较重要的字段

简单讲讲其中几个重要的成员

**node_zones**:node 的 zone 列表

节点中最重要的字段 node_zones 作为一个 zone 结构体数组 记录了本节点上所有的 zone,其中有效的 zone 的个数由节点结构体的 nr_zones 字段指出

node_zonelists:内存分配时备用 zone 的搜索顺序

该字段用以确定内存分配时对备用的 zone 的搜索顺序,在本节点常规内存分配失败时会沿着这个数组进行搜索,其中包含的 zone 可以是非本节点的 zone

这是一个其为一个 zonelist 类型的结构体数组,该结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 单次分配请求在一个 zonelist 上操作. 一个 zonelist 便是一组 zone 的列表,
* 其中第一个 zone 为分配的“目标”,而其他的 zone 为后备的zone,优先级降低。
*
* 为了提高 zonelist 的读取速度, 在 zonerefs 中包含正在被读取的 entry 的 zone index。
* 用来访问所给的 zoneref 结构体信息的帮助函数有:
*
* zonelist_zone() - 返回一个 struct zone 的指针作为 _zonerefs 中的一个 entry
* zonelist_zone_idx() - 返回作为 entry 的 zone 的 index
* zonelist_node_idx() - 返回作为 entry 的 node 的 index
*/
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};

可以看到的是其为一个 zoneref 类型的结构体数组,该结构体定义如下,包含了一个 zone 的指针以及一个 index:

1
2
3
4
5
6
7
8
/*
* 该结构包含了 zonelist 中一个 zone 的信息。
* 其被储存在这里以预防对大结构体的解引用与对表的查询。
*/
struct zoneref {
struct zone *zone; /* 指向实际上的 zone 的指针 */
int zone_idx; /* zone_idx(zoneref->zone) */
};

nr_zones:node 中 zone 的数量

该字段存储了该节点中所有可用的 zone 的数量

node_start_pfn:node 的起始页框标号

该字段记录了该节点上的物理内存起始页框标号

node_present_pages:node 中物理页的总数量

该字段记录了节点中可用的物理页的总数量

unsigned long node_spanned_pages: node 中物理页的总大小

该字段记录了节点上包括空洞在内的页帧为单位的该节点内存的总长度

node_id:node 的标号

该字段记录了该节点在系统中的标号,从 0 开始

node 存储方式:全局数组 node_data[]

/arch/x86/mm/numa.c 中定义了一个 pglist_data 数组,如下:

1
2
struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
EXPORT_SYMBOL(node_data);

该数组中保存了系统中的所有的节点

由此,我们最终得到这样一张架构图:

还是偷的图,侵删

image.png

我们可以使用 numactl 工具来查看系统中的节点信息,如下:

1
2
3
4
5
6
7
8
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3
node 0 size: 11942 MB
node 0 free: 4464 MB
node distances:
node 0
0: 10

笔者的机器比较弱,只有一个节点

node 状态:全局数组 node_states[]

/mm/page_alloc.c 中定义了一个全局数组 node_states 用以标识对应标号的节点的状态,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* Array of node states.
*/
nodemask_t node_states[NR_NODE_STATES] __read_mostly = {
[N_POSSIBLE] = NODE_MASK_ALL,
[N_ONLINE] = { { [0] = 1UL } },
#ifndef CONFIG_NUMA
[N_NORMAL_MEMORY] = { { [0] = 1UL } },
#ifdef CONFIG_HIGHMEM
[N_HIGH_MEMORY] = { { [0] = 1UL } },
#endif
[N_MEMORY] = { { [0] = 1UL } },
[N_CPU] = { { [0] = 1UL } },
#endif /* NUMA */
};
EXPORT_SYMBOL(node_states);

在这里的 nodemask_t 类型为一个位图类型,定义于 /include/linux/nodemask.h 中,如下:

1
typedef struct { DECLARE_BITMAP(bits, MAX_NUMNODES); } nodemask_t;

这个状态由一个枚举类型 node_states 定义,该枚举类型定义于 /include/linux/nodemask.h 中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 位掩码将为所有节点保存
*/
enum node_states {
N_POSSIBLE, /* 节点在某个时刻是联机的 */
N_ONLINE, /* 节点是联机的 */
N_NORMAL_MEMORY, /* 节点有着普通的内存 */
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY, /* 节点有着普通或高端内存 */
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
N_MEMORY, /* 节点有着内存(普通,高端,可移动) */
N_CPU, /* 节点有着一个或多个 cpu */
N_GENERIC_INITIATOR, /* 节点有一个或多个 Generic Initiators */
NR_NODE_STATES
};

【OS.0x02】Linux 内核内存管理I - 页、区、节点
https://arttnba3.github.io/2021/11/28/OS-0X02-LINUX-KERNEL-MEMORY-5.11-PART-I/
作者
arttnba3
发布于
2021年11月28日
许可协议