[linux内存管理] 第014篇 /proc/zoneinfo的详细解析

0. 前言

内存管理是一个相对复杂的内核模块,错综复杂的 数据结构 和管理逻辑。 Linux 内核为了帮助开发者从宏观上把握内存的使用情况,在几大核心数据结构中都有相应的计数统计,如物理页面使用情况、伙伴系统分配情况、内存管理区的页面使用情况、内存回收扫描回收情况、内存规整触发情况等等。

start_kernel() 中,会通过 setup_arch()build_all_zonelists()page_alloc_init()mm_init() 等函数对内存相关模块进行初始化,最后会通过 arch_call_rest_init() 对 Linux 系统做剩余的初始化工作,此函数中会调用 rest_init(),并在此调用 kernel_init() 启动init 线程,这里创建了本文将要剖析的虚拟文件节点 /proc/zoneinfo

1. zoneinfo_show

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
static int zoneinfo_show(struct seq_file *m, void *arg)
{
pg_data_t *pgdat = (pg_data_t *)arg;
walk_zones_in_node(m, pgdat, false, false, zoneinfo_show_print);
return 0;
}

static void walk_zones_in_node(struct seq_file *m, pg_data_t *pgdat,
bool assert_populated, bool nolock,
void (*print)(struct seq_file *m, pg_data_t *, struct zone *))
{
struct zone *zone;
struct zone *node_zones = pgdat->node_zones;
unsigned long flags;

// 遍历每一个node节点
for (zone = node_zones; zone - node_zones < MAX_NR_ZONES; ++zone) {
if (assert_populated && !populated_zone(zone))
continue;

if (!nolock)
spin_lock_irqsave(&zone->lock, flags);
//输出每个节点的zone信息
print(m, pgdat, zone);
if (!nolock)
spin_unlock_irqrestore(&zone->lock, flags);
}
}

调用 zoneinfo_show() 时,会通过函数 walk_zones_in_node() 对每个zone 进行遍历,从第 0 个zone 开始,当参数 zone 为第 0 个 zone 且该zone 的 present_pages 不为0 时, is_zone_first_populated () 返回为 true,则会将该 node 的page 信息打印出来。

2. zoneinfo_show_print

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
static void zoneinfo_show_print(struct seq_file *m, pg_data_t *pgdat,
struct zone *zone)
{
int i;
seq_printf(m, "Node %d, zone %8s", pgdat->node_id, zone->name);
if (is_zone_first_populated(pgdat, zone)) {
seq_printf(m, "\n per-node stats");
for (i = 0; i < NR_VM_NODE_STAT_ITEMS; i++) {
unsigned long pages = node_page_state_pages(pgdat, i);

if (vmstat_item_print_in_thp(i))
pages /= HPAGE_PMD_NR;
seq_printf(m, "\n %-12s %lu", node_stat_name(i),
pages);
}
}
seq_printf(m,
"\n pages free %lu"
"\n min %lu"
"\n low %lu"
"\n high %lu"
"\n spanned %lu"
"\n present %lu"
"\n managed %lu"
"\n cma %lu",
zone_page_state(zone, NR_FREE_PAGES),
min_wmark_pages(zone),
low_wmark_pages(zone),
high_wmark_pages(zone),
zone->spanned_pages,
zone->present_pages,
zone_managed_pages(zone),
zone_cma_pages(zone));

seq_printf(m,
"\n protection: (%ld",
zone->lowmem_reserve[0]);
for (i = 1; i < ARRAY_SIZE(zone->lowmem_reserve); i++)
seq_printf(m, ", %ld", zone->lowmem_reserve[i]);
seq_putc(m, ')');

/* If unpopulated, no other information is useful */
if (!populated_zone(zone)) {
seq_putc(m, '\n');
return;
}

for (i = 0; i < NR_VM_ZONE_STAT_ITEMS; i++)
seq_printf(m, "\n %-12s %lu", zone_stat_name(i),
zone_page_state(zone, i));

#ifdef CONFIG_NUMA
for (i = 0; i < NR_VM_NUMA_EVENT_ITEMS; i++)
seq_printf(m, "\n %-12s %lu", numa_stat_name(i),
zone_numa_event_state(zone, i));
#endif

seq_printf(m, "\n pagesets");
for_each_online_cpu(i) {
struct per_cpu_pages *pcp;
struct per_cpu_zonestat __maybe_unused *pzstats;

pcp = per_cpu_ptr(zone->per_cpu_pageset, i);
seq_printf(m,
"\n cpu: %i"
"\n count: %i"
"\n high: %i"
"\n batch: %i",
i,
pcp->count,
pcp->high,
pcp->batch);
#ifdef CONFIG_SMP
pzstats = per_cpu_ptr(zone->per_cpu_zonestats, i);
seq_printf(m, "\n vm stats threshold: %d",
pzstats->stat_threshold);
#endif
}
seq_printf(m,
"\n node_unreclaimable: %u"
"\n start_pfn: %lu",
pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES,
zone->zone_start_pfn);
seq_putc(m, '\n');
}

