内存管理
内存管理的实现涵盖了许多领域:
内存中的物理内存页的管理;
分配大块内存的伙伴系统;
分配较小块内存的
slab、slub和slob分配器;分配非连续内存块的
vmalloc机制;进程的地址空间。
实际上内核会区分3 种配置选项:FLATMEM、DISCONTIGMEM和SPARSEMEM。SPARSEMEM和DISCONTIGMEM实际上作用相同,但从开发者的角度看来,对应代码的质量有所不同。SPARSEMEM被认为更多是试验性的,不那么稳定,但有一些性能优化。我们认为DISCONTIGMEM相关代码更稳定一些,但不具备内存热插拔之类的新特性。
🍃 3.2.1 概述
首先,内存划分为结点。每个结点关联到系统中的一个处理器,在内核中表示为pg_data_t的实例。各个结点又划分为内存域,是内存的进一步细分。例如,对可用于(ISA设备的)DMA操作的内存区是有限制的。只有前16 MiB适用,还有一个高端内存区域无法直接映射。在二者之间是通用的“普通”内存区。因此一个结点最多由3个内存域组成。内核引入了下列常量来区分它们。
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
/* file: linux/mmzone.h */
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. Therefore, we do not allow
* pinning long-term pages in ZONE_MOVABLE. When pages are pinned and
* faulted, they come from the right zone right away. However, it is
* still possible that address space already has pages in
* ZONE_MOVABLE at the time when pages are pinned (i.e. user has
* touches that memory before pinning). In such case we migrate them
* to a different zone. When migration fails - pinning fails.
* 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).
* 6. ZERO_PAGE(0), kernelcore/movablecore setups might create
* situations where ZERO_PAGE(0) which is allocated differently
* on different platforms may end up in a movable zone. ZERO_PAGE(0)
* cannot be migrated.
* 7. Memory-hotplug: when using memmap_on_memory and onlining the
* memory to the MOVABLE zone, the vmemmap pages are also placed in
* such zone. Such pages cannot be really moved around as they are
* self-stored in the range, but they are treated as movable when
* the range they describe is about to be offlined.
*
* 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
};
ZONE_DMA标记适合DMA的内存域。该区域的长度依赖于处理器类型。在IA-32计算机上,一般的限制是16 MiB,这是由古老的ISA设备强加的边界。但更现代的计算机也可能受这一限制的影响。ZONE_DMA32标记了使用32位地址字可寻址、适合DMA的内存域。显然,只有在64位系统上,两种DMA内存域才有差别。在32位计算机上,本内存域是空的,即长度为0 MiB。在Alpha和AMD64系统上,该内存域的长度可能从0到4 GiB。ZONE_NORMAL标记了可直接映射到内核段的普通内存域。这是在所有体系结构上保证都会存在的唯一内存域,但无法保证该地址范围对应了实际的物理内存。例如,如果AMD64系统有2 GiB内存,那么所有内存都属于ZONE_DMA32范围,而ZONE_NORMAL则为空。ZONE_HIGHMEM标记了超出内核段的物理内存。
根据编译时的配置,可能无需考虑某些内存域。例如在64位系统中,并不需要高端内存域。如果支持了只能访问
4 GiB以下内存的32位外设,才需要DMA32内存域。
🍃 3.2.2 数据结构
我已经解释了用于内存管理的各种数据结构之间的关系,现在我们分别讲解各个数据结构。
- 结点管理
pg_data_t是用于表示结点的基本元素,定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
typedef structure pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
struct page *node_mem_map;
unsigned long node_start_pfn;
unsigned long node_present_pages; /* 物理内存页的总数 */
unsigned long node_spanned_pages; /* 物理内存页的总长度,包含洞在内 */
int node_id;
wait_queue_head_t kswapd_wait;
struct task_struct *kswapd;
} pg_data_t;
node_zones是一个数组,包含了结点中各内存域的数据结构。node_zonelists指定了备用结点及其内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存。结点中不同内存域的数目保存在
nr_zones。node_mem_map是指向page实例数组的指针,用于描述结点的所有物理内存页。它包含了结点中所有内存域的页。node_start_pfn是该NUMA结点第一个页帧的逻辑编号。系统中所有结点的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点内唯一)。node_start_pfn在UMA系统中总是0,因为其中只有一个结点,因此其第一个页帧编号总是0。node_present_pages指定了结点中页帧的数目,而node_spanned_pages则给出了该结点以页帧为单位计算的长度。二者的值不一定相同,因为结点中可能有一些空洞,并不对应真正的页帧。node_id是全局结点ID。系统中的NUMA结点都从0开始编号。kswapd_wait是交换守护进程(swap daemon)的等待队列,在将页帧换出结点时会用到(第18章会详细讨论该过程) 。kswapd指向负责该结点的交换守护进程的task_struct。
结点状态管理
如果系统中结点多于一个,内核会维护一个位图,用以提供各个结点的状态信息。状态是用位掩码指定的,可使用下列值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* include/linux/nodemask.h */
/*
* Bitmasks that are kept for all the nodes.
*/
enum node_states {
N_POSSIBLE, /* The node could become online at some point */
N_ONLINE, /* The node is online */
N_NORMAL_MEMORY, /* The node has regular memory */
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY, /* The node has regular or high memory */
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
N_MEMORY, /* The node has memory(regular, high, movable) */
N_CPU, /* The node has one or more cpus */
N_GENERIC_INITIATOR, /* The node has one or more Generic Initiators */
NR_NODE_STATES
};
状态N_POSSIBLE、N_ONLINE和N_CPU用于CPU和内存的热插拔。对内存管理有必要的标志是N_HIGH_MEMORY和N_NORMAL_MEMORY。如果结点有普通或高端内存则使用N_HIGH_MEMORY,仅当结点没有高端内存才设置N_NORMAL_MEMORY。
- 内存域
内核使用zone结构来描述内存域。其定义如下:
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
struct zone {
/* Read-mostly fields */
/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long _watermark[NR_WMARK];
/*通常由页分配器访问的字段 */
/*unsigned long pages_min, pages_low, pages_high;*/
/*
* We don't know if the memory that we're going to allocate will be
* freeable or/and it will be released eventually, so to avoid totally
* wasting several GB of ram we must reserve some of the lower zone
* memory (otherwise we risk to run OOM on the lower zones despite
* there being tons of freeable ram on the higher zones). This array is
* recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
* changes.
*/
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
/*
* 不同长度的空闲区域
*/
spinlock_t lock;
struct free_area free_area[NR_PAGE_ORDER];
ZONE_PADDING(_pad1_)
/* 通常由页面收回扫描程序访问的字段 */
spinlock_t lru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_scan_active;
unsigned long nr_scan_inactive;
unsigned long pages_scanned; /* 上一次回收以来扫描过的页 */
unsigned long flags; /* 内存域标志,见下文 */
/* Write-intensive fields used by compaction and vmstats. */
CACHELINE_PADDING(_pad2_);
/* 内存域统计量 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
/* 很少使用或大多数情况下只读的字段 */
wait_queue_head_t * wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
/* 支持不连续内存模型的字段。 */
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long unsigned long spanned_pages; /* 总长度,包含空洞 */
present_pages; /* 内存数量(除去空洞) */
char *name;
} ____cacheline_maxaligned_in_smp;
补充1:
从
Linux内核v4.13开始,zone->pages_min等字段被整合进了一个新的字段:
1 unsigned long watermark[NR_WMARK];
NR_WMARK是一个枚举值,定义为3(在include/linux/mmzone.h中):
1 2 3 4 5 6 enum zone_watermarks { WMARK_MIN, WMARK_LOW, WMARK_HIGH, NR_WMARK };
所以
zone->watermark[WMARK_MIN]就相当于原来的zone->pages_min
zone->watermark[WMARK_LOW]替代了pages_low
zone->watermark[WMARK_HIGH]替代了pages_high
补充2:
zone->free_area[order]
这是一个数组,大小是
MAX_ORDER(通常是 11),表示从$2^0$到$2^{10}$的页块。每一个
zone->free_area[order]里都存储了该order下的空闲页块。每个
free_list[migrate_type]是一个链表,链表里的元素是struct page对象。
该结构各个成员的语义是什么呢?由于内存管理是内核中一个复杂而牵涉颇广的部分,因此在这里将该结构所有成员的确切语义都讲解清楚是不太可能的,本章和后续章节相当一部分都会专注于讲述相关的数据结构和机制。此处只能对即将讨论的问题给予概述,读者姑且浅尝辄止。尽管如此,仍然会出现大量的向前引用。
pages_min、pages_high、pages_low是页换出时使用的“水印”。如果内存不足,内核可以将页写到硬盘。这3个成员会影响交换守护进程的行为。- 如果空闲页多于
pages_high,则内存域的状态是理想的。 如果空闲页的数目低于
pages_low,则内核开始将页换出到硬盘。- 如果空闲页的数目低于
pages_min,那么页回收工作的压力就比较大,因为内存域中急需空闲页。
- 如果空闲页多于
zone_start_pfn是内存域第一个页帧的索引。
假设有这几个zone:
ZONE_DMAZONE_NORMALZONE_HIGHMEM
每个zone的lowmem_reserve[]值可能是:
| zone name | lowmem_reserve[DMA] | lowmem_reserve[NORMAL] | lowmem_reserve[HIGHMEM] |
|---|---|---|---|
DMA | 0 | - | - |
NORMAL | 128 | 0 | - |
HIGHMEM | 256 | 128 | 0 |
这意味着:
- 从
HIGHMEM分配内存时,必须保证:HIGHMEM至少剩 256 页(为NORMAL留)NORMAL至少剩 128 页(为DMA留)
否则,就不能继续从HIGHMEM分配。
🧠 为什么需要它?
Linux的分配器(__alloc_pages())是从高zone向低zone回退的。
但我们必须保留低地址物理内存用于一些“只能用低地址内存”的场景,如:
DMA设备(只能访问0~16MB)- 页表、内核栈(可能不支持
highmem)atomic分配
所以要强制“高zone不准把自己用光”,以保证低zone有足够资源。
⚙️ 那它是如何参与分配的?
watermark 计算公式(简化版):
1
2
3
4
5
c
复制编辑
watermark = pages_min + lowmem_reserve[zone_idx];
这个 watermark 用于判断:
1
2
3
4
5
c
复制编辑
free_pages(zone) >= watermark ?
如果不满足,就不能从这个 zone 分配。
📌 实际代码路径:
你可以看到这些逻辑出现在:
mm/page_alloc.c中的zone_watermark_ok()、zone_watermark_fast()等函数;分配函数
__alloc_pages_nodemask()中调用它来判断能否从某个 zone 分配。- 。
🔄 如何动态调整?
这个数组会根据 /proc/sys/vm/lowmem_reserve_ratio 来实时更新。
例如:
1
2
3
4
5
6
bash
复制编辑
cat /proc/sys/vm/lowmem_reserve_ratio
# 输出类似于: 256 256 32
你可以手动调整:
1
2
3
4
5
bash
复制编辑
echo "128 128 32" > /proc/sys/vm/lowmem_reserve_ratio
也有可能这些标志均未设置。这是内存域的正常状态。ZONE_ALL_UNRECLAIMABLE状态出现
在内核试图重用该内存域的一些页时(页面回收,参见第18章),但因为所有的页都被钉住
而无法回收。例如,用户空间应用程序可以使用mlock系统调用通知内核页不能从物理内存
移出,比如换出到磁盘上。这样的页称之为钉住的。如果一个内存域中的所有页都被钉住,
那么该内存域是无法回收的,即设置该标志。为不浪费时间,交换守护进程在寻找可供回
收的页时,只会简要地扫描一下此类内存域。①
在SMP系统上,多个CPU可能试图并发地回收一个内存域。ZONE_RECLAIM_LOCKED标志可防
止这种情况:如果一个CPU在回收某个内存域,则设置该标志。这防止了其他CPU的尝试。
ZONE_OOM_LOCKED专用于某种不走运的情况:如果进程消耗了大量的内存,致使必要的操
作都无法完成,那么内核会试图杀死消耗内存最多的进程,以获得更多的空闲页。该标志
可以防止多个CPU同时进行这种操作。
内核提供了3个辅助函数用于测试和设置内存域的标志
- 内存域水印的计算
1. setup_per_zone_pages_min() 的原始作用
该函数用于初始化每个内存区域(struct zone)的 min、low、high 水位值(watermarks),这些水位用于触发内存回收(如 kswapd)和分配决策。
min:最低空闲页面阈值,低于此值会触发直接回收(direct reclaim)。low:目标空闲页面阈值,kswapd异步回收内存直到达到此值。high:上限阈值,kswapd停止回收。
2. 替代方案(新内核中的实现)
在较新内核(如 v5.x+),水位计算被分散到以下流程中:
(1) init_per_zone_wmark_min()(部分替代)
作用:初始化
zone->_watermark[WMARK_MIN],基于全局变量min_free_kbytes(通过/proc/sys/vm/min_free_kbytes配置)。调用路径:
c
1 2 3 4
start_kernel() → mm_init() → init_per_zone_wmark_min() // 计算 MIN 水位 → setup_per_zone_lowmem_reserve() // 设置 lowmem 保留
(2) calculate_watermark()(动态调整水位)
- 作用:动态计算水位值(如
WMARK_LOW、WMARK_HIGH),基于当前内存压力和min_free_kbytes。 - 调用场景:
- 内存热插拔(
hotplug)时。 - 通过
/proc/sys/vm/min_free_kbytes修改配置时。
- 内存热插拔(
(3) zone_watermark_ok() / zone_watermark_fast()
- 作用:检查当前 Zone 的水位是否满足分配请求,替代了旧版中硬编码的水位逻辑。
3. 关键代码示例(新内核)
水位初始化(mm/page_alloc.c)
c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void __init init_per_zone_wmark_min(void) {
unsigned long lowmem_kbytes;
int new_min_free_kbytes;
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
if (new_min_free_kbytes > 65536)
new_min_free_kbytes = 65536;
if (new_min_free_kbytes < 128)
new_min_free_kbytes = 128;
min_free_kbytes = new_min_free_kbytes;
setup_per_zone_wmarks(); // 计算各 Zone 的水位
}
水位动态调整(mm/vmscan.c)
c
1
2
3
4
void update_wmark_min(void) {
calculate_watermark();
refresh_zone_stat_thresholds();
}
4. 为什么被替代?
- 模块化设计:水位计算拆分为更小的函数,便于维护和动态调整。
- 动态响应:支持运行时根据内存压力调整水位(如通过
sysctl)。 - 代码清晰性:旧版
setup_per_zone_pages_min()耦合了多个逻辑,新版分离了初始化和动态更新。
5. 检查当前水位(调试方法)
查看
/proc/zoneinfo:bash
1
cat /proc/zoneinfo | grep -A 10 "Node 0" | grep -E "min|low|high"
内核参数调整:
bash
1
sysctl vm.min_free_kbytes=65536 # 修改后触发水位重计算
总结
- 替代函数:
init_per_zone_wmark_min()+calculate_watermark()+setup_per_zone_wmarks()。 - 核心变化:从静态初始化变为动态计算,更灵活适应内存压力。
- 关联机制:水位值与
kswapd、direct reclaim、min_free_kbytes紧密相关。
这个结构体 struct per_cpu_pages 是 Linux 内核中用于管理 每 CPU 页帧缓存(Per-CPU Page Frame Cache, PCP) 的关键数据结构,主要用于优化内存分配性能,减少对全局锁的竞争。以下是各字段的详细解释:
1. 核心字段解析
(1) 锁与保护机制
spinlock_t lock保护lists和count等字段的自旋锁,确保多核并发访问时的安全性。
(2) 页面计数与水位控制
int count当前 CPU 的 PCP 列表中总页面数(所有迁移类型的页面总和)。int high高水位线,当count超过此值时,需将多余页面返还给伙伴系统(Buddy System)。int high_min/int high_max高水位的最小/最大值,动态调整high的边界(避免频繁回收或内存浪费)。int batch从伙伴系统批量添加或移除页面时的块大小(减少频繁操作的开销)。
(3) 分配与释放策略
u8 flags标志位,用于控制 PCP 的特殊行为(如预填充策略)。u8 alloc_factor分配时的动态缩放因子,用于调整batch的大小,适应不同负载。short free_count连续释放页面的计数,用于优化批量释放操作(避免频繁锁竞争)。
(4) 页面链表
struct list_head lists[NR_PCP_LISTS]按迁移类型(MIGRATE_TYPES)分类的页面链表。例如:MIGRATE_UNMOVABLE(不可移动页面,如内核栈)MIGRATE_MOVABLE(可移动页面,如用户态内存)MIGRATE_RECLAIMABLE(可回收页面,如文件缓存)
(5) NUMA 相关字段
u8 expire仅在CONFIG_NUMA启用时有效,表示远程节点页面缓存的过期时间,超时后需回收。
- 页帧
页帧代表系统内存的最小单位,对内存中的每个页都会创建struct page的一个实例。内核程序
员需要注意保持该结构尽可能小,因为即使在中等程度的内存配置下,系统的内存同样会分解为大量
的页。例如,IA-32系统的标准页长度为4 KiB,在主内存大小为384 MiB时,大约共有100 000页。就
当今的标准而言,这个容量算不上很大,但页的数目已经非常可观。
这也是为什么内核尽力保持struct page尽可能小的原因。在典型系统中,由于页的数目巨大,
因此对page结构的小改动,也可能导致保存所有page实例所需的物理内存暴涨。
页的广泛使用,增加了保持结构长度的难度:内存管理的许多部分都使用页,用于各种不同的用
途。内核的一个部分可能完全依赖于struct page提供的特定信息,而该信息对内核的另一部分可能
完全无用,该部分依赖于struct page提供的其他信息,而这部分信息对内核的其他部分也可能是完
全无用的,等等。
C语言的联合很适合于该问题,尽管它未能增加struct page的清晰程度。考虑一个例子:一个
物理内存页能够通过多个地方的不同页表映射到虚拟地址空间,内核想要跟踪有多少地方映射了该
页。为此,struct page中有一个计数器用于计算映射的数目。如果一页用于slub分配器(将整页细
分为更小部分的一种方法,请参见3.6.1节),那么可以确保只有内核会使用该页,而不会有其他地方
使用,因此映射计数信息就是多余的。因此内核可以重新解释该字段,用来表示该页被细分为多少个
小的内存对象使用。在数据结构定义中,这种双重解释如下所示:
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
154
155
156
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
/*
* Five words (20/40 bytes) are available in this union.
* WARNING: bit 0 of the first word is used for PageTail(). That
* means the other users of this union MUST NOT use the bit to
* avoid collision and false-positive PageTail().
*/
union {
struct { /* Page cache and anonymous pages */
/**
* @lru: Pageout list, eg. active_list protected by
* lruvec->lru_lock. Sometimes used as a generic list
* by the page owner.
*/
union {
struct list_head lru;
/* Or, for the Unevictable "LRU list" slot */
struct {
/* Always even, to negate PageTail */
void *__filler;
/* Count page's or folio's mlocks */
unsigned int mlock_count;
};
/* Or, free page */
struct list_head buddy_list;
struct list_head pcp_list;
struct {
struct llist_node pcp_llist;
unsigned int order;
};
};
/* See page-flags.h for PAGE_MAPPING_FLAGS */
struct address_space *mapping;
union {
pgoff_t __folio_index; /* Our offset within mapping. */
unsigned long share; /* share count for fsdax */
};
/**
* @private: Mapping-private opaque data.
* Usually used for buffer_heads if PagePrivate.
* Used for swp_entry_t if swapcache flag set.
* Indicates order in the buddy system if PageBuddy.
*/
unsigned long private;
};
struct { /* page_pool used by netstack */
/**
* @pp_magic: magic value to avoid recycling non
* page_pool allocated pages.
*/
unsigned long pp_magic;
struct page_pool *pp;
unsigned long _pp_mapping_pad;
unsigned long dma_addr;
atomic_long_t pp_ref_count;
};
struct { /* Tail pages of compound page */
unsigned long compound_head; /* Bit zero is set */
};
struct { /* ZONE_DEVICE pages */
/*
* The first word is used for compound_head or folio
* pgmap
*/
void *_unused_pgmap_compound_head;
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: You can use this to free a page by RCU. */
struct rcu_head rcu_head;
};
union { /* This union is 4 bytes in size. */
/*
* For head pages of typed folios, the value stored here
* allows for determining what this page is used for. The
* tail pages of typed folios will not store a type
* (page_type == _mapcount == -1).
*
* See page-flags.h for a list of page types which are currently
* stored here.
*
* Owners of typed folios may reuse the lower 16 bit of the
* head page page_type field after setting the page type,
* but must reset these 16 bit to -1 before clearing the
* page type.
*/
unsigned int page_type;
/*
* For pages that are part of non-typed folios for which mappings
* are tracked via the RMAP, encodes the number of times this page
* is directly referenced by a page table.
*
* Note that the mapcount is always initialized to -1, so that
* transitions both from it and to it can be tracked, using
* atomic_inc_and_test() and atomic_add_negative(-1).
*/
atomic_t _mapcount;
};
/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
atomic_t _refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#elif defined(CONFIG_SLAB_OBJ_EXT)
unsigned long _unused_slab_obj_exts;
#endif
/*
* On machines where all RAM is mapped into kernel address space,
* we can simply calculate the virtual address. On machines with
* highmem some memory is mapped into kernel virtual memory
* dynamically, so we need a place to store that address.
* Note that this field could be 16 bits on x86 ... ;)
*
* Architectures with slow multiplication can define
* WANT_PAGE_VIRTUAL in asm/page.h
*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
#ifdef CONFIG_KMSAN
/*
* KMSAN metadata for this page:
* - shadow page: every bit indicates whether the corresponding
* bit of the original page is initialized (0) or not (1);
* - origin page: every 4 bytes contain an id of the stack trace
* where the uninitialized value was created.
*/
struct page *kmsan_shadow;
struct page *kmsan_origin;
#endif
}
_struct_page_alignment;
该结构的格式是体系结构无关的,不依赖于使用的CPU类型,每个页帧都由该结构描述。除了slub
相关成员之外,page结构也包含了若干其他成员,只能在讨论相关内核子系统时准确地解释。
flags存储了体系结构无关的标志,用于描述页的属性。_mapcount表示在页表中有多少项指向该页。内核可以将多个毗连的页合并为较大的复合页(
compound page)。分组中的第一个页称作首页(head page) ,而所有其余各页叫做尾页(tail page) 。所有尾页对应的page实例中,都将first_page设置为指向首页。mapping指定了页帧所在的地址空间。index是页帧在映射内部的偏移量。地址空间是一个非常一般的概念,例如,可以用在向内存读取文件时。地址空间用于将文件的内容(数据)与装载数据的内存区关联起来。通过一个小技巧,mapping不仅能够保存一个指针,而且还能包含一些额外的信息,用于判断页是否属于未关联到地址空间的某个匿名内存区。如果将mapping置为1,则该指针并不指向address_space的实例,而是指向另一个数据结构(anon_vma),该结构对实现匿名页的逆向映射很重要。对该指针的双重使用是可能的,因为address_space实例总是对齐到sizeof(long)。因此在Linux支持的所有计算机上,指向该实例的指针最低位总是0。该指针如果指向address_space实例,则可以直接使用。如果使用了技巧将最低位设置为1,内核可使用下列操作恢复来恢复指针:anon_vma = (struct anon_vma *) (mapping -PAGE_MAPPING_ANON)private是一个指向“私有”数据的指针,虚拟内存管理会忽略该数据。virtual用于高端内存区域中的页,换言之,即无法直接映射到内核内存中的页。virtual用
1
2
3
4
5
于存储该页的虚拟地址。
按照预处理器语句#if defined(WANT_PAGE_VIRTUAL),只有定义了对应的宏,virtual才能
成为struct page的一部分。
页的不同属性通过一系列页标志描述,存储为struct page的flags成员中的各个比特位。这些
标志独立于使用的体系结构,因而无法提供特定于CPU或计算机的信息(该信息保存在页表中,见下
文可知)。
各个标志是由page-flags.h中的宏定义的,此外还生成了一些宏,用于标志的设置、删除、查
询。这样做时,内核遵守了一种通用的命名方案。
例如,PG_locked常数定义了标志中用于指定页锁定与否的比特位置。下列宏可以用来操作该 比
特位:
PageLocked查询比特位是否置位;
SetPageLocked设置PG_locked位,不考虑先前的状态;
TestSetPageLocked设置比特位,而且返回原值;
ClearPageLocked清除比特位,不考虑先前的状态;
TestClearPageLocked清除比特位,返回原值。
对其他的页标志,同样有一组宏用来操作对应的比特位。这些宏的实现是原子的。尽管其中一些
由若干语句组成,但使用了特殊的处理器命令,确保其行为如同单一的语句。即这些语句是无法中断
的,否则会导致竞态条件。第5章讲述了竞态条件是如何出现的,以及如何防止。
有哪些页标志可用?以下列出了最重要的标志(其含义在以后几章里会变得清楚一些)。
PG_locked指定了页是否锁定。如果该比特位置位,内核的其他部分不允许访问该页。这防止
了内存管理出现竞态条件,例如,在从硬盘读取数据到页帧时。
如果在涉及该页的I/O操作期间发生错误,则PG_error置位。
PG_referenced和PG_active控制了系统使用该页的活跃程度。在页交换子系统选择换出页
时,该信息是很重要的。这两个标志的交互将在第18章解释。
PG_uptodate表示页的数据已经从块设备读取,其间没有出错。
如果与硬盘上的数据相比,页的内容已经改变,则置位PG_dirty。出于性能考虑,页并不在
每次改变后立即回写。因此内核使用该标志注明页已经改变,可以在稍后刷出。
设置了该标志的页称为脏的(通常,该意味着内存中的数据没有与外存储器介质如硬盘上的
数据同步)。
PG_lru有助于实现页面回收和切换。内核使用两个最近最少使用(least recently used,lru)链
表①来区别活动和不活动页。如果页在其中一个链表中,则设置该比特位。还有一个PG_active
标志,如果页在活动页链表中,则设置该标志。第18章详细讨论了这一重要机制。
PG_highmem表示页在高端内存中,无法持久映射到内核内存中。
如果page结构的private成员非空,则必须设置PG_private位。用于I/O的页,可使用该字段
将页细分为多个缓冲区(更多信息请参见第16章),但内核的其他部分也有各种不同的方法,
将私有数据附加到页上。
如果页的内容处于向块设备回写的过程中,则需要设置PG_writeback位。
如果页是3.6节讨论的slab分配器的一部分,则设置PG_slab位。
如果页处于交换缓存,则设置PG_swapcache位。在这种情况下,private包含一个类型为
swap_entry_t的项(更多信息请参见第18章)。
在可用内存的数量变少时,内核试图周期性地回收页,即剔除不活动、未用的页。第18章讨
论了相关细节。在内核决定回收某个特定的页之后,需要设置PG_reclaim标志通知。
如果页空闲且包含在伙伴系统的列表中,则设置PG_buddy位,伙伴系统是页分配机制的核心。
PG_compound表示该页属于一个更大的复合页,复合页由多个毗连的普通页组成。
内核定义了一些标准宏,用于检查页是否设置了某个特定的比特位,或者操作某个比特位。这些
宏的名称有一定的模式,如下所述。
PageXXX(page)会检查页是否设置了PG_XXX位。例如,PageDirty检查PG_dirty位,而Page-
Active检查PG_active位,等等。
SetPageXXX在某个比特位没有设置的情况下,设置该比特位,并返回原值。
ClearPageXXX无条件地清除某个特定的比特位。
TestClearPageXXX清除某个设置的比特位,并返回原值。
3.3 页表
在以后几节里描述的数据结构和函数,通常基于体系结构相关的文件中提供的接口。定义可以在
头文件include/asm-arch/page.h和include/asm-arch/pgtable.h中找到,下文简称为page.h和
pgtable.h。虽然AMD64和IA-32已经统一为一个体系结构,但在处理页表方面仍然有很大差别,因
此相关的定义分为两个不同的文件: include/asm-x86/page_32.h和include/asm-x86/page_64.h,
类似地有pgtable_XX.h。
3.3.1 数据结构
在C语言中,void *数据类型用于定义可能指向内存中任何字节位置的指针。该类型所需的比特
位数目依不同体系结构而不同。所有常见的处理器(包括Linux支持的所有处理器)都使用32位或64位。
内核源代码假定void *和unsigned long类型所需的比特位数相同,因此它们之间可以进行强制
转换而不损失信息。该假定的形式表示为sizeof(void *) == sizeof(unsigned long),在Linux
支持的所有体系结构上都是正确的。
内存管理更喜欢使用unsigned long类型的变量, 而不是void指针,因为前者更易于处理和操作。
技术上,它们都是有效的。
3.4 初始化内存管理
在内存管理的上下文中,初始化(initialization)可以有多种含义。在许多CPU上,必须显式设置
适于Linux内核的内存模型。例如,在IA-32系统上需要切换到保护模式,然后内核才能检测可用内存
和寄存器。在初始化过程中,还必须建立内存管理的数据结构,以及其他很多事务。因为内核在内存
管理完全初始化之前就需要使用内存,在系统启动过程期间,使用了一个额外的简化形式的内存管理
模块,然后又丢弃掉。
因为内存管理初始化中特定于CPU的部分使用了底层体系结构许多次要、微妙的细节,这些与内
核的结构没什么关系,最多不过是汇编语言程序设计的最佳实践而已,因此我们在本节中只是从一个比较高的层次来考虑初始化相关的工作。关键是pg_data_t数据结构的初始化
3.4.1 建立数据结构
对相关数据结构的初始化是从全局启动例程start_kernel中开始的,该例程在加载内核并激活
各个子系统之后执行。由于内存管理是内核一个非常重要的部分,因此在特定于体系结构的设置步骤
中检测内存并确定系统中内存的分配情况后,会立即执行内存管理的初始化(3.4.2节以IA-32系统为
例,简要描述了初始化中系统相关部分的实现)。此时,已经对各种系统内存模式生成了一个pgdata_t
实例,用于保存诸如结点中内存数量以及内存在各个内存域之间分配情况的信息。所有平台上都实现
了特定于体系结构的NODE_DATA宏,用于通过结点编号,来查询与一个NUMA结点相关的pgdata_t
实例。
- 先决条件
由于大部分系统都只有一个内存结点,下文只考察此类系统。具体是什么样的情况呢?为确保内
存管理代码是可移植的(因此它可以同样用于UMA和NUMA系统),内核在mm/page_alloc.c中定义
了一个pg_data_t实例(称作contig_page_data)管理所有的系统内存。根据该文件的路径名可以
看出,这不是特定于CPU的实现。实际上,大多数体系结构都采用了该方案。NODE_DATA的实现现在
更简单了。
1
2
3
4
5
6
7
8
9
10
/* file: include/linux/numa.h */
extern struct pglist_data *node_data[];
#define NODE_DATA(nid) (node_data[nid])
/* file: include/linux/mmzone.h */
extern struct pglist_data contig_page_data;
static inline struct pglist_data *NODE_DATA(int nid)
{
return &contig_page_data;
}
简单了。
✅ `Linux`内核是否使用`bitmap`表示整个物理内存? 是,但不是直接拿`bitmap`代表整个物理内存分配状态。在`Linux`的内存管理中,`bitmap`用于一些子系统,比如: - `buddy allocator`:使用`bitmap`来跟踪每阶每页块是否空闲 - `page allocator`:每个页的元数据由`struct page`表示,整个物理页数组`mem_map[]`是主控 - `SLAB/SLUB/SLOB allocators`:也使用`bitmap`管理小块分配(<1页) ✅ 物理内存划分成阶级(`order`)管理是否基于`bitmap`? 是的,`buddy system`在每个`zone`的每个`order`中都有一个`freelist`和`bitmap`。 - `Linux`内核将物理内存分为不同的`zone`(`DMA, Normal, HighMem...`) - 每个`zone`维护一个`free_area[]`数组,每个元素表示一个阶 ```c /* file: include/linux/mmzone.h */ struct zone { /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */ unsigned long zone_start_pfn; /* * spanned_pages is the total pages spanned by the zone, including * holes, which is calculated as: * spanned_pages = zone_end_pfn - zone_start_pfn; * * present_pages is physical pages existing within the zone, which * is calculated as: * present_pages = spanned_pages - absent_pages(pages in holes); * * present_early_pages is present pages existing within the zone * located on memory available since early boot, excluding hotplugged * memory. * * managed_pages is present pages managed by the buddy system, which * is calculated as (reserved_pages includes pages allocated by the * bootmem allocator): * managed_pages = present_pages - reserved_pages; * * cma pages is present pages that are assigned for CMA use * (MIGRATE_CMA). * * So present_pages may be used by memory hotplug or memory power * management logic to figure out unmanaged pages by checking * (present_pages - managed_pages). And managed_pages should be used * by page allocator and vm scanner to calculate all kinds of watermarks * and thresholds. * * Locking rules: * * zone_start_pfn and spanned_pages are protected by span_seqlock. * It is a seqlock because it has to be read outside of zone->lock, * and it is done in the main allocator path. But, it is written * quite infrequently. * * The span_seq lock is declared along with zone->lock because it is * frequently read in proximity to zone->lock. It's good to * give them a chance of being in the same cacheline. * * Write access to present_pages at runtime should be protected by * mem_hotplug_begin/done(). Any reader who can't tolerant drift of * present_pages should use get_online_mems() to get a stable value. */ atomic_long_t managed_pages; unsigned long spanned_pages; unsigned long present_pages; struct free_area free_area[MAX_ORDER]; // MAX_ORDER=11,阶0~10 }; ``` - 每个 `free_area` 长这样: ``` c 复制编辑 struct free_area { struct list_head free_list; // 双向链表,记录空闲块 unsigned long *map; // bitmap,记录每个块是否空闲 }; ``` ### bitmap 的作用: - 标记某个阶级的页块是否空闲 - 和 freelist 一起支持快速合并/拆分 ------ ## ✅ 3. 内存是如何分阶管理的? 以 1GB 内存为例(不考虑 NUMA 和 zone): - 拆分成 4KB 页(2^20 / 2^12 = 2^8 = 256K 页) - 每个 `struct page` 代表一个物理页,在 boot 时初始化 `mem_map[]` - 每个 zone 建立阶级数组 `free_area[0...MAX_ORDER-1]` - 每阶下有若干个 block,每 block 是 2^order 页 - 每阶有对应的 bitmap 和 free_list `/proc/zoneinfo` 是 Linux 内核提供的一个文件,用于显示系统内存区域(zone)的详细信息。这些信息对于理解系统内存使用情况和性能调优非常有帮助。 ## 主要部分解析 ### 1. 节点和区域结构 输出按 NUMA 节点(Node)和内存区域(zone)组织: text ``` Node 0, zone DMA Node 0, zone DMA32 Node 0, zone Normal Node 0, zone Movable Node 0, zone Device ``` - **Node 0**:表示第一个 NUMA 节点(在单处理器系统中通常只有 Node 0) - **zone**:内存区域类型,常见的有: - **DMA**:直接内存访问区域(通常 <16MB) - **DMA32**:32位设备可访问区域(通常 <4GB) - **Normal**:普通内存区域 - **Movable**:可移动内存区域(用于内存热插拔) - **Device**:设备内存区域 ### 2. 关键字段解释 每个区域包含以下重要信息: #### 内存统计 - **pages free**:空闲页面数 - **min/low/high**:内存水位标记,用于内存回收 - `min`:最低水位,低于此值开始积极回收内存 - `low`:低水位,低于此值开始温和回收 - `high`:高水位,回收内存到此值停止 - **spanned/present/managed**: - `spanned`:区域总大小(包括空洞) - `present`:实际存在的物理内存 - `managed`:由伙伴系统管理的内存 #### 使用情况统计 - **nr_*** 系列:各种类型页面的计数 - `nr_inactive_anon`:非活跃的匿名页 - `nr_active_anon`:活跃的匿名页(进程堆栈、堆等) - `nr_inactive_file`:非活跃的文件缓存页 - `nr_active_file`:活跃的文件缓存页 - `nr_slab_reclaimable`:可回收的slab内存 - `nr_slab_unreclaimable`:不可回收的slab内存 - `nr_mapped`:被映射到进程地址空间的页面 #### NUMA相关统计 - **numa_hit**:在本节点分配成功次数 - **numa_miss**:在本节点分配失败次数 - **numa_local**:本地分配次数 #### 每CPU页缓存(pagesets) 显示每个CPU核心的页缓存状态: - `count`:当前缓存页面数 - `high`:高水位标记 - `batch`:批量操作大小 ### 3. 具体区域分析 #### DMA 区域 - 非常小的内存区域(仅4095页,约16MB) - 主要用于旧设备DMA操作 - 当前空闲页面很少(3576页,约14MB) #### DMA32 区域 - 中等大小区域(约3GB物理内存) - 空闲内存较多(426926页,约1.7GB) - 文件缓存较少,匿名页较多 #### Normal 区域 - 主要内存区域(约5GB物理内存) - 空闲内存较少(13307页,约52MB) - 文件缓存和匿名页都很多 - 有133页等待写入磁盘(nr_zone_write_pending) ### 4. 系统整体内存状况 从输出可以看出: 1. 系统主要使用Normal区域内存,且空闲内存较少(仅52MB) 2. DMA32区域还有较多空闲内存(1.7GB) 3. 文件缓存占用较多内存(约732902页,2.8GB) 4. 匿名页也占用较多内存(约650843页,2.5GB) 5. 有144页脏数据等待写入(nr_dirty) 6. 系统没有使用交换空间(nr_swapcached=0) ### 5. 性能调优提示 - 系统Normal区域内存压力较大(空闲内存接近low水位) - 如果应用性能下降,可能需要: - 减少内存使用 - 增加swap空间 - 调整vm.swappiness参数 - 优化应用使用文件缓存的方式 这个输出对于诊断内存相关性能问题非常有用,特别是在内存不足或交换频繁的情况下。