3. 结点和内存域初始化
build_all_zonelist建立管理结点及其内存域所需的数据结构。该函数可以通过上文引入的宏和抽象机制实现,而不用考虑NUMA或UMA系统。
build_all_zonelist基本上所有工作都在__build_all_zonelists()中完成。而__build_all_zonelists的核心工作又是各NUMA节点分别调用build_zonelists来实现。
//linux/mm/page_alloc.c static int __build_all_zonelists(void *data) { ... for_each_online_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); build_zonelists(pgdat); build_zonelist_cache(pgdat); } ... return 0; }
for_each_online_node遍历了系统中所有的活动结点。由于UMA系统只有一个结点, build_zonelists只调用了一次,就对所有的内存创建了内存域列表。NUMA系统调用该函数的次数等同于结点的数目。每次调用对一个不同结点生成内存域数据。
build_zonelists需要一个指向pg_data_t实例的指针作为参数:
static void build_zonelists(pg_data_t *pgdat);
其中包含了结点内存配置的所有现存信息,而新建的数据结构也会放置其中。
#define MAX_NUMNODES (1 << NODES_SHIFT)
pg_data_t node_data[MAX_NUMNODES];
#define NODE_DATA(nid) (&node_data[(nid)])
在UMA系统上,NODE_DATA会返回node_data的地址
build_zonelists的任务是,在当前处理的结点和系统中其他结点的内存域中建立一种等级次序。接下来依据这种等级次序分配内存。如果在期望的结点内存域中,没有空闲内存,那么这种次序就很重要。
例如,其中内核想要分配高端内存。首先企图在当前结点的高端内存域找到一个大小适当的空闲段。如果失败,则查看该结点的普通内存域。如果还失败,则试图在该结点的DMA内存域执行分配。如果在3个本地内存域都失败,则查看其他结点。在这种概况下,备选结点应该尽可能靠近主结点,以最小化由于访问非本地内存引起的性能损失。
内核定义了内存的一个层次结构,首先试图分配“廉价的”内存,如果失败,则根据访问速度和容量,逐渐尝试分配“更昂贵的”内存。
高端内存时最廉价的,因为内核没有任何部分依赖于从该内存域分配的内存。如果高端内存域用尽,对内核没有任何副作用,这也是优先分配高端内存的原因。
普通内存域的情况有所不同。许多内核数据结构必须保存在该内存域,而不能放置到高端内存域。因此如果普通内存完全用尽,那么内核会面临紧急情况。所以只要高端内存域的内存没有用尽,都不会从普通内存域分配内存。
最昂贵的是DMA内存域,因为它用于外设和系统之间的数据传输。因此从该内存域分配内存时最后一招。
内核还针对当前内存结点的备选结点,定义了一个等级次序。这有助于当前结点所有内存域的内存都用尽时,确定一个备选结点。
内核使用pg_data_t中的zonelist数组,来表示所描述的层次结构。
typedef struct pglist_data { struct zone node_zones[MAX_NR_ZONES]; struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones; } pg_data_t; /* Maximum number of zones on a zonelist */ #define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES) struct zonelist { struct zonelist_cache *zlcache_ptr; // NULL or &zlcache struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1]; };
node_zonelists数组对每种可能的内存域类型都配置了一个独立的数组项。数组项包含了类型为zonelist的一个备用列表。
由于该设备用列表必须包括所有结点的所有内存域,因此由 (MAX_NUMNODES * MAX_NR_ZONES)项组成,外加一个用于标记列表结束的空指针。
建立备用层次结构的任务委托给build_zonelists,该函数为每个NUMA结点都创建了相应的数据结构。它需要指向相关的pg_data_t实例的指针作为参数。为什么必须考虑多个NUMA结点?实际上,如果设置了CONFIG_NUMA,内核会使用不同的实现替换下列代码。但也有可能某个体系结构在UMA系统上选择不连续或稀疏内存选项。在地址空间包含较大空洞的情况下,这样做可能是有好处的。这样的空洞造成的内存“块”,最好通过NUMA提供的数据结构来处理。这也是为什么此处需要处理NUMA结点的原因。
一个大的外部循环首先迭代所有的结点内存域。每个循环在zonelist数组中找到第i个zonelist,对第i个内存域计算备用列表。
static void build_zonelists(pg_data_t *pgdat) { /* initialize zonelists */ for (i = 0; i < MAX_ZONELISTS; i++) { zonelist = pgdat->node_zonelists + i; zonelist->_zonerefs[0].zone = NULL; zonelist->_zonerefs[0].zone_idx = 0; } ... }
node_zonelists的数组元素通过指针操作寻址,这在C语言中是完全合法的管理。实际工作则委托给buile_zonelist_in_node_order。在调用时,它首先生成本地结点内分配内存时的备用次序。
/* * Build zonelists ordered by node and zones within node. * This results in maximum locality--normal zone overflows into local * DMA zone, if any--but risks exhausting DMA zone. */ static void build_zonelists_in_node_order(pg_data_t *pgdat, int node) { int j; struct zonelist *zonelist; zonelist = &pgdat->node_zonelists[0]; 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; } 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--; zone = pgdat->node_zones + zone_type; if (populated_zone(zone)) { zoneref_set_zone(zone, &zonelist->_zonerefs[nr_zones++]); check_highest_zone(zone_type); } } while (zone_type); return nr_zones; }
备用列表的各项是借助于zone_type参数排序的,该参数指定了最优先选择哪个内存域,这参数的初始值是外层循环的控制变量i。我们知道其值可能是ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA或ZONE_DMA32之一。nr_zones表示从备用列表中的哪个位置开始填充新项。由于列表中尙没有项,因此调用者传递了0.
内核在build_zonelists中按分配代价从昂贵到低廉的次序,迭代了结点中所有的内存域。而在build_zonelists_node中,则按照分配代价从低廉到昂贵的次序,迭代了分配代价不低于当前内存域的内存域。在build_zonelists_zone的每一步中,都对所选的内存域调用populated_zone,确认zone->present_pages大于0,即确认内存域中确实有页存在。倘若如此,则将指向zone实例的指针添加到zonelist->zones中的当前位置。后备列表的当前位置保存在nr_zone。
do { zone_type--; zone = pgdat->node_zones + zone_type; if (populated_zone(zone)) { zoneref_set_zone(zone, &zonelist->_zonerefs[nr_zones++]); check_highest_zone(zone_type); } } while (zone_type);
在每一步结束时候,都将内存域类型减一,即设置为一个更昂贵的内存域类型。
考虑一个系统,有内存域ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA。在第一次运行build_zonelists_node时,实际上会执行下列赋值:
Zonelist->zones[0] = ZONE_HIGHMEM
Zonelist->zones[1] = ZONE_NORMAL
Zonelist->zones[2] = ZONE_DMA
static void build_zonelists(pg_data_t *pgdat) { while ((node = find_next_best_node(local_node, &used_mask)) >= 0) { 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) build_zonelists_in_node_order(pgdat, node); else node_order[j++] = node; /* remember order */ } }
这里循环迭代大于当前结点编号的所有结点。新的项通过build_zonelists_in_node_order添加到备用列表。此时的j的作用就体现出来了。在本地结点的备用目标找到之后,该变量值改变。该值作为新项的起始地址。
static void build_zonelists_in_node_order(pg_data_t *pgdat, int node) { for (j = 0; zonelist->_zonerefs[j].zone != NULL; j++) ; j = build_zonelists_node(NODE_DATA(node), zonelist, j); ... }
这里对所有的编号小于当前结点的结点生成备用列表项。备用列表项中的数目一般无法准确知道,因为系统中不同结点的内存域配置可能并不相同。因此列表的最后一项赋值为空指针,显示标记列表结束。
对总数N个结点的结点m来说,内核生成备用列表时,选择备用结点的顺序总是:m、m+1、m+2、......、N-1、0、1、...m-1。这确保了不过度使用任何结点。
再梳理下这里的调用关系为:
start_kernel ->setup_arch ->setup_per_cpu_areas ->build_all_zonelists ->__build_all_zonelists ->build_zonelists ->build_zonelists_in_node_order ->build_zonelists_node ->mm_init ->mem_init ->setup_per_cpu_pageset