该函数大致分下面几个部分剖析:

  • 第一次调用时,输出当前 node 的内存统计信息,记录在 vm_node_stat 数组中,见 2.1 节;
  • 输出 zone 的总信息,见 2.2 节;
  • 输出 zone 的详细页面信息,记录在 zone->vm_stat 数组中,见 2.3 节;
  • 输出 zone 的 pageset 信息,即zone 在每个 CPU 内存分配器信息,记录在 zone->pageset 链表中,见 2.4 节;
  • 输出 zone 回收的 kswapd_failures 是否大于 MAX_RECLAIM_RETRIES(16) ,见 2.5节;
  • 输出 zone 的start_pfn,见 2.5 节;

2.1 输出当前node的内存统计信息

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
Node 0, zone   Normal
per-node stats
nr_inactive_anon 122141
nr_active_anon 58839
nr_inactive_file 189638
nr_active_file 208381
nr_unevictable 26959
nr_slab_reclaimable 23751
nr_slab_unreclaimable 66678
nr_isolated_anon 0
nr_isolated_file 0
workingset_nodes 9493
workingset_refault_anon 233015
workingset_refault_file 737280
workingset_activate_anon 65319
workingset_activate_file 283723
workingset_restore_anon 20181
workingset_restore_file 92763
workingset_nodereclaim 3456
nr_anon_pages 167321
nr_mapped 113653
nr_file_pages 467931
nr_dirty 12
nr_writeback 0
nr_writeback_temp 0
nr_shmem 12442
nr_shmem_hugepages 0
nr_shmem_pmdmapped 0
nr_file_hugepages 0
nr_file_pmdmapped 0
nr_anon_transparent_hugepages 0
nr_vmscan_write 398073
nr_vmscan_immediate_reclaim 8683
nr_dirtied 701230
nr_written 1083923
nr_kernel_misc_reclaimable 2752
nr_foll_pin_acquired 0
nr_foll_pin_released 0
nr_kernel_stack 63056
nr_shadow_call_stack 15792

参数解释:

参数名称 含义
nr_inactive_anon 非活跃的匿名页面数(以页为单位)。匿名页面通常与进程的私有内存相关,这些页面可能成为回收目标。
nr_active_anon 活跃的匿名页面数(以页为单位)。活跃页面通常在频繁使用,优先保留,不会立即回收。
nr_inactive_file 非活跃的文件缓存页面数(以页为单位)。这些页面存储文件数据但使用频率低,可能成为回收目标。
nr_active_file 活跃的文件缓存页面数(以页为单位)。这些页面存储文件数据且使用频率高,不容易被回收。
nr_unevictable 无法驱逐的页面数,通常被锁定(如通过 mlock)。这些页面不能被回收。
nr_slab_reclaimable 可回收的 slab 缓存页面数(以页为单位)。这些缓存可以在内存不足时被释放。
nr_slab_unreclaimable 不可回收的 slab 缓存页面数(以页为单位)。这些缓存用于内核关键结构,不能被释放。
nr_isolated_anon 正在隔离的匿名页面数(以页为单位)。用于内存回收时暂时从匿名页面中隔离的页面数量。
nr_isolated_file 正在隔离的文件缓存页面数(以页为单位)。用于内存回收时暂时从文件缓存中隔离的页面数量。
workingset_nodes 工作集的节点数,表示访问过的内存节点数量,用于评估内存活跃性。
workingset_refault_anon 匿名页面在工作集中重新加载的次数。表示曾经被回收的匿名页面再次被访问的频率。
workingset_refault_file 文件缓存页面在工作集中重新加载的次数。表示曾经被回收的文件缓存页面再次被访问的频率。
workingset_activate_anon 被激活的匿名页面数,表明非活跃匿名页面被重新标记为活跃页面的次数。
workingset_activate_file 被激活的文件缓存页面数,表明非活跃文件页面被重新标记为活跃页面的次数。
workingset_restore_anon 恢复的匿名页面数,表示曾被回收的匿名页面被重新使用的次数。
workingset_restore_file 恢复的文件缓存页面数,表示曾被回收的文件页面被重新使用的次数。
workingset_nodereclaim 节点回收失败的次数,表示由于 NUMA 策略或内存不足而无法完成页面迁移的情况。
nr_anon_pages 匿名页面的总数,包含活跃和非活跃的匿名页面。
nr_mapped 映射到用户空间的页面数,通常是由 mmap 映射的页面。
nr_file_pages 文件缓存页面的总数,包含活跃和非活跃的文件页面。
nr_dirty 脏页面的数量,这些页面修改过但尚未写回磁盘。
nr_writeback 正在写回的页面数,这些页面正从内存同步到磁盘。
nr_writeback_temp 临时写回页面数,通常用于短期写回操作。
nr_shmem 共享内存页面数,用于进程间共享的匿名内存。
nr_shmem_hugepages 共享内存中使用的大页数量(以页为单位)。
nr_shmem_pmdmapped 共享内存中通过 PMD 映射的大页数量(以页为单位)。
nr_file_hugepages 文件缓存中使用的大页数量(以页为单位)。
nr_file_pmdmapped 文件缓存中通过 PMD 映射的大页数量(以页为单位)。
nr_anon_transparent_hugepages 匿名内存中透明大页的数量。
nr_vmscan_write 内存扫描期间写回的页面数,通常在内存回收时触发。
nr_vmscan_immediate_reclaim 内存扫描期间立即回收的页面数。
nr_dirtied 被标记为脏的页面总数(历史累计值)。
nr_written 写回磁盘的页面总数(历史累计值)。
nr_kernel_misc_reclaimable 内核中其他可回收的页面数。
nr_foll_pin_acquired 通过 get_user_pages() 获取的页面数。
nr_foll_pin_released 通过 put_page() 释放的页面数。
nr_kernel_stack 用于内核堆栈的页面数。
nr_shadow_call_stack 用于影子调用堆栈的页面数(通常与硬件支持的功能相关)。

2.2 输出zone的总信息

1
2
3
4
5
6
7
8
9
pages free     30498
min 2048
low 6047
high 6559
spanned 8381632
present 930287
managed 894377
cma 38912
protection: (0, 0, 0)

参数解释:

字段名称 含义
pages free 30498 当前分区中空闲的页面数(以页为单位)。表示可以直接分配的内存大小,30498页约为30498×4KB = 119.1MB。
min 2048 区域的最低水位线(以页为单位)。当pages free低于此值时,系统会触发紧急内存回收。
low 6047 区域的低水位线(以页为单位)。当pages free低于此值时,系统会优先触发后台内存回收。
high 6559 区域的高水位线(以页为单位)。当pages free高于此值时,系统会停止内存回收。
spanned 8381632 当前区域所覆盖的页面数(以页为单位),包括物理内存和可能存在的地址空洞。8381632 页约为8381632×4KB= 33,526,528KB = 31.9GB。
present 930287 此分区中实际存在的物理页面数(以页为单位),即物理内存中属于该分区的页面总数。930287 页约为930287×4KB=3,721,148KB =3.55GB。
managed 894377 此分区中受内核管理的页面数(以页为单位),即可供分配或回收的内存。894377 页约为894377×4KB=3,577,508 KB=3.41GB。
cma 38912 为连续内存分配器(CMA)预留的页面数(以页为单位)。38912 页约为38912×4KB=155,648KB=152MB。
protection (0, 0, 0) 保护值数组,用于描述区域间的内存保护机制。此处值为 (0, 0, 0),表明没有额外的内存保护设置。

