3.内存初始化整体大概流程(建立结点及内存域备用列表)
由于大部分系统都只有一个内存结点,下文只考察此类系统。具体是什么样的情况呢?为确保内存管理代码是可移植的(因此它可以同样用于UMA和NUMA系统),内核在mm/page_alloc.c中定义
了一个pg_data_t实例(称作contig_page_data)管理所有的系统内存。所有平台上都实现了特定于体系结构的NODE_DATA宏,用于通过结点编号,来查询与一个NUMA结点相关的pgdata_t实例。
<mmzone.h>
#define NODE_DATA(nid) (&contig_page_data)
系统启动
setup_arch是一个特定于体系结构的设置函数,其中一项任务是负责初始化自举分配器。
在SMP系统上,setup_per_cpu_areas初始化源代码中(使用per_cpu宏)定义的静态per-cpu变量,这种变量对系统中的每个CPU都有一个独立的副本。此类变量保存在内核二进制映像的一个独立的段中。setup_per_cpu_areas的目的是为系统的各个CPU分别创建一份这些数据的副本。在非SMP系统上该函数是一个空操作。详细见per-cpu变量
build_all_zonelists建立结点和内存域的数据结构(见下文)。
mem_init是另一个特定于体系结构的函数,用于停用bootmem分配器并迁移到实际的内存管理函数,稍后讨论。
kmem_cache_init初始化内核内部用于小块内存区的分配器。
setup_per_cpu_pageset从上文提到的struct zone,为pageset数组的第一个数组元素分配内存。分配第一个数组元素,换句话说,就是意味着为第一个系统处理器分配。系统的所有内存域都会考虑进来。该函数还负责设置冷热分配器的限制,并将在3.5.3节详细地讨论。请注意,在SMP系统上对应于其他CPU的pageset数组成员,将会在相应的CPU激活时初始化。
结点和内存域初始化
build_all_zonelists建立管理结点及其内存域所需的数据结构。有趣的是,该函数可以通过上文引入的宏和抽象机制实现,而不用考虑具体的NUMA或UMA系统。因为执行的函数实际上有两种形式,所以这样做是可能的:一种用于NUMA系统,而另一种用于UMA系统。由于内核经常使用这种小技巧,我会对此作简要讨论。假定需要根据编译时配置,以不同方式执行某一任务。一种可能的方法是,使用两个不同的函数,每次调用时,根据某些预处理器条件来选择正确的一个:
void do_something() { ... #ifdef CONFIG_WORK_HARD do_work_fast(); #else do_work_at_your_leisure(); #endif ... }
由于这需要在每次调用相应的函数时都使用预处理器,内核开发者认为这种方法代表了糟糕的风格。更优雅的一个方案是根据选择的不同配置,来定义函数自身:
#ifdef CONFIG_WORK_HARD void do_work() { /* 开始,快点! */ ... } #else void do_work() { /* 放松点,不要紧张 */ ... } #endif
build_all_zonelists中我们当前感兴趣的那部分(对于页分配器的页组可移动性扩展,实际上还有另外一些工作,我会在下文单独讨论)将所有工作都委托给__build_all_zonelists,后者又对系统中的各个NUMA结点分别调用build_zonelists。
该函数的任务是,在当前处理的结点和系统中其他结点的内存域之间建立一种等级次序。接下来,依据这种次序分配内存。如果在期望的结点内存域中,没有空闲内存,那么这种次序就很重要。我们考虑一个例子,其中内核想要分配高端内存。它首先企图在当前结点的高端内存域找到一个大小适当的空闲段。如果失败,则查看该结点的普通内存域。如果还失败,则试图在该结点的DMA内存域执行分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。在这种情况下,备选结点应该尽可能靠近主结点,以最小化由于访问非本地内存引起的性能损失。内核定义了内存的一个层次结构,首先试图分配“廉价的”内存。如果失败,则根据访问速度和容量,逐渐尝试分配“更昂贵的”内存。高端内存是最廉价的,因为内核没有任何部份依赖于从该内存域分配的内存。如果高端内存域用尽,对内核没有任何副作用,这也是优先分配高端内存的原因。普通内存域的情况有所不同。许多内核数据结构必须保存在该内存域,而不能放置到高端内存域。因此如果普通内存完全用尽,那么内核会面临紧急情况。所以只要高端内存域的内存没有用尽,都不会从普通内存域分配内存。
最昂贵的是DMA内存域,因为它用于外设和系统之间的数据传输。因此从该内存域分配内存是最后一招。
内核还针对当前内存结点的备选结点,定义了一个等级次序。这有助于在当前结点所有内存域的内存都用尽时,确定一个备选结点。
内核使用pg_data_t中的zonelist数组,来表示所描述的层次结构。
<mmzone.h> typedef struct pglist_data { ... struct zonelist node_zonelists[MAX_ZONELISTS]; //NUMA zonelists 是双倍的,因为我们需要 zonelists 来限制分配给 GFP_THISNODE 的单个节点。 ... } pg_data_t;
#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
struct zonelist {
...
struct zone *zones[MAX_ZONES_PER_ZONELIST + 1]; // NULL分隔
};
由于该备用列表必须包括所有结点的所有内存域,因此由MAX_NUMNODES * MAX_NZ_ZONES项组成,外加一个用于标记列表结束的空指针。

mm/page_alloc.c static int __init build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist,int nr_zones, enum zone_type zone_type) { struct zone *zone; do { zone = pgdat->node_zones + zone_type; if (populated_zone(zone)) { zonelist->zones[nr_zones++] = zone; } zone_type--; } while (zone_type >= 0); return nr_zones; }
(循环表明优先使用相应结点相对应的内存域,如果内存不够,则随后便是逐渐往相应结点“昂贵”内存域递进,最后才是其它结点)。
浙公网安备 33010602011771号