在bootmem_init初始化的时候,已经初始化了内存节点的zone成员,该成员是struct zone数组,存放该内存节点的zone信息。在linux的内存管理中,分几个阶段进行抽象,用数据结构来管理。先用结点集合管理内存,然后用zone管理结点,再用页的管理zone。此时使用的数据结构分别为pglist_data、zone、page结构体,本章的主要是来分析内核是如何完成zonelist的初始化。
1. 数据结构
在结点的pglist_data数据结构中有一个node_zone_list[]类型的struct zonelist
typedef struct pglist_data {
...
struct zonelist node_zonelists[MAX_ZONELISTS];
...
}pg_data_t;
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
enum {
ZONELIST_FALLBACK, /* zonelist with fallback */
#ifdef CONFIG_NUMA
ZONELIST_NOFALLBACK, /* zonelist without fallback (__GFP_THISNODE) */
#endif
MAX_ZONELISTS
}
#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
- node_zonelists []包含了2个zonelist,一个是由本node的zones组成,另一个是由从本node分配不到内存时可选的备用zones组成,相当于是选择了一个退路,所以叫fallback。而对于本开发板,没有定义NUMA,没有备份。
- struct zonelist只有一个_zonerefs[]数组构成,_zonerefs[]数组的大小为MAX_ZONES_PER_ZONELIST,最大的节点数和节点可拥有的ZONE数w为1 *MAX_NR_ZONES
- _zonerefs[]数组的类型struct zoneref定义如下,主要是zone指针和索引号构成。
struct zoneref {
struct zone *zone; /* Pointer to actual zone */
int zone_idx; /* zone_idx(zoneref->zone) */
};
2. zonelist初始化
内核在start_kernel中通过build_all_zonelists完成了内存结点及其管理内存域的初始化工作, 调用如下
void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone)
{
set_zonelist_order(); ----------------(1)
if (system_state == SYSTEM_BOOTING) { ----------------(2)
build_all_zonelists_init();
} else {
#ifdef CONFIG_MEMORY_HOTPLUG
if (zone)
setup_zone_pageset(zone);
#endif
/* we have to stop all cpus to guarantee there is no user
of zonelist */
stop_machine(__build_all_zonelists, pgdat, NULL);
/* cpuset refresh routine should be here */
}
vm_total_pages = nr_free_pagecache_pages(); ----------------(3)
/*
* 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)) ----------------(4)
page_group_by_mobility_disabled = 1;
else
page_group_by_mobility_disabled = 0;
----------------(5)
pr_info("Built %i zonelists in %s order, mobility grouping %s. Total pages: %ld\n",
nr_online_nodes,
zonelist_order_name[current_zonelist_order],
page_group_by_mobility_disabled ? "off" : "on",
vm_total_pages);
#ifdef CONFIG_NUMA
pr_info("Policy zone: %s\n", zone_names[policy_zone]);
#endif
}
- 1.调用set_zonelist_order函数决定zone排列方式;按照相同区域排列,还是以节点为基准排列
- 2.不同的系统状态调用的函数不同,系统状态为启动阶段时(SYSTEM_BOOTING)时,就调用build_all_zonelists_init函数,其他状态就调用stop_machine函数。让系统的所有CPU执行停止函数。其系统状态可分为6中,其定义如下
extern enum system_states {
SYSTEM_BOOTING,
SYSTEM_RUNNING,
SYSTEM_HALT,
SYSTEM_POWER_OFF,
SYSTEM_RESTART,
} system_state;
-
3.调用nr_free_pagecache_pages,从函数名字可以看出,该函数求出可处理的空页数
-
4.通过nr_free_pagecache_pages求出vm_total_pages和页移动性比较,决定是否激活grouping
-
5.打印到控制台,打印的信息输出内容为online node、zone列表顺序,是否根据移动性对页面执行集合(grouping)、vm_total_pages、NUMA时输出policy zone
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KWlvYpMv-1592019440924)(D:\学习总结\内存管理单元\image-20200606225539321.png)]
2.1. set_zonelist_order
set_zonelist_order函数决定用节点顺序构建还是用zone顺序构建,其定义如下
static void set_zonelist_order(void)
{
if (user_zonelist_order == ZONELIST_ORDER_DEFAULT)
current_zonelist_order = default_zonelist_order();
else
current_zonelist_order = user_zonelist_order;
}
该函数检查user\_zonelist\_order是否为ZONELIST\_ORDER\_DEFAULT,user\_zonelist\_order具有以下3个值之一
#define ZONELIST_ORDER_DEFAULT 0
#define ZONELIST_ORDER_NODE 1
#define ZONELIST_ORDER_ZONE 2
如果是user\_zonelist\_order,就调用default\_zonelist\_order,决定将zonelist顺序作为节点顺序还是zone顺序,否则用user\_zonelist\_order,如果是32位系统,其为ZONELIST\_ORDER\_ZONE,而如果是64位系统,则为ZONELIST\_ORDER\_NODE。即为zone优先还是节点优先。
假设节点0的zone类型由ZONE\_NORMAL和ZONE\_DMA构成,节点1由ZONE\_NORMAL构成,可将节点0的zonelist按下面构建
* 类型A,Node(0)ZONE\_NORMAL->Node(0)ZONE\_DMA->Node(1)ZONE\_NORMAL
* 类型B,Node(0)ZONE\_NORMAL->Node(1)ZONE\_NORMAL->Node(0)ZONE\_DMA
对于类型A,如果节点0的ZONE\_NORMAL中无法分配内存,就从节点0的ZONE\_DMA开始分配内存,但由于一般ZONE\_DMA区域比较小,就会发生ZONE\_DMA的OOM(Out of memory)问题
对于类型B,如果节点的ZONE\_NORMAL中无法分配内存,就从节点1的ZONE\_NORMAL开始分配。
因此对于类型A为节点优先,类型B为zone顺序优先。
## 2.2 build\_all\_zonelists\_init ##
构建备用列表的主要工作是在\_\_build\_all\_zonelists函数中实现的,其主要是遍历每一个节点,然后调用build\_zonelists
static int __build_all_zonelists(void *data)
{
int nid;
int cpu;
pg_data_t *self = data;
#ifdef CONFIG_NUMA
memset(node_load, 0, sizeof(node_load));
#endif
if (self && !node_online(self->node_id)) {
build_zonelists(self);
}
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid);
build_zonelists(pgdat);
}
for_each_possible_cpu(cpu) {
setup_pageset(&per_cpu(boot_pageset, cpu), 0);
#ifdef CONFIG_HAVE_MEMORYLESS_NODES
if (cpu_online(cpu))
set_cpu_numa_mem(cpu, local_memory_node(cpu_to_node(cpu)));
#endif
}
return 0;
}
其主要是来分析下build\_zonelists的流程
static void build_zonelists(pg_data_t *pgdat)
{
int i, node, load;
nodemask_t used_mask;
int local_node, prev_node;
struct zonelist *zonelist;
unsigned int order = current_zonelist_order;
/* initialize zonelists */
for (i = 0; i < MAX_ZONELISTS; i++) { ------------(1)
zonelist = pgdat->node_zonelists + i;
zonelist->_zonerefs[0].zone = NULL;
zonelist->_zonerefs[0].zone_idx = 0;
}
/* NUMA-aware ordering of nodes */
local_node = pgdat->node_id;
load = nr_online_nodes;
prev_node = local_node;
nodes_clear(used_mask);
memset(node_order, 0, sizeof(node_order));
i = 0;
while ((node = find_next_best_node(local_node, &used_mask)) >= 0) { ------------(2)
/*
* We don't want to pressure a particular node.
* So adding penalty to the first node in same
* distance group to make it round-robin.
*/
if (node_distance(local_node, node) !=
node_distance(local_node, prev_node))
node_load[node] = load;
prev_node = node;
load--;
if (order == ZONELIST_ORDER_NODE) ------------(3)
build_zonelists_in_node_order(pgdat, node);
else
node_order[i++] = node; /* remember order */
}
if (order == ZONELIST_ORDER_ZONE) { ------------(4)
/* calculate node order -- i.e., DMA last! */
build_zonelists_in_zone_order(pgdat, i);
}
build_thisnode_zonelists(pgdat); ------------(5)
}
1. 从当前节点的节点描述符pgdat访问struct zonelist结构体类型node\_zonelists,并初始化成员变量zone和zone\_idx。
2. 调用find\_nex\_best\_node,该函数为添加当前节点的备份列表,以当前节点为基准查找最佳节点。
3. 由while循环查找当前节点的最佳节点的节点号,因此,如果zone列表顺序为节点顺序,就调用build\_zonelists\_in\_node\_order函数,以节点顺序构建备份列表,如果是zone顺序,则调用node\_order\[\]数组保持节点顺序。
4. 如果利用node\_order\[\]数组保持的节点顺序就调用build\_zonelists\_in\_zone\_order,用zone顺序构建备份列表
5. 最后调用build\_thisnode\_zonelists,在node\_zonelists\[\]和\_zonerefs\[\]数组中构建相应节点的zone列表
static void build_zonelists_in_node_order(pg_data_t *pgdat, int node)
{
int j;
struct zonelist *zonelist;
zonelist = &pgdat->node_zonelists[ZONELIST_FALLBACK];
for (j = 0; zonelist->_zonerefs[j].zone != NULL; j++)
;
j = build_zonelists_node(NODE_DATA(node), zonelist, j);
zonelist->_zonerefs[j].zone = NULL;
zonelist->_zonerefs[j].zone_idx = 0;
}
该函数以节点为单位构建备份列表,各节点的zone按顺序构建,具有这些zone的列表的数组就是zonelist的\_zonerefs成员变量。\_
* 首先通过node\_zonelists找到对应的zonelist,然后通过for循环线找到\_zonerefs的成员中zone为非NULL得索引j后,将相应节点的zone从\_\_zonerefs\[j\]开始添加到数组即可。
* 然后调用build\_zonelists\_node将相应的节点的zone添加到\_zonerefs\[\]数组,然后初始化zonelist->\_zonerefs\[j\]的zone和zone\_idx,以添加下一个节点zone。这样,就可以为备份列表添加下一个最佳节点的zone。
static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist,
int nr_zones)
{
struct zone *zone;
enum zone_type zone_type = MAX_NR_ZONES;
do {
zone_type--; ------------(1)
zone = pgdat->node_zones + zone_type;
if (managed_zone(zone)) { ------------(2)
zoneref_set_zone(zone,
&zonelist->_zonerefs[nr_zones++]);
check_highest_zone(zone_type);
}
} while (zone_type);
return nr_zones;
}
* 将节点的zone注册到备份列表时,zone的类型是按照逆时针注册的。即HIGHMEM->NORMAL->DMA32->DMA的顺序。也就是说HIGHMEM中没有内存,就从NORMAL开始分配;如果NORMAL没有内存,就从DMA开始分配。这样为了减小分配内存时候发生的OOM风险,最大降低对系统的影响
* 当在相应的zone中有实际的物理内存时就将zone注册到\_zonerefs\[\]数组
## 2.3 输出备用列表信息 ##
下面分析mminit\_verify\_zonelist函数
void __init mminit_verify_zonelist(void)
{
int nid;
if (mminit_loglevel < MMINIT_VERIFY)
return;
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid);
struct zone *zone;
struct zoneref *z;
struct zonelist *zonelist;
int i, listid, zoneid;
BUG_ON(MAX_ZONELISTS > 2);
for (i = 0; i < MAX_ZONELISTS * MAX_NR_ZONES; i++) {
/* Identify the zone and nodelist */
zoneid = i % MAX_NR_ZONES;
listid = i / MAX_NR_ZONES;
zonelist = &pgdat->node_zonelists[listid];
zone = &pgdat->node_zones[zoneid];
if (!populated_zone(zone))
continue;
/* Print information about the zonelist */
printk(KERN_DEBUG "mminit::zonelist %s %d:%s = ",
listid > 0 ? "thisnode" : "general", nid,
zone->name);
/* Iterate the zonelist */
for_each_zone_zonelist(zone, z, zonelist, zoneid) {
#ifdef CONFIG_NUMA
pr_cont("%d:%s ", zone->node, zone->name);
#else
pr_cont("0:%s ", zone->name);
#endif /* CONFIG_NUMA */
}
pr_cont("\n");
}
}
}
该函数,对各个节点进行遍历,对各个节点具有的最大ZONE数,输出zonelist的信息,对各个zonelist输出zone名称。该函数输出系统内所有节点的备份列表信息,只是执行for循环访问节点的备份列表,输出构建备份列表的zone节点号和节点名。
## 2.4 处理页分配请求节点 ##
cpuset\_init\_current\_mems\_allowed函数只调用nodes\_setall函数,在当前任务current的mems\_allowed位图中,将系统的所有节点设置为1。mems\_allowed位图决定处理当前任务中发生的页分配请求的节点。
void __init cpuset_init_current_mems_allowed(void)
{
nodes_setall(current->mems_allowed);
}
## 2.5 求空页数 ##
将gfp\_zone(GFP\_HIGHUSER\_MOVABLE)的结果值作为参数传递,gfp\_zone函数对传递来的参数标签值进行检查并返回zone类型,并返回zone类型中的可用页数。
unsigned long nr_free_pagecache_pages(void)
{
return nr_free_zone_pages(gfp_zone(GFP_HIGHUSER_MOVABLE));
}
下面来看看nr\_free\_zone\_pages函数
static unsigned long nr_free_zone_pages(int offset)
{
struct zoneref *z;
struct zone *zone;
/* Just pick one node, since fallback list is circular */
unsigned long sum = 0;
struct zonelist *zonelist = node_zonelist(numa_node_id(), GFP_KERNEL);
for_each_zone_zonelist(zone, z, zonelist, offset) {
unsigned long size = zone->managed_pages;
unsigned long high = high_wmark_pages(zone);
if (size > high)
sum += size - high;
}
return sum;
}
该函数主要是对zonelist执行循环,访问zonelist的所有zone,在sum中累积从zone->present\_pages减掉zone->pages\_high的值。zone->present\_pages是相应的zone中的物理页数,zone->pages\_high变量用于决定相应zone是否为Idle状态。
* 若存在比page\_high更多的空页,则当前zone变成idle状态。
* 可用内存不足时,内核将虚拟内存的页面会置换到硬盘,前面提到的struct zone结构体中的min、high、low会用到。
该函数主要是用于求出可处理的空页数。
## 2.6 页移动性 ##
if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
page_group_by_mobility_disabled = 1;
else
page_group_by_mobility_disabled = 0;
通过前面的函数求出vm\_total\_pages,若比(pageblock\_nr\_pages \* MIGRATE\_TYPES)小,就不允许以移动性为基准执行。
pageblock\_nr\_pages和MIGRATE\_TYPES定义如下
#define MAX_ORDER 11
#define pageblock_order (MAX_ORDER-1)
#define pageblock_nr_pages (1UL << pageblock_order)
MIGRATE\_TYPES表示移动类型的宏,其值为5,其定义为
enum {
MIGRATE_UNMOVABLE, //不可以动
MIGRATE_MOVABLE, //可回收
MIGRATE_RECLAIMABLE, //可移动
MIGRATE_PCPTYPES,
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
MIGRATE_TYPES
}
通过以上方式,最终构建了free_list以移动性为基准执行的页集合,其主要有以下好处
-
防止内存碎片:以顺序为单位对具有相同移动属性的页执行集合,防止内存碎片
-
分配大内存的方法:将具有相同移动属性的页集合在一处,使其能够在大内存分配中使用,例如
- 内核内存(kmalloc): UNMOVABLE
- 磁盘缓存(inode、dentry):RECLAIMABLE
- 用户内存+页缓存:MOVABLE
3 总结
build_all_zonelists()用来初始化内存分配器使用的存储节点中的管理区链表,是为内存管理算法(伙伴管理算法)做准备工作的,对Linux管理的各内存结构体进行初始化和设置操作,如下图所示
4. 参考资料
ARM Linux内核源码剖析
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。