Post

内存管理

内存管理

内存管理的实现涵盖了许多领域:

  • 内存中的物理内存页的管理;

  • 分配大块内存的伙伴系统;

  • 分配较小块内存的slabslubslob分配器;

  • 分配非连续内存块的vmalloc机制;

  • 进程的地址空间。

实际上内核会区分3 种配置选项:FLATMEMDISCONTIGMEMSPARSEMEMSPARSEMEMDISCONTIGMEM实际上作用相同,但从开发者的角度看来,对应代码的质量有所不同。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。在AlphaAMD64系统上,该内存域的长度可能从0到4 GiB

  • ZONE_NORMAL标记了可直接映射到内核段的普通内存域。这是在所有体系结构上保证都会存在的唯一内存域,但无法保证该地址范围对应了实际的物理内存。例如,如果AMD64系统有2 GiB内存,那么所有内存都属于ZONE_DMA32范围,而ZONE_NORMAL则为空。

  • ZONE_HIGHMEM标记了超出内核段的物理内存。

根据编译时的配置,可能无需考虑某些内存域。例如在64位系统中,并不需要高端内存域。如果支持了只能访问4 GiB以下内存的32位外设,才需要DMA32内存域。

🍃 3.2.2 数据结构

我已经解释了用于内存管理的各种数据结构之间的关系,现在我们分别讲解各个数据结构。

  1. 结点管理

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_pfnUMA系统中总是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_POSSIBLEN_ONLINEN_CPU用于CPU和内存的热插拔。对内存管理有必要的标志是N_HIGH_MEMORYN_NORMAL_MEMORY。如果结点有普通或高端内存则使用N_HIGH_MEMORY,仅当结点没有高端内存才设置N_NORMAL_MEMORY

  1. 内存域

内核使用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_minpages_highpages_low是页换出时使用的“水印”。如果内存不足,内核可以将页写到硬盘。这3个成员会影响交换守护进程的行为。

    • 如果空闲页多于pages_high,则内存域的状态是理想的。
    • 如果空闲页的数目低于pages_low,则内核开始将页换出到硬盘。

    • 如果空闲页的数目低于pages_min,那么页回收工作的压力就比较大,因为内存域中急需空闲页。
  • zone_start_pfn是内存域第一个页帧的索引。

假设有这几个zone

  • ZONE_DMA
  • ZONE_NORMAL
  • ZONE_HIGHMEM

每个zonelowmem_reserve[]值可能是:

zone namelowmem_reserve[DMA]lowmem_reserve[NORMAL]lowmem_reserve[HIGHMEM]
DMA0--
NORMAL1280-
HIGHMEM2561280

这意味着:

  • 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. 内存域水印的计算

1. setup_per_zone_pages_min() 的原始作用

