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

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

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的内存统计信息

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的总信息

pages free     30498
        min      2048
        low      6047
        high     6559
        spanned  8381632
        present  930287
        managed  894377
        cma      38912
        protection: (0, 0, 0)

参数解释:

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

2.3 输出 zone 的详细页面信息

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_pages30498当前区域中空闲的页面数,表示可以直接分配的页面数量。30498 页约为30498×4KB=121,992KB=119.1MB。
nr_zone_inactive_anon122141区域内非活跃的匿名页面数(以页为单位)。匿名页面通常与进程的私有内存相关,这些页面可能会被回收。
nr_zone_active_anon58839区域内活跃的匿名页面数(以页为单位)。活跃的匿名页面通常在使用中,不容易被回收。
nr_zone_inactive_file189638区域内非活跃的文件缓存页面数(以页为单位)。这些页面存储文件数据但使用频率低,可能成为回收目标。
nr_zone_active_file208381区域内活跃的文件缓存页面数(以页为单位)。这些页面存储文件数据且使用频率高,不容易被回收。
nr_zone_unevictable26959区域内无法回收的页面数(以页为单位)。通常是通过mlock锁定的内存或某些内核专用页面。
nr_zone_write_pending12区域内待写回的页面数(以页为单位)。这些页面已被标记为脏页,等待写回磁盘。
nr_mlock16525mlock系统调用锁定的页面数(以页为单位)。这些页面被固定在内存中,防止被回收。
nr_page_table_pages22903用于存储页表的页面数(以页为单位)。页表记录了虚拟地址和物理地址的映射关系,通常随进程数量增加而增加。
nr_bounce0用于 I/O 操作的中转页面数(以页为单位)。这些页面用于支持不直接访问高地址的 DMA 设备,此处为 0 表明未使用中转页面。
nr_zspages53155压缩页的总数(以页为单位)。压缩页是zswapzram等机制压缩后的页面数,能够节省内存空间。
nr_free_cma0连续内存分配器(CMA)中空闲的页面数(以页为单位)。此处为 0 表示预留的 CMA 内存已经全部分配或尚未启用。

2.4 输出 zone 的 pageset 信息

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 其余信息

node_unreclaimable:  0
start_pfn:           525888

参数解释:

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