2.3 输出 zone 的详细页面信息

1
2
3
4
5
6
7
8
9
10
11
12
nr_free_pages 30498
nr_zone_inactive_anon 122141
nr_zone_active_anon 58839
nr_zone_inactive_file 189638
nr_zone_active_file 208381
nr_zone_unevictable 26959
nr_zone_write_pending 12
nr_mlock 16525
nr_page_table_pages 22903
nr_bounce 0
nr_zspages 53155
nr_free_cma 0
参数名称 含义
nr_free_pages 30498 当前区域中空闲的页面数,表示可以直接分配的页面数量。30498 页约为30498×4KB=121,992KB=119.1MB。
nr_zone_inactive_anon 122141 区域内非活跃的匿名页面数(以页为单位)。匿名页面通常与进程的私有内存相关,这些页面可能会被回收。
nr_zone_active_anon 58839 区域内活跃的匿名页面数(以页为单位)。活跃的匿名页面通常在使用中,不容易被回收。
nr_zone_inactive_file 189638 区域内非活跃的文件缓存页面数(以页为单位)。这些页面存储文件数据但使用频率低,可能成为回收目标。
nr_zone_active_file 208381 区域内活跃的文件缓存页面数(以页为单位)。这些页面存储文件数据且使用频率高,不容易被回收。
nr_zone_unevictable 26959 区域内无法回收的页面数(以页为单位)。通常是通过mlock锁定的内存或某些内核专用页面。
nr_zone_write_pending 12 区域内待写回的页面数(以页为单位)。这些页面已被标记为脏页,等待写回磁盘。
nr_mlock 16525 mlock系统调用锁定的页面数(以页为单位)。这些页面被固定在内存中,防止被回收。
nr_page_table_pages 22903 用于存储页表的页面数(以页为单位)。页表记录了虚拟地址和物理地址的映射关系,通常随进程数量增加而增加。
nr_bounce 0 用于 I/O 操作的中转页面数(以页为单位)。这些页面用于支持不直接访问高地址的 DMA 设备,此处为 0 表明未使用中转页面。
nr_zspages 53155 压缩页的总数(以页为单位)。压缩页是zswapzram等机制压缩后的页面数,能够节省内存空间。
nr_free_cma 0 连续内存分配器(CMA)中空闲的页面数(以页为单位)。此处为 0 表示预留的 CMA 内存已经全部分配或尚未启用。

2.4 输出 zone 的 pageset 信息

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
pagesets
cpu: 0
count: 87
high: 378
batch: 63
vm stats threshold: 48
cpu: 1
count: 112
high: 378
batch: 63
vm stats threshold: 48
cpu: 2
count: 355
high: 378
batch: 63
vm stats threshold: 48
cpu: 3
count: 49
high: 378
batch: 63
vm stats threshold: 48
cpu: 4
count: 0
high: 378
batch: 63
vm stats threshold: 48
cpu: 5
count: 24
high: 378
batch: 63
vm stats threshold: 48
cpu: 6
count: 353
high: 378
batch: 63
vm stats threshold: 48
cpu: 7
count: 54
high: 378
batch: 63
vm stats threshold: 48

参数解释:

参数名称 含义
cpu 表示具体的 CPU 编号,pagesets为每个 CPU 维护独立的页面分配状态。
count 表示当前 CPU 页面分配缓存中可用的页面数(以页为单位)。用于快速分配页面,减少全局锁的开销。
high 页面缓存的高水位线,超过该值时,内核会将多余的页面返还到全局空闲页面池。
batch 页面批量分配的大小,页面缓存中每次从全局池中获取或释放的页面数。
vm stats threshold 表示更新虚拟内存统计数据的阈值,当页面的变化量超过该值时,会触发统计更新(优化性能)。

2.5 其余信息

1
2
node_unreclaimable:  0
start_pfn: 525888

参数解释:

参数名称 含义
node_unreclaimable 0 当前节点是否不可回收的标志位。值为 0 表示当前节点中的页面可以被回收,没有被标记为不可回收。
start_pfn 525888 当前节点的起始页帧号(PFN, Page Frame Number)。用于指示该节点的物理内存起始地址,单位为页(4KB)。