该函数用于初始化每个内存区域(struct zone)的 minlowhigh 水位值(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_LOWWMARK_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. 为什么被替代?

  1. 模块化设计:水位计算拆分为更小的函数,便于维护和动态调整。
  2. 动态响应:支持运行时根据内存压力调整水位(如通过 sysctl)。
  3. 代码清晰性:旧版 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()
  • 核心变化:从静态初始化变为动态计算,更灵活适应内存压力。
  • 关联机制:水位值与 kswapddirect reclaimmin_free_kbytes 紧密相关。

这个结构体 struct per_cpu_pages 是 Linux 内核中用于管理 每 CPU 页帧缓存(Per-CPU Page Frame Cache, PCP) 的关键数据结构,主要用于优化内存分配性能,减少对全局锁的竞争。以下是各字段的详细解释:


1. 核心字段解析

(1) 锁与保护机制

  • spinlock_t lock 保护 listscount 等字段的自旋锁,确保多核并发访问时的安全性。

(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 启用时有效,表示远程节点页面缓存的过期时间,超时后需回收。
  1. 页帧

页帧代表系统内存的最小单位,对内存中的每个页都会创建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

实例。

  1. 先决条件

由于大部分系统都只有一个内存结点,下文只考察此类系统。具体是什么样的情况呢?为确保内

存管理代码是可移植的(因此它可以同样用于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;
}

简单了。

\#define NODE_DATA(nid) (&contig_page_data) 尽管该宏有一个形式参数用于选择NUMA结点,但在UMA系统中只有一个伪结点,因此总是返 回同样的数据。 内核也可以依赖于下述事实:体系结构相关的初始化代码将numnodes变量设置为系统中结点的 数目。在UMA系统上因为只有一个(形式上的)结点,因此该数量是1。 在编译时间,预处理器语句会为特定的配置选择正确的定义。  setup_arch是一个特定于体系结构的设置函数,其中一项任务是负责初始化自举分配器。  在SMP系统上, setup_per_cpu_areas初始化源代码中(使用per_cpu宏) 定义的静态per-cpu 变量,这种变量对系统中的每个CPU都有一个独立的副本。此类变量保存在内核二进制映像的 一个独立的段中。setup_per_cpu_areas的目的是为系统的各个CPU分别创建一份这些数据 的副本。 在非SMP系统上该函数是一个空操作。 ```c /* * unless system_state == SYSTEM_BOOTING. * * __ref due to call of __init annotated helper build_all_zonelists_init * [protected by SYSTEM_BOOTING]. */ void __ref build_all_zonelists(pg_data_t *pgdat) { unsigned long vm_total_pages; if (system_state == SYSTEM_BOOTING) { build_all_zonelists_init(); } else { __build_all_zonelists(pgdat); /* cpuset refresh routine should be here */ } /* Get the number of free pages beyond high watermark in all zones. */ vm_total_pages = nr_free_zone_pages(gfp_zone(GFP_HIGHUSER_MOVABLE)); /* * Disable grouping by mobility if the number of pages in the * system is too low to allow the mechanism to work. It would be * more accurate, but expensive to check per-zone. This check is * made on memory-hotadd so a system can start with mobility * disabled and enable it later */ if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES)) page_group_by_mobility_disabled = 1; else page_group_by_mobility_disabled = 0; pr_info("Built %u zonelists, mobility grouping %s. Total pages: %ld\n", nr_online_nodes, str_off_on(page_group_by_mobility_disabled), vm_total_pages); #ifdef CONFIG_NUMA pr_info("Policy zone: %s\n", zone_names[policy_zone]); #endif } ``` 们回到建立内存域列表的工作。build_all_zonelists_init中我们当前感兴趣的那部分(对于页分 配器的页组可移动性扩展,实际上还有另外一些工作,我会在下文单独讨论)将所有工作都委托给 __build_all_zonelists,后者又对系统中的各个NUMA结点分别调用build_zonelists。 ```c static void __build_all_zonelists(void *data) { int nid; int __maybe_unused cpu; pg_data_t *self = data; unsigned long flags; /* * The zonelist_update_seq must be acquired with irqsave because the * reader can be invoked from IRQ with GFP_ATOMIC. */ write_seqlock_irqsave(&zonelist_update_seq, flags); /* * Also disable synchronous printk() to prevent any printk() from * trying to hold port->lock, for * tty_insert_flip_string_and_push_buffer() on other CPU might be * calling kmalloc(GFP_ATOMIC | __GFP_NOWARN) with port->lock held. */ printk_deferred_enter(); #ifdef CONFIG_NUMA memset(node_load, 0, sizeof(node_load)); #endif /* * This node is hotadded and no memory is yet present. So just * building zonelists is fine - no need to touch other nodes. */ if (self && !node_online(self->node_id)) { build_zonelists(self); } else { /* * All possible nodes have pgdat preallocated * in free_area_init */ for_each_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); build_zonelists(pgdat); } #ifdef CONFIG_HAVE_MEMORYLESS_NODES /* * We now know the "local memory node" for each node-- * i.e., the node of the first zone in the generic zonelist. * Set up numa_mem percpu variable for on-line cpus. During * boot, only the boot cpu should be on-line; we'll init the * secondary cpus' numa_mem as they come on-line. During * node/memory hotplug, we'll fixup all on-line cpus. */ for_each_online_cpu(cpu) set_cpu_numa_mem(cpu, local_memory_node(cpu_to_node(cpu))); #endif } printk_deferred_exit(); write_sequnlock_irqrestore(&zonelist_update_seq, flags); } ``` for_each_online_node遍历了系统中所有的活动结点。由于UMA系统只有一个结点,build_ zonelists只调用了一次,就对所有的内存创建了内存域列表。NUMA系统调用该函数的次数等同于 结点的数目。每次调用对一个不同结点生成内存域数据。 build_zonelists需要一个指向pgdata_t实例的指针作为参数,其中包含了结点内存配置的所 有现存信息,而新建的数据结构也会放置在其中。 在UMA系统上,NODE_DATA返回contig_page_data的地址。 该函数的任务是,在当前处理的结点和系统中其他结点的内存域之间建立一种等级次序。接下来, 依据这种次序分配内存。如果在期望的结点内存域中,没有空闲内存,那么这种次序就很重要。 我们考虑一个例子,其中内核想要分配高端内存。它首先企图在当前结点的高端内存域找到一个 大小适当的空闲段。如果失败,则查看该结点的普通内存域。如果还失败,则试图在该结点的DMA 内存域执行分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。在这种情况下,备 选结点应该尽可能靠近主结点,以最小化由于访问非本地内存引起的性能损失。 内核定义了内存的一个层次结构,首先试图分配“廉价的”内存。如果失败,则根据访问速度和 容量,逐渐尝试分配“更昂贵的”内存。 高端内存是最廉价的,因为内核没有任何部份依赖于从该内存域分配的内存。如果高端内存域用 尽,对内核没有任何副作用,这也是优先分配高端内存的原因。 普通内存域的情况有所不同。许多内核数据结构必须保存在该内存域,而不能放置到高端内存域。 因此如果普通内存完全用尽,那么内核会面临紧急情况。所以只要高端内存域的内存没有用尽,都不 会从普通内存域分配内存。 最昂贵的是DMA内存域,因为它用于外设和系统之间的数据传输。因此从该内存域分配内存是 最后一招。 内核还针对当前内存结点的备选结点,定义了一个等级次序。这有助于在当前结点所有内存域的 内存都用尽时,确定一个备选结点。 内核使用pg_data_t中的zonelist数组,来表示所描述的层次结构。 typedef struct pglist_data { ... struct zonelist node_zonelists[MAX_ZONELISTS]; ... } pg_data_t; \#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES) struct zonelist { ... struct zone *zones[MAX_ZONES_PER_ZONELIST + 1]; // NULL分隔 }; node_zonelists数组对每种可能的内存域类型,都配置了一个独立的数组项。数组项包含了类 型为zonelist的一个备用列表,其结构在下面讨论。 由于该备用列表必须包括所有结点的所有内存域,因此由MAX_NUMNODES * MAX_NZ_ZONES项组 成,外加一个用于标记列表结束的空指针。 建立备用层次结构的任务委托给build_zonelists,该函数为每个NUMA结点都创建了相应的数 据结构。它需要指向相关的pg_data_t实例的指针作为参数。在我详细讨论代码之前,先回想一下上 文提到的一个问题。我们已经将讨论的范围限制到UMA系统,为什么必须考虑多个NUMA结点呢? 实际上,如果设置了CONFIG_NUMA,内核会使用不同的实现替换下列代码。但也有可能某个体系结构 在UMA系统上选择不连续或稀疏内存选项。在地址空间包含较大空洞的情况下,这样做可能是有好处 的。这样的洞造成的内存“块”,最好通过NUMA提供的数据结构来处理。这也是为什么此处需要处 理NUMA结点的原因。 一个大的外部循环首先迭代所有的结点内存域。每个循环在zonelist数组中找到第i个zonelist, 对第i个内存域计算备用列表。 mm/page_alloc.c static void __init build_zonelists(pg_data_t *pgdat) { int node, local_node; enum zone_type i,j; local_node = pgdat->node_id; for (i = 0; i < MAX_NR_ZONES; i++) { struct zonelist *zonelist; zonelist = pgdat->node_zonelists + i; j = build_zonelists_node(pgdat, zonelist, 0, i); ... } node_zonelists的数组元素通过指针操作寻址,这在C语言中是完全合法的惯例。实际工作则委 托给build_zonelist_node。在调用时,它首先生成本地结点内分配内存时的备用次序。 mm/page_alloc.c static int __init build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int nr_zones, enum zone_type zone_type) { struct zone *zone; 1 2 3 4 5 6 7 8 9 10134 第 3 章 内 存 管 理 do { zone = pgdat->node_zones + zone_type; if (populated_zone(zone)) { zonelist->zones[nr_zones++] = zone; } zone_type--; } while (zone_type >= 0); return nr_zones; } 备用列表的各项是借助于zone_type参数排序的,该参数指定了最优先选择哪个内存域,该参数 的初始值是外层循环的控制变量i。我们知道其值可能是ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA 或ZONE_DMA32之一。nr_zones表示从备用列表中的哪个位置开始填充新项。由于列表中尚没有项, 因此调用者传递了0。 内核在build_zonelists中按分配代价从昂贵到低廉的次序,迭代了结点中所有的内存域。而在 build_zonelists_node中,则按照分配代价从低廉到昂贵的次序,迭代了分配代价不低于当前内存 域的内存域。在build_zonelists_node的每一步中,都对所选的内存域调用populated_zone,确认 zone->present_pages大于0,即确认内存域中确实有页存在。倘若如此,则将指向zone实例的指针 添加到zonelist->zones中的当前位置。后备列表的当前位置保存在nr_zones。 在每一步结束时,都将内存域类型减1。换句话说,设置为一个更昂贵的内存域类型。例如,如 果开始的内存域是ZONE_HIGHMEM,减1后下一个内存域类型是ZONE_NORMAL。 考虑一个系统,有内存域ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA。在第一次运行build_ zonelists_node时,实际上会执行下列赋值: zonelist->zones[0] = ZONE_HIGHMEM; zonelist->zones[1] = ZONE_NORMAL; zonelist->zones[2] = ZONE_DMA; 图3-9以某个系统的结点2为例说明了这一点,图中示范了一个备用列表在多次循环中不断填充的 过程。系统中总共有4个结点(numnodes = 4)。 A=(NUMA)结点0 0=DMA内存域 B=(NUMA)结点1 1=普通内存域 C=(NUMA)结点2 2=高端内存域 D=(NUMA)结点3 图3-9 连续填充备用列表 第一步之后,列表中的分配目标是高端内存,接下来是第二个结点的普通和DMA内存域。 内核接下来必须确立次序,以便将系统中其他结点的内存域按照次序加入到备用列表。 mm/page_alloc.c static void __init build_zonelists(pg_data_t *pgdat) { ... for (node = local_node + 1; node < MAX_NUMNODES; node++) { j = build_zonelists_node(NODE_DATA(node), zonelist, j, i); } for (node = 0; node < local_node; node++) {3.4 初始化内存管理 135 j = build_zonelists_node(NODE_DATA(node), zonelist, j, i); } zonelist->zones[j] = NULL; } } } 第一个循环依次迭代大于当前结点编号的所有结点。在我们的例子中,有4个结点编号副本为0、 1、2、3,此时只剩下结点3。新的项通过build_zonelists_node被加到备用列表。此时j的作用就体 现出来了。在本地结点的备用目标找到之后,该变量的值是3。该值用作新项的起始位置。如果结点3 也由3个内存域组成,备用列表在第二个循环之后的情况如图3-9的第二步所示。 第二个for循环接下来对所有编号小于当前结点的结点生成备用列表项。在我们的例子中,这些 结点的编号为0和1。 如果这些结点也有3个内存域,则循环完毕之后备用列表的情况如图3-9下半部分 所示。 备用列表中项的数目一般无法准确知道,因为系统中不同结点的内存域配置可能并不相同。因此 列表的最后一项赋值为空指针,显式标记列表结束。 对总数N个结点中的结点m来说,内核生成备用列表时,选择备用结点的顺序总是:m、m+1、 m+2、…、N1、0、1、…、m1。这确保了不过度使用任何结点。例如,对照情况是:使用一个独立 于m、不变的备用列表。 图3-10给出了有4个结点的系统中为第三结点建立的备用列表。 普通内存域 高端内存域 DMA内存域 图3-10 完成的备用列表 3.5.5节讨论了如何利用此处生成的备用列表实现伙伴系统。 ###### 3.4.2 特定于体系结构的设置 在 IA-32 系统上,内存管理的初始化在某些方面显得格外微妙,需要克服一些源于处理器架构历史的障碍。例如,必须将处理器从实模式切换到保护模式,并显式授予 CPU 访问 32 位地址空间的权限——这些步骤都是为了兼容早期的 16 位 8086 处理器而保留下来的遗产。同样,分页机制在默认情况下是关闭的,必须通过操作 CR0 寄存器手动启用。 虽然用来划定段边界的变量定义在内核源代码(arch/x86/kernel/setup_32.c)中, 但此时尚未赋值。这是因为不太可能。编译器在编译时间怎么能知道内核最终有多大?只有 在目标文件链接完成后,才能知道确切的数值,接下来则打包为二进制文件。该操作是由 arch/arch/vmlinux.ld.S控制的(对IA-32来说,该文件是arch/x86/vmlinux_32.ld.S), 其中也划定了内核的内存布局。 2. 初始化步骤 在内核已经载入内存、而初始化的汇编程序部分已经执行完毕后,内核必须执行哪些特定于系统 的步骤? 3. 分页机制的初始化 paging_init负责建立只能用于内核的页表,用户空间无法访问。这对管理普通应用程序和内核 访问内存的方式,有深远的影响。因此在仔细考察其实现之前,很重要的一点是解释该函数的目的。 第1章提到,在IA-32系统上内核通常将总的4 GiB可用虚拟地址空间按3 : 1的比例划分。低端3 GiB 用于用户状态应用程序,而高端的1 GiB则专用于内核。尽管在分配内核的虚拟地址空间时,当前系统 上下文是不相干的,但每个进程都有自身特定的地址空间。 这些划分主要的动机如下所示。  在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性 的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配 给内核专用。  物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作。 ##### 3.5 物理内存的管理 阶是伙伴系统中一个非常重要的术语。它描述了内存分配的数量单位。内存块的长度是2order,其 中order的范围从0到MAX_ORDER。 \#ifndef CONFIG_FORCE_MAX_ZONEORDER \#define MAX_ORDER 11 \#else \#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER \#endif \#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER -1)) 该常数通常设置为11,这意味着一次分配可以请求的页数最大是211=2 048。但如果特定于体系结 构的代码设置了FORCE_MAX_ZONEORDER配置选项,该值也可以手工改变。例如,IA-64系统上巨大的 地址空间可以处理MAX_ORDER = 18的情形,而ARM或v850系统则使用更小的值(如8或9) 。但这不一 定是由计算机支持的内存数量比较小引起的,也可能是内存对齐方式的要求所导致。或者可以参考 v850体系结构的Kconfig配置文件的描述: 伙伴不必是彼此连接的。如果一个内存区在分配其间分解为两半,内核会自动将未用的一半加入 到对应的链表中。如果在未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通 过其地址判断其是否为伙伴。管理工作较少,是伙伴系统的一个主要优点。 基于伙伴系统的内存管理专注于某个结点的某个内存域,例如,DMA或高端内存域。但所有内 存域和结点的伙伴系统都通过备用分配列表连接起来。图3-23说明了这种关系。 在首选的内存域或节点无法满足内存分配请求时,首先尝试同一结点的另一个内存域,接下来再 尝试另一个结点,直至满足请求。 这段话虽然有些拗口,但它在讲述 **Linux 内核中伙伴系统(buddy allocator)的设计和内存分配策略**。下面我帮你逐句拆解并解释: ------ ### 📌 原文一: > **伙伴不必是彼此连接的。如果一个内存区在分配其间分解为两半,内核会自动将未用的一半加入到对应的链表中。** #### ✅ 理解: - 在 buddy 分配器中,一个大块的内存(例如 order 3)可能被拆成两个较小块(order 2)。 - 被使用的一半会返回给调用者,**另一半(空闲的)会被加入 `free_area[2]` 链表**中管理。 - 所谓“伙伴”是指两个相邻的同阶块,它们可以在未来被合并。 > ⚠️ “不必连接”是说:伙伴页并不一定在链表中直接相邻或以某种结构绑定在一起,而是**按需拆分、按需组合**。 ------ ### 📌 原文二: > **如果在未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通过其地址判断其是否为伙伴。** #### ✅ 理解: - 内核通过页框物理地址快速判断两个空闲块是否是“伙伴”。 - 判断方式是: ``` c 复制编辑 is_buddy = ((addr_of_block_1 ^ addr_of_block_2) == size_of_block) ``` 或者换句话说,两个块必须: - 连续 - 大小相同 - 地址对齐 - 如果判断为伙伴,就可以将它们从链表中移除、合并为更大的块(order+1)。 > 📌 这体现了 buddy 的高效:**无需维护复杂结构,仅靠地址判断是否合并**。 ------ ### 📌 原文三: > **管理工作较少,是伙伴系统的一个主要优点。** #### ✅ 理解: - 不需要复杂的树结构或指针关系,仅需按 `order` 管理多个链表。 - 空闲块之间靠地址关系判断是否为“伙伴”,合并时也不复杂。 - 这让它的实现简单、分配/释放速度快,适合内核页框分配器使用。 ------ ### 📌 原文四: > **基于伙伴系统的内存管理专注于某个结点的某个内存域,例如,DMA或高端内存域。** #### ✅ 理解: - 在 NUMA 或普通系统中,物理内存被分成多个 zone: - `ZONE_DMA`:用于特殊设备 - `ZONE_NORMAL`:常规内存 - `ZONE_HIGHMEM`:x86 32 位上的高端内存(x86-64 没有了) - 每个 `zone` 都维护自己的 `buddy allocator`(即 `zone->free_area[]`) - **伙伴系统的作用范围局限于某一个 zone**。 ------ ### 📌 原文五: > **但所有内存域和结点的伙伴系统都通过备用分配列表连接起来。图3-23说明了这种关系。** #### ✅ 理解: - 当某个 zone 中分配失败,比如 `ZONE_NORMAL` 无法满足需求: - 内核可以尝试 fallback 到: - 该节点(node)中的其他 zone(如 `ZONE_HIGHMEM`) - 或其他节点的内存(在 NUMA 系统中) - 这种“备用分配逻辑”由 `zonelist` 结构管理。 - `zonelist` 是一个 per-node 的 fallback 链表:记录了优先尝试的 zone 顺序。 ------ ### 📌 原文六: > **在首选的内存域或节点无法满足内存分配请求时,首先尝试同一结点的另一个内存域,接下来再尝试另一个结点,直至满足请求。** #### ✅ 理解: - 内存分配不是全局无序查找,而是有一套优先级策略。 - 举个例子: 1. 首先尝试 `local node -> ZONE_NORMAL` 2. 然后 `local node -> ZONE_HIGHMEM` 3. 再尝试 `remote node -> ZONE_NORMAL` - 这种逻辑由 `__alloc_pages()` 和 `get_page_from_freelist()` 实现。 ------ ## ✅ 总结一句话: > Linux 的 buddy 系统通过每个 zone 的 `free_area[]` 分级管理空闲页面,块之间无需显式连接,是否为伙伴靠地址判断,结构简单高效。而实际分配策略则由 `zonelist` 控制,从首选 zone 向其他 zone 和 node 回退,直到满足需求。 如果你还有关于 `buddy` 的代码细节、数据结构或性能问题,也可以深入聊聊。 ##### 3.5 物理内存的管理 ###### 3.5.1 伙伴系统的结构 系统内存中的每个物理内存页,都对应于一个`struct page`实例。每个内存域都关联了一个`struct zone`的实例,其中保存了用于管理伙伴数据的主要数组。 ```c /* Free memory management - zoned buddy allocator. */ #ifndef CONFIG_ARCH_FORCE_MAX_ORDER #define MAX_PAGE_ORDER 10 #else #define MAX_PAGE_ORDER CONFIG_ARCH_FORCE_MAX_ORDER #endif #define MAX_ORDER_NR_PAGES (1 << MAX_PAGE_ORDER) #define NR_PAGE_ORDERS (MAX_PAGE_ORDER + 1) struct zone { struct free_area free_area[NR_PAGE_ORDERS]; }; ``` `nr_free`表示当前内存区域中空闲页块的数量,其统计方式依赖于阶数(`order`): - 对于0阶,按单页统计; - 对于1阶,按每对页统计; - 对于2阶,按每4页为一个单位统计; - 依此类推,`n`阶空闲块表示$2^n$页为一组的空闲块数量。 `free_list`是用于连接空闲页的链表。 阶是伙伴系统中一个非常重要的术语。它描述了内存分配的数量单位。内存块的长度是$2^\text{order}$,其中`order`的范围从0到`MAX_ORDER`。该常数通常设置为11,这意味着一次分配可以请求的页数最大是$2^{11}=2 048$。但如果特定于体系结构的代码设置了`CONFIG_ARCH_FORCE_MAX_ORDER`配置选项,该值也可以手工改变。例如,`IA-64`系统上巨大的地址空间可以处理`MAX_ORDER` = 18的情形,而`ARM`或`v850`系统则使用更小的值(如8或9) 。但这不一定是由计算机支持的内存数量比较小引起的,也可能是内存对齐方式的要求所导致。 每个链表中的节点就是一个连续页块,内核通过该页块中**起始页的第一个页结构(通常是 `struct page`)**中的链表指针来将这些块组织成链表。因此,Linux 不需要为这些连续页块额外引入独立的数据结构——通过复用第一个页帧的元数据就可以将它们挂接进伙伴系统的链表中,实现管理与回收。 伙伴不必是彼此连接的。如果一个内存区在分配其间分解为两半,内核会自动将未用的一半加入到对应的链表中。如果在未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通过其地址判断其是否为伙伴。 基于伙伴系统的内存管理专注于某个结点的某个内存域,例如,DMA或高端内存域。但所有内 存域和结点的伙伴系统都通过备用分配列表连接起来。图3-23说明了这种关系。 在首选的内存域或节点无法满足内存分配请求时,首先尝试同一结点的另一个内存域,接下来再 尝试另一个结点,直至满足请求。 最后要注意,有关伙伴系统当前状态的信息可以在/proc/buddyinfo中获得: ```bash $ cat /proc/buddyinfo Node 0, zone DMA 0 0 0 1 1 1 1 1 1 2 2 Node 0, zone DMA32 2 3 0 1 3 1 4 2 3 3 742 Node 0, zone Normal 3 1 17 142 294 97 52 53 11 6 385 ``` 上述输出给出了各个内存域中每个分配阶中空闲项的数目,从左至右,阶依次升高。 ###### 3.5.2 避免碎片 在第1章给出的简化说明中,一个双链表即可满足伙伴系统的所有需求。在内核版本2.6.23之前,的确是这样。但在内核2.6.24开发期间,内核开发者对伙伴系统的争论持续了相当长时间。这是因为伙伴系统是内核最值得尊敬的一部分,对它的改动不会被大家轻易接受。 1. 依据可移动性组织页 伙伴系统的基本原理已经在第1章中讨论过,其方案在最近几年间确实工作得非常好。但在`Linux`内存管理方面,有一个长期存在的问题:在系统启动并长期运行后,物理内存会产生很多碎片。该情形如图3-24所示。 假定内存由60页组成,这显然不是超级计算机,但用于示例却足够了。左侧的地址空间中散布着空闲页。尽管大约25%的物理内存仍然未分配,但最大的连续空闲区只有一页。这对用户空间应用程序没有问题:其内存是通过页表映射的,无论空闲页在物理内存中的分布如何,应用程序看到的内存似乎总是连续的。右图给出的情形中,空闲页和使用页的数目与左图相同,但所有空闲页都位于一个连续区中。 但对内核来说,碎片是一个问题。由于(大多数)物理内存一致映射到地址空间的内核部分,那么在左图的场景中,无法映射比一页更大的内存区。尽管许多时候内核都分配的是比较小的内存,但也有时候需要分配多于一页的内存。显而易见,在分配较大内存的情况下,右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的。 很有趣的一点是,在大部分内存仍然未分配时,就也可能发生碎片问题。考虑图3-25的情形。只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次。 我提到内存碎片只涉及内核,这只是部分正确的。大多数现代CPU都提供了使用巨型页的可能性,比普通页大得多。这对内存使用密集的应用程序有好处。在使用更大的页时,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但分配巨型页需要连续的空闲物理内存! 很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。在内核2.6.24开发期间,防止碎片的方法最终加入内核。在我讨论具体策略之前,有一点需要澄清。文件系统也有碎片,该领域的碎片问题主要通过碎片合并工具解决。它们分析文件系统,重新排序已分配存储块,从而建立较大的连续存储区。理论上,该方法对物理内存也是可能的,但由于许多物理内存页不能移动到任意位置,阻碍了该方法的实施。因此,内核的方法是反碎片(anti-fragmentation),即试图从最初开始尽可能防止碎片。 反碎片的工作原理如何?为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。 - 不可移动页:在内存中有固定位置,不能移动到其他地方。核心内核分配的大多数内存属于该类别。 - 可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成。例如,映射自文件的数据属于该类别。`kswapd`守护进程会根据可回收页访问的频繁程度,周期性释放此类内存。这是一个复杂的过程,本身就需要详细论述:第18章详细描述了页面回收。目前,了解到内核会在可回收页占据了太多内存时进行回收,就足够了。另外,在内存短缺(即分配失败)时也可以发起页面回收。有关内核发起页面回收的时机,更具体的信息请参考下文。 - 可移动页可以随意地移动。属于用户空间应用程序的页属于该类别。它们是通过页表映射的。如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事。 页的可移动性,依赖该页属于3种类别的哪一种。内核使用的反碎片技术,即基于将具有相同可移动性的页分组的思想。为什么这种方法有助于减少碎片?回想图3-25中,由于页无法移动,导致在原本几乎全空的内存区中无法进行连续分配。根据页的可移动性,将其分配到不同的列表中,即可防止这种情形。例如,不可移动的页不能位于可移动内存区的中间,否则就无法从该内存区分配较大的连续内存块。 想一下,图3-25中大多数空闲页都属于可回收的类别,而分配的页则是不可移动的。如果这些页聚集到两个不同的列表中,如图3-26所示。在不可移动页中仍然难以找到较大的连续空闲空间,但对可回收的页,就容易多了。 但要注意,从最初开始,内存并未划分为可移动性不同的区。这些是在运行时形成的。内核的另一种方法确实将内存分区,分别用于可移动页和不可移动页的分配,我会下文讨论其工作原理。但这种划分对这里描述的方法是不必要的。 **数据结构** 尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些宏来表示不同的迁移类型: ```c 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. */ MIGRATE_CMA, #endif #ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE, /* can't allocate from here */ #endif MIGRATE_TYPES }; ``` 对伙伴系统数据结构的主要调整,是将空闲列表分解为`MIGRATE_TYPE`个列表: ```c struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; }; ``` ​ `nr_free`统计了所有列表上空闲页的数目,而每种迁移类型都对应于一个空闲列表。宏`for_each_migratetype_order(order, type)`可用于迭代指定迁移类型的所有分配阶。 如果内核无法满足针对某一给定迁移类型的分配请求,会怎么样?此前已经出现过一个类似的问题,即特定的`NUMA`内存域无法满足分配请求时。内核在这种情况下的做法是类似的,提供了一个备用列表,规定了在指定列表中无法满足分配请求时,接下来应使用哪一种迁移类型: ```c <mm/page_alloc.c> /* * This array describes the order lists are fallen back to when * the free lists for the desirable migrate type are depleted * * The other migratetypes do not have fallbacks. */ static int fallbacks[MIGRATE_PCPTYPES][MIGRATE_PCPTYPES - 1] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE }, }; ``` 该数据结构大体上是自明的:在内核想要分配不可移动页时,如果对应链表为空,则后退到可回收页链表,接下来到可移动页链表。
✅ `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参数 - 优化应用使用文件缓存的方式 这个输出对于诊断内存相关性能问题非常有用,特别是在内存不足或交换频繁的情况下。
This post is licensed under CC BY 4.0 by the author.