3.4.1 初始化内存管理(二):结点和内存域初始化

3. 结点和内存域初始化

build_all_zonelist建立管理结点及其内存域所需的数据结构。该函数可以通过上文引入的宏和抽象机制实现,而不用考虑NUMAUMA系统。

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数组中找到第izonelist,对第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_HIGHMEMZONE_NORMALZONE_DMAZONE_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_HIGHMEMZONE_NORMALZONE_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来说,内核生成备用列表时,选择备用结点的顺序总是:mm+1m+2......N-101...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


本文参考《深入Linux内核架构》及3.18.3版本内核


你可能感兴趣的:(3.4.1 初始化内存管理(二):结点和内存域初始化)