在介绍伙伴系统之前,我们先看一下伙伴系统所需要的数据结构的初始化流程。
伙伴系统使用的数据结构主要有每个内存节点的内存信息和每个内存区域的内存信息。

1. 内存节点

内存节点是内核管理物理内存的最上层抽象。对于 NUMA 系统,至少包含一个节点 0 。每个节点都有一个 struct pglist_data 对象指针,包含该节点的所有物理内存信息。

1.1. stuct pglist_data *node_data[]

x86架构下,变量 struct pglist_data *node_data[] 保存系统中的所有节点信息,定义在 arch/x86/mm/numa.c 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
typedef struct pglist_data {
    /* 节点包含的所有zone信息 */
    struct zone node_zones[MAX_NR_ZONES];
    struct zonelist node_zonelists[MAX_ZONELISTS];
    int nr_zones;
#ifdef CONFIG_MEMORY_HOTPLUG
    /*
     * Must be held any time you expect node_start_pfn, node_present_pages
     * or node_spanned_pages stay constant.  Holding this will also
     * guarantee that any pfn_valid() stays that way.
     *
     * pgdat_resize_lock() and pgdat_resize_unlock() are provided to
     * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG.
     *
     * Nests above zone->lock and zone->span_seqlock
     */
    spinlock_t node_size_lock;
#endif
    unsigned long node_start_pfn;
    unsigned long node_present_pages; /* total number of physical pages */
    unsigned long node_spanned_pages; /* total size of physical page
                         range, including holes */
    int node_id;
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd; /* Protected by
                       mem_hotplug_begin/end() */
    int kswapd_max_order;
    enum zone_type classzone_idx;
#ifdef CONFIG_NUMA_BALANCING
    /* Lock serializing the migrate rate limiting window */
    spinlock_t numabalancing_migrate_lock;

    /* Rate limiting time interval */
    unsigned long numabalancing_migrate_next_window;

    /* Number of pages migrated during the rate limiting time interval */
    unsigned long numabalancing_migrate_nr_pages;
#endif
} pg_data_t;

1.2. node_data 的初始化

arch/x86/include/asm/mmzone_64.h 中定义了访问 node_data 变量的宏 #define NODE_DATA(nid) (node_data[nid]) ,内核中的代码大多使用这个宏访问 node_data 变量。

1.2.1. setup_node_data

函数 setup_node_data 负责分配 node_data 变量,进行一些成员的简单初始化,函数的调用路径如下:

  • start_kernel ( init/main.c )
    • setup_arch ( arch/x86/kernel/setup.c )
      • initmem_init ( arch/x86/mm/numa_64.c )
        • x86_numa_init ( arch/x86/mm/numa.c )
          • numa_init ( arch/x86/mm/numa.c )
            • x86_acpi_numa_init ( arch/x86/mm/srat.c )
              • acpi_numa_init ( drivers/acpi/numa.c )
            • numa_register_memblks ( arch/x86/mm/numa.c )
              • setup_node_data ( arch/x86/mm/numa.c )

需要说明的是,实际测试时,根据 boot log, x86_numa_init 的执行过程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __init x86_numa_init(void)
{
    if (!numa_off) {      // 通常为false,即默认启用NUMA
#ifdef CONFIG_ACPI_NUMA   // x86-64通常为true

        /* 
         * numa_init 函数执行时,会调用 x86_acpi_numa_init 函数,
         * 主要是转化 ACPI 的 SRAT 和 SLIT 的操作。从 boot log 来看,           
         * 函数返回值为负数,导致 numa_init 函数退出 
         */
        if (!numa_init(x86_acpi_numa_init))
            return;
#endif
#ifdef CONFIG_AMD_NUMA  // x86-64为true

        // amd_numa_init 函数从北桥的 PCI 配置空间获取 NUMA 信息
        if (!numa_init(amd_numa_init))
            return;
#endif
    }

    // 函数执行 dummy_numa_init
    numa_init(dummy_numa_init);
}

也就是说,默认情况下, x86 架构开启 NUMA 配置, x86_numa_init 函数首先执行 x86_acpi_numa_init 函数,尝试从 ACPI 的 SRAT 中获取 NUMA 信息,成功的话函数直接退出;否则再调用 amd_numa_init ,从北桥的 PCI 配置空间获取 NUMA 信息,成功的话直接退出;否则再调用 dummy_numa_init ,直接将 0 - max_pfn 范围的物理页框作为节点 0 ( 这个范围内可能有空洞 ) 。

不论是哪种方式,成功获取到 NUMA 信息后,都会将其添加到 numa_meminofo 变量。这个变量由 numa_init 调用函数 numa_register_memblks 时作为参数传递,添加到 memblock 变量,同时传递给 setup_node_data 函数,即每个节点的起始页框索引。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* Initialize NODE_DATA for a node on the local memory */
static void __init setup_node_data(int nid, u64 start, u64 end)
{
    const size_t nd_size = roundup(sizeof(pg_data_t), PAGE_SIZE);
    u64 nd_pa;
    void *nd;
    int tnid;

    /*
     * Don't confuse VM with a node that doesn't have the
     * minimum amount of memory:
     */
    if (end && (end - start) < NODE_MIN_SIZE)
        return;

    /* ZONE_ALIGN = 8MB */
    start = roundup(start, ZONE_ALIGN);

    printk(KERN_INFO "Initmem setup node %d [mem %#010Lx-%#010Lx]\n",
           nid, start, end - 1);

    /*
     * Allocate node data.  Try node-local memory and then any node.
     * Never allocate in DMA zone.
     */
    nd_pa = memblock_alloc_nid(nd_size, SMP_CACHE_BYTES, nid);
    if (!nd_pa) {
        nd_pa = __memblock_alloc_base(nd_size, SMP_CACHE_BYTES,
                          MEMBLOCK_ALLOC_ACCESSIBLE);
        if (!nd_pa) {
            pr_err("Cannot find %zu bytes in node %d\n",
                   nd_size, nid);
            return;
        }
    }
    nd = __va(nd_pa);

    /* report and initialize */
    printk(KERN_INFO "  NODE_DATA [mem %#010Lx-%#010Lx]\n",
           nd_pa, nd_pa + nd_size - 1);
    tnid = early_pfn_to_nid(nd_pa >> PAGE_SHIFT);

    // 分配node_data的节点和当前内存节点不一致
    if (tnid != nid)
        printk(KERN_INFO "    NODE_DATA(%d) on node %d\n", nid, tnid);

    node_data[nid] = nd;
    memset(NODE_DATA(nid), 0, sizeof(pg_data_t));
    NODE_DATA(nid)->node_id = nid;
    NODE_DATA(nid)->node_start_pfn = start >> PAGE_SHIFT;
    NODE_DATA(nid)->node_spanned_pages = (end - start) >> PAGE_SHIFT;

    node_set_online(nid);
}

1.2.2. free_area_init_node

setup_node_data 函数只会初始化 pg_data_t 的部分成员变量 ( 节点信息,起始页框信息 ) ,其余成员变量的初始化在函数 free_area_init_node 完成,两个函数的调用路径的先后顺序如下:

  • start_kernel ( init/main.c )
    • setup_arch ( arch/x86/kernel/setup.c )
      • initmem_init ( arch/x86/mm/numa_64.c )
        • x86_numa_init ( arch/x86/mm/numa.c )
          • numa_init ( arch/x86/mm/numa.c )
            • x86_acpi_numa_init ( arch/x86/mm/srat.c )
              • acpi_numa_init ( drivers/acpi/numa.c )
            • numa_register_memblks ( arch/x86/mm/numa.c )
              • setup_node_data ( arch/x86/mm/numa.c )
      • x86_init.paging.pagetable_init ( arch/x86/kernel/x86_init.c )
        • paging_init ( arch/x86/mm/init_64.c )
          • zone_sizes_init ( arch/x86/mm/init.c )
            • free_area_init_nodes(max_zone_pfns) ( mm/page_alloc.c )
              • free_area_init_node ( mm/page_alloc.c )
    • build_all_zonelists ( mm/page_alloc.c )
    • page_alloc_init ( mm/page_alloc.c )
    • mm_init ( init/main.c )
      • kmem_cache_init ( mm/slub.c )

setup_node_data 使用的 NUMA 信息来自 ACPI 表或者 BIOS-e820 ,可能包含空洞, PFN 的范围不准确; free_area_init_node 使用的内存信息来自 struct memblock memblock 变量,用 memblock 的信息对 NUMA 信息进一步修正 ( memblock 定义在 mm/memblock.c 中,保存始化阶段通过 memblock_reservememblock_set_node 函数保留的内存信息。):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* 执行上面的函数路径时,
   @zones_size = NULL
   @zholes_size = NULL */
void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
        unsigned long node_start_pfn, unsigned long *zholes_size)
{
    // 获取指定 nid 的 pg_data_t 指针 
    pg_data_t *pgdat = NODE_DATA(nid);
    unsigned long start_pfn = 0;
    unsigned long end_pfn = 0;

    /* pg_data_t should be reset to zero when it's allocated */
    /* struct pglist_data *node_data[MAX_NUMNODES] */
    WARN_ON(pgdat->nr_zones || pgdat->classzone_idx);

    pgdat->node_id = nid;
    pgdat->node_start_pfn = node_start_pfn;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP    //true
    /* 
     * 根据 struct memblock memblock 提供的信息获取给定
     * node 的起始页框。如果节点没有可用内存,起始和结束页框号
     * 都设置为 0 ,并输出警告信息 
     */
    get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);
#endif

    /* 
     * 计算 node 中的所有页面数
     * pgdata->node_spanned_pages = 页面总数
     * pgdata->node_present_pages = 总数 - hole 
     */
    calculate_node_totalpages(pgdat, start_pfn, end_pfn,
                  zones_size, zholes_size);

    // x86_64下函数为空
    alloc_node_mem_map(pgdat);
#ifdef CONFIG_FLAT_NODE_MEM_MAP     //false
    printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",
        nid, (unsigned long)pgdat,
        (unsigned long)pgdat->node_mem_map);
#endif
    /* 
     * 初始化pgdat中的成员变量,以及包含的每个zone的信息
     * @zones_size = NULL
     * @zholes_size = NULL 
     */
    free_area_init_core(pgdat, start_pfn, end_pfn,
                zones_size, zholes_size);
}

至此,变量 node_data 初始化完毕,包含的节点信息供后续的内存初始化操作使用。

1.3. zonelist的初始化

start_kernel 函数会调用 build_all_zonelists 初始化内存区域表,即 pg_data_tnode_zonelists 成员。

build_all_zonelists 会调用 __build_all_zonelists ,后者分别调用 build_zonelistsbuild_zonelist_cache 完成 zonelist 的初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static int __build_all_zonelists(void *data)
{
    int nid;
    int cpu;
    pg_data_t *self = data;

#ifdef CONFIG_NUMA

    // node_load 数组记录每个内存节点的负载情况
    memset(node_load, 0, sizeof(node_load));
#endif
    /*
     * self 指针在响应内存的热插拔时为 pg_data_t ,
     * 否则为 NULL 。这个分支用来执行内存的热插拔操作
     */
    if (self && !node_online(self->node_id)) {
        build_zonelists(self);
        build_zonelist_cache(self);
    }

    // 为每个在线的节点创建 zonelist ,以及 zlcache
    for_each_online_node(nid) {
        pg_data_t *pgdat = NODE_DATA(nid);

        build_zonelists(pgdat);
        build_zonelist_cache(pgdat);
    }
     /*
     * Initialize the boot_pagesets that are going to be used
     * for bootstrapping processors. The real pagesets for
     * each zone will be allocated later when the per cpu
     * allocator is available.
     *
     * boot_pagesets are used also for bootstrapping offline
     * cpus if the system is already booted because the pagesets
     * are needed to initialize allocators on a specific cpu too.
     * F.e. the percpu allocator needs the page allocator which
     * needs the percpu allocator in order to allocate its pagesets
     * (a chicken-egg dilemma).
     */
    for_each_possible_cpu(cpu) {
        setup_pageset(&per_cpu(boot_pageset, cpu), 0);

#ifdef CONFIG_HAVE_MEMORYLESS_NODES   // x86默认为false
        /*
         * We now know the "local memory node" for each node--
         * i.e., the node of the first zone in the generic zonelist.
         * Set up numa_mem percpu variable for on-line cpus.  During
         * boot, only the boot cpu should be on-line;  we'll init the
         * secondary cpus' numa_mem as they come on-line.  During
         * node/memory hotplug, we'll fixup all on-line cpus.
         */
        if (cpu_online(cpu))
            set_cpu_numa_mem(cpu, local_memory_node(cpu_to_node(cpu)));
#endif
    }

    return 0;
}

struct zonelist 结构如下,

1
2
3
4
5
6
7
struct zonelist {
    struct zonelist_cache *zlcache_ptr;          // NULL or &zlcache
    struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
#ifdef CONFIG_NUMA
    struct zonelist_cache zlcache;               // optional ...
#endif
};

MAX_ZONES_PER_ZONELIST 为系统中的节点数和 zone 类型数的乘积,假如系统中共有 2 个节点, zone 有 3 种类型 ( DMA,DMA32,NORMAL ) ,则 _zonerefs 的长度为 6 + 1 = 7 。数组的最后一个元素总是设置为空,作为结束标志。

1.3.1. build_zonelists

build_zonelists 函数完成 pg_data_t 成员 struct zonelist node_zonelists[MAX_ZONELISTS] 的初始化工作。 zonelist 是页面请求操作的对象,其中第一个 zone 是首选目标,其余的 zone 是备选项,按照优先级降序排列。

NUMA系统中, MAX_ZONELISTS 为 2 ,因为我们需要支持 __GFP_THISNODE 选项:

  • node_zonelists[0]
    带有备选node的zonelist
  • node_zonelists[1]
    没有备选node的zonelist

zonelist 的备选 node 通过函数 find_next_best_node 确定:备选 node 不能已经包含在zonelist 中,应该是距离当前节点次近的节点,不属于任何 CPU 的节点更佳 —— 这些节点没有分配内存的压力。距离根据距离数组计算 —— 数组中包含系统中所有节点间的距离。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
static void build_zonelists(pg_data_t *pgdat)
{
    int j, node, load;
    enum zone_type i;
    nodemask_t used_mask;
    int local_node, prev_node;
    struct zonelist *zonelist;
    int order = current_zonelist_order;

    /* initialize zonelists */
    for (i = 0; i < MAX_ZONELISTS; i++) {
        zonelist = pgdat->node_zonelists + i;
        /* 
         * 为了加速 zonelist 的读取操作, _zonerefs 包含
         * 正在读取的 zone 的索引,避免访问大的数据结构。
         */
        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);
    /*
     * 代码中此变量的注释写道:
     * 创建 zonelist 时会按照 zone type 从高到低的顺序排列,
     * 防止出现高位内存即 normal 内存消耗殆尽,而 DMA 内存
     * 还没有使用的情况,导致页面的分配操作被较远的节点响应。
     * 
     * 结合下面的代码,这个数组用来记录创建 zonelist 时每个
     * node 出现的先后顺序,按照和当前节点的距离降序排列。 
     */
    memset(node_order, 0, sizeof(node_order));
    j = 0;

    // 寻找可以作为备选节点的 node
    while ((node = find_next_best_node(local_node, &used_mask)) >= 0) {
        /*
         * 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 供 find_next_best_node 函数寻找下一个备选节点时作为参考依据
        load--;
        if (order == ZONELIST_ORDER_NODE)

            /*
             * zonelist 首先按照节点的距离排序,然后按照节点内 zone
             * 类型排序。这种情况能够保证最大局部性:
             * normal 内存消耗完之后,继续消耗本节点内的 DMA 内存。
             * 最后才会转向其他的节点,但是可能会耗尽 DMA 内存。 
             */
            build_zonelists_in_node_order(pgdat, node);
        else
            node_order[j++] = node; /* remember order */
    }

    /* 
     * 根据 node_order 数组按照距离由小到大添加所有节点同一类型的
     * zone,这种方式可以避免耗尽 DMA 内存 
     */
    if (order == ZONELIST_ORDER_ZONE) {
        /* calculate node order -- i.e., DMA last! */
        build_zonelists_in_zone_order(pgdat, j);
    }

    /* 
     * 创建支持 GFP_THISNODE 的 zonelist ,即没有备选节点的
     * node_zonelists[1] 。这种情况下,系统为非 NUMA ,只需要
     * 将节点 0 添加到 zonelist 即可 
     */
    build_thisnode_zonelists(pgdat);
}

可以看到,创建zonelist时,有两种策略,一种为了保证最大程度的局部性,即 build_zonelists_in_node_order ;一种为了避免DMA内存被耗尽,即 build_zonelists_in_zone_order 。两种策略可以通过grub参数 “numa_zonelist_order” 设置,也可以通过 sysctl 设置。

build_zonelists_in_node_order 找到传入的 pgdatanode_zonelists[0] 中第一个没有设置的 _zonerefs ,即 _zonerefs 中的最后一个元素,然后将 zonelist 中包含的所有 zone 按照类型从高到低的顺序添加到 _zonerefs 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static void build_zonelists_in_node_order(pg_data_t *pgdat, int node)
{
    int j;
    struct zonelist *zonelist;

    zonelist = &pgdat->node_zonelists[0];
    // 获取 _zonerefs 中最后一个元素的索引 j
    for (j = 0; zonelist->_zonerefs[j].zone != NULL; j++)
        ;
    /*
     * 将 zonelist 中的所有 zone 按照类型由高到低的顺序添加到
     * _zonerefs 中从 j 开始的位置 
     */
    j = build_zonelists_node(NODE_DATA(node), zonelist, j);

    // 设置_zonerefs中的最后一个元素为空,作为结束标志
    zonelist->_zonerefs[j].zone = NULL;
    zonelist->_zonerefs[j].zone_idx = 0;
}

build_zonelists_in_zone_order 则根据 node_order 数组中按照距当前节点的距离保存的内存节点先将同一类型的每个节点的所有zone添加,然后添加其他类型的 zone ,按照类型从高到低的顺序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void build_zonelists_in_zone_order(pg_data_t *pgdat, int nr_nodes)
{
    int pos, j, node;
    int zone_type;      /* needs to be signed */
    struct zone *z;
    struct zonelist *zonelist;

    zonelist = &pgdat->node_zonelists[0];
    pos = 0;

    // 按照zone类型从高到底的顺序 
    for (zone_type = MAX_NR_ZONES - 1; zone_type >= 0; zone_type--) {
        for (j = 0; j < nr_nodes; j++) {
            node = node_order[j];
            z = &NODE_DATA(node)->node_zones[zone_type];

            // zone 有效
            if (populated_zone(z)) {
                zoneref_set_zone(z,
                    &zonelist->_zonerefs[pos++]);
                check_highest_zone(zone_type);
            }
        }
    }

    // 设置最后一个_zonerefs为空,作为结束标志
    zonelist->_zonerefs[pos].zone = NULL;
    zonelist->_zonerefs[pos].zone_idx = 0;
}

假设系统中有两个内存节点 0 和 1 ,每个节点都有 ZONE_DMA , ZONE_DMA32 , ZONE_NORMAL 三种类型的区域,则 NODE_DATA(0) 的 zonelist->_zonerefs 在两种情况下初始化后分别为:

  • node order
    { ZONE_NORMAL-0, ZONE_DMA32-0, ZONE_DMA-0,
    ZONE_NORMAL-1, ZONE_DMA32-1, ZONE_DMA-1 }
  • zone order
    { ZONE_NORMAL-0, ZONE_NORMAL-1, ZONE_DMA32-0,
    ZONE_DMA32-1, ZONE_DMA-0, ZONE_DMA-1 }

1.3.2. build_zonelist_cache

1.3.2.1. zlcache的说明

build_zonelist_cache 初始化 struct zonelist 中的剩余两个成员变量: zlcache_ptrzlcache

struct zonelist_cache 缓存每个zonelist的关键信息,以便在get_page_from_freelist 函数中扫描空闲页面时减小高速缓存的 footprint 。

1
2
3
4
5
struct zonelist_cache {
    unsigned short z_to_n[MAX_ZONES_PER_ZONELIST];      /* zone->nid */
    DECLARE_BITMAP(fullzones, MAX_ZONES_PER_ZONELIST);  /* zone full? */
    unsigned long last_full_zap;        /* when last zap'd (jiffies) */
};

位图 fullzones 追踪上次 zero’d fullzones 之后, zonelist 中缺少空闲内存的 zone 信息。

数组 z_to_n[] 将 zonelist 中的每个 zone 映射到所属的节点的 ID ,以便快速确认该节点是否包含在当前进程的 mems_allowed 中。

fullzonesz_to_n[] 都和 zonelist 一一对应,通过 zonelist 的 _zonerefs 提供的偏移量进行索引。

get_page_from_freelist 函数执行两次扫描。第一次扫描时,跳过设置了 fullzones 中对应位的 zone ,以及对应的节点没有包含在 current->mems_allowed 中的 zone 。第二次扫描时,跳过 zonelist_cache ,保证查看了每个 zone 。

每一秒钟执行一次 zero out(zap) fullzones 的操作, last_full_zap 保存上一次 zap 操作的时间。这种机制可以减少 zone 刚刚进入低内存状态、不停地检测 zone 是否有空闲内存所花费的时间。

1.3.2.2. zlcache 的初始化

函数 build_zonelist_cache 完成 zlcache 的初始化,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void build_zonelist_cache(pg_data_t *pgdat)
{
    struct zonelist *zonelist;
    struct zonelist_cache *zlc;
    struct zoneref *z;

    /* 
     * 只需要初始化 node_zonelists[0],即有备选节点的 zonelist;
     * 没有备选节点的 zonelist 不需要 zlcache 来减小 zonelist 
     * 的 footprint 
     */
    zonelist = &pgdat->node_zonelists[0];

    // zlcache_ptr 指向自身的 zlcache 即可
    zonelist->zlcache_ptr = zlc = &zonelist->zlcache;

    // 初始化 fullzones 位图
    bitmap_zero(zlc->fullzones, MAX_ZONES_PER_ZONELIST);

    // 将 zonelist 中的每个 zone 所属的节点信息保存在z_to_n[]
    for (z = zonelist->_zonerefs; z->zone; z++)
        zlc->z_to_n[z - zonelist->_zonerefs] = zonelist_node_idx(z);
}

1.3.3. zonelist的额外说明

struct zonelist_cache 的成员属于 struct zonelist 。但是, MPOL_BIND 内存策略的 zonelist 结构体长度不同,通常更短 —— MPOL_BIND 策略不需要 zonelist_cache 成员,因此我们将固定长度的成员放在 zonelist 结构体的起始位置,以 zonelist_cache 结尾。

zonelist_cache 这个可选的成员变量通过 zlcache_ptr 指针定位,在 MPOL_BIND 的情况下指针为NULL。

由此, strut zonelist 有两种形式:

  1. 完整的、固定长度的版本
  2. 针对 MPOL_BIND 内存策略的版本 ( zlcache_ptr 为空 )

虽然存在多个 CPU 同时修改 zonelist_cachefullzoneslast_full_zap 的情况,我们没有加锁 —— 这只是提示信息,如果这些信息有误,只不过会影响分配器的运行速度,但是不会影响分配器的功能。

2. 内存区域

根据物理内存的使用方式,内核将物理内存分为多个内存区域 ( zone ) ,每个 zone 用 struct zone 来描述。以 x86_64 架构为例,内存包括 ZONE_DMA,ZONE_DMA32 , ZONE_NORMAL , ZONE_MOVABLE 四种。

因为系统中的 DMA 设备寻址范围小于 16MB ,因此需要 ZONE_DMA ;有的设备只能寻址小于 4GB 的地址,需要 ZONE_DMA32 。

2.1. struct zone

Linux内核用 struct zone 描述内存区域:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct zone {
    unsigned long watermark[NR_WMARK];
    long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
    int node;  /* zone所属的内存节点 */
#endif
    unsigned int inactive_ratio;

    struct pglist_data *zone_pgdat;  /* zone所属的pg_data_t */
    struct per_cpu_pageset __percpu *pageset;
    unsigned long dirty_balance_reserve;
#ifdef CONFIG_NUMA
    /*
     * zone reclaim becomes active if more unmapped pages exist.
     */
    unsigned long       min_unmapped_pages;
    unsigned long       min_slab_pages;
#endif /* CONFIG_NUMA */
    unsigned long       zone_start_pfn; /* zone的起始PFN */

    unsigned long       managed_pages;
    unsigned long       spanned_pages;
    unsigned long       present_pages;

    const char      *name;  /* zone的名称 */
    int         nr_migrate_reserve_block;

    wait_queue_head_t   *wait_table;
    unsigned long       wait_table_hash_nr_entries;
    unsigned long       wait_table_bits;

    ZONE_PADDING(_pad1_)

    /* Write-intensive fields used from the page allocator */
    spinlock_t      lock;

    /* free areas of different sizes */
    struct free_area    free_area[MAX_ORDER];

    /* zone flags, see below */
    unsigned long       flags;

    ZONE_PADDING(_pad2_)
    /* Fields commonly accessed by the page reclaim scanner */
    spinlock_t      lru_lock;
    struct lruvec       lruvec;

    /* Evictions & activations on the inactive file list */
    atomic_long_t       inactive_age;

    /*
     * When free pages are below this point, additional steps are taken
     * when reading the number of free pages to avoid per-cpu counter
     * drift allowing watermarks to be breached
     */
    unsigned long percpu_drift_mark;

    ZONE_PADDING(_pad3_)
    /* Zone statistics */
    atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];
};

2.1.1. struct zone 的成员变量

2.1.1.1. watermark

watermark 数组长度为 3 ,三个元素分别是 WMARK_MIN , WMARK_LOW , WMARK_HIGH 。

watermark 的描述来自 《 Undering the Linux Virtual Memory Manager 》 一书:

可用的内存少于 LOW 值, kswapd 程序就会被唤醒,执行页面释放操作。如果压力很大,进程会同步的释放内存,即 direct-reclaim 路径。

每一个区域都有一个 watermark 数组,追踪区域的内存压力。 MIN 值在内存初始化时通过函数 free_area_init_core 计算,通常是 ZoneSizeInPages/128 。

2.1.1.1.1. WMARK_LOW

区域的空闲页面数到达 LOW 时,伙伴分配器会唤醒 kswapd ,开始释放页面。 LOW 的默认值时 MIN 的两倍。

2.1.1.1.2. WMARK_MIN

到达 MIN 时,分配器会以同步的方式执行 kswapd 操作,被称作 direct-reclaim 路径。

2.1.1.1.3. WMARK_HIGH

kswapd 唤醒之后,直到空闲页面的数量到达 HIGH ,内存区域才会被视为 balanced ,此时 kswapd 回到休眠状态。 HIGH 的默认值为 MIN 的三倍。

2.1.1.2. lowmem_reserve

内核中的注释如下:

我们不知道分配出去的内存最终是否会被释放,为了避免浪费数 GB 的内存,我们必须保留 lower zone 的内存,否则就有在 lower zone 出现 OOM 的风险,尽管在 higher zone 可能有大量可用的内存。

这个数组在运行时会随着 sysctl_lowmem_reserve_ratio 的变化而变化。

根据之前的说明,较高区域的内存通常较大,较低区域的内存通常较小。这个数组用来限制每个内存区域应该保留的内存页的数量,避免在较低内存区域出现 OOM 的情况。

2.1.1.3. xx_pages

2.1.1.3.1. spanned_pages

spanned_pages 包含空洞,计算方法为 spanned_pages = zone_end_pfn - zone_start_pfn

2.1.1.3.2. present_pages

present_pages 是区域包含的物理页框数,不包含空洞,计算方法为 present_pages = spanned_pages - absent_pages(pages in holes)

2.1.1.3.3. managed_pages

managed_pages 是伙伴系统当前管理的页框数,计算方法为 managed_pages = present_pages - reserved_pages

因此,内存热插拔系统或者内存的电源管理逻辑可以使用 present_pages - managed_pages 来获得不受管理的页面数。页面分配器和 VM 扫描器负责计算被管理页面的所有阈值信息。

2.1.1.4. wait_table_xx

wait_table 的相关变量用于追踪等待页面的进程信息,在页面可用时唤醒进程。问题是可能占用大量的空间,特别是同一时刻等待页面的进程很少。因此,我们使用哈希表来代替 per-page 等待队列。

进程被唤醒时,必须再次确认等待的页面已经可用 —— 由于使用的是哈希表,某一个页面可用时,被唤醒的进程可能有一大堆。

wait_table 保存等待队列哈希表; wait_table_hash_nr_entries 是哈希表的长度,即 1 << wait_table_bits

2.1.2. 内存区域的初始化

strut zone 的成员变量的初始化主要由 free_area_init_core 函数完成:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
static void __paginginit free_area_init_core(struct pglist_data *pgdat,
        unsigned long node_start_pfn, unsigned long node_end_pfn,
        unsigned long *zones_size, unsigned long *zholes_size)
{
    enum zone_type j;
    int nid = pgdat->node_id;
    unsigned long zone_start_pfn = pgdat->node_start_pfn;
    int ret;

    /* 
     * resize 用来支持内存的热插拔,这个函数在开启
     * CONFIG_MEMORY_HOTPLUG 时初始化 pgdat 中的 node_size_lock 自旋锁 
     */
    pgdat_resize_init(pgdat);
#ifdef CONFIG_NUMA_BALANCING

    // 和内存负载均衡相关的参数,此处不多介绍 
    spin_lock_init(&pgdat->numabalancing_migrate_lock);
    pgdat->numabalancing_migrate_nr_pages = 0;
    pgdat->numabalancing_migrate_next_window = jiffies;
#endif

    /* 
     * 初始化两个等待队列 kswap_wait 用来唤醒 kswapd 进程执行
     * 内存回收操作, pfmemalloc_wait 暂时不清楚用途 
     */
    init_waitqueue_head(&pgdat->kswapd_wait);
    init_waitqueue_head(&pgdat->pfmemalloc_wait);

    // 和cgroup相关的初始化
    pgdat_page_cgroup_init(pgdat);

    // 初始化 pg_data_t 中包含的每个 zone 的信息
    for (j = 0; j < MAX_NR_ZONES; j++) {
        struct zone *zone = pgdat->node_zones + j;
        unsigned long size, realsize, freesize, memmap_pages;

        size = zone_spanned_pages_in_node(nid, j, node_start_pfn,
                          node_end_pfn, zones_size);
        realsize = freesize = size - zone_absent_pages_in_node(nid, j,
                                node_start_pfn,
                                node_end_pfn,
                                zholes_size);

        /*
         * Adjust freesize so that it accounts for how much memory
         * is used by this zone for memmap. This affects the watermark
         * and per-cpu initialisations
         */
        memmap_pages = calc_memmap_size(size, realsize);
        if (freesize >= memmap_pages) {
            freesize -= memmap_pages;
            if (memmap_pages)
                printk(KERN_DEBUG
                       "  %s zone: %lu pages used for memmap\n",
                       zone_names[j], memmap_pages);
        } else
            printk(KERN_WARNING
                "  %s zone: %lu pages exceeds freesize %lu\n",
                zone_names[j], memmap_pages, freesize);

        /* Account for reserved pages */
        if (j == 0 && freesize > dma_reserve) {
            freesize -= dma_reserve;
            printk(KERN_DEBUG "  %s zone: %lu pages reserved\n",
                    zone_names[0], dma_reserve);
        }

        if (!is_highmem_idx(j))
            nr_kernel_pages += freesize;
        /* Charge for highmem memmap if there are enough kernel pages */
        else if (nr_kernel_pages > memmap_pages * 2)
            nr_kernel_pages -= memmap_pages;
        nr_all_pages += freesize;

        zone->spanned_pages = size;
        zone->present_pages = realsize;
        /*
         * Set an approximate value for lowmem here, it will be adjusted
         * when the bootmem allocator frees pages into the buddy system.
         * And all highmem pages will be managed by the buddy system.
         */
        zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
#ifdef CONFIG_NUMA
        zone->node = nid;
        zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio)
                        / 100;
        zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100;
#endif
        zone->name = zone_names[j];
        spin_lock_init(&zone->lock);
        spin_lock_init(&zone->lru_lock);
        zone_seqlock_init(zone);
        zone->zone_pgdat = pgdat;

        // 初始化 per-cpu 成员变量 pageset
        zone_pcp_init(zone);

        /* For bootup, initialized properly in watermark setup */
        mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages);

        // 初始化成员变量 lruvec
        lruvec_init(&zone->lruvec);
        if (!size)
            continue;

        set_pageblock_order();

        // sparse 模型下为空
        setup_usemap(pgdat, zone, zone_start_pfn, size);
        /* 初始化zone的wait_table和free_area域 */
        ret = init_currently_empty_zone(zone, zone_start_pfn,
                        size, MEMMAP_EARLY);
        BUG_ON(ret);

        // 设置 zone 内页框对应的 struct page 信息
        memmap_init(size, nid, j, zone_start_pfn);
        zone_start_pfn += size;
    }
}

3. zone_pcp_init

free_area_init_core 还会调用 zone_pcp_init 初始化 struct zone 的成员 struct per_cpu_pageset __percpu *pageset ,即《深入理解 Linux 内核》一书中第八章内存管理的“每 cpu 页框高速缓存”一节。

根据书中所述:

为了提升系统性能,每个内存管理区定义了一个“每 CPU ”页框高速缓存。所有“每CPU”页框高速缓存包含一些预先分配的页框,它们用于满足本地 CPU 发出的单一内存请求。

这里的“每 CPU ”页框高速缓存就是 struct per_cpu_pageset ,定义在 include/linux/mmzone.h 。书中还说每个 per_cpu_pageset 对象都包含两个 struct per_cpu_pages 对象,一个热高速缓存,一个冷高速缓存 —— 这个是2.6内核的实现,3.16只包含一个 per_cpu_pages 对象。

struct per_cpu_pages 定义如下:

1
2
3
4
5
6
7
8
struct per_cpu_pages {
    int count;      /* 高速缓存中页框数 */
    int high;       /* 上界,表示高速缓存用尽 */
    int batch;      /* 添加/移除操作的页框数 */

    /* Lists of pages, one per migrate type stored on the pcp-lists */
    struct list_head lists[MIGRATE_PCPTYPES];
};

3.1. boot_pageset

zone_pcp_init 直接将 zone 的 pageset 指向定义在 mm/page_alloc.c 中的 boot_pageset 变量,这个变量在 __build_all_zonelists 函数中通过 setup_pageset 初始化:

1
2
3
4
5
static void setup_pageset(struct per_cpu_pageset *p, unsigned long batch)
{
    pageset_init(p);
    pageset_set_batch(p, batch);
}

其中 pageset_init 初始化 struct per_cpu_pageset 的每个成员; pageset_set_batch 设置 per-cpu 页框高速缓存的 batch 为传入的值,即 0 。

3.2. rmqueue_bulk

zone_pcp_init 将 zone 的 pageset 指向 boot_pageset 后, per-cpu 页框高速缓存中没有可用的页。
buddy allocator 分配内存页时,会调用 rmqueue_bulk 分配页框到 per-cpu 页框高速缓存。这个函数在 buddy allocator 一文中再进行详细说明。

4. free_area 的初始化

上面的函数建立了 zone 和 zonelist ,但是分配内存页真正用到的其实是 struct zone 中的 struct free_area free_area[MAX_ORDER] 成员, zone 中的所有内存会分成 $2^{0}-2^{10}$ 个连续的页框,以解决外碎片的问题。

4.1. init_currently_empty_zone

free_area_init_core 调用 init_currently_empty_zone 函数完成 free_area 的初始化,这个函数只是将每种大小的 free_area 的数量初始化为零,并且初始化每种 free_area 包含的所有 migrate type 的链表。
但是并没有真正建立可以分配内存页的 free_area 结构。

4.2. memmap_init_zone

free_area_init_core 调用的另一个函数 memmap_init , x86 架构下定义为 memmap_init_zone
这个函数主要设置 zone 中每个 page 的具体信息,即 struct page 的各个成员变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
        unsigned long start_pfn, enum memmap_context context)
{
    struct page *page;
    unsigned long end_pfn = start_pfn + size;
    unsigned long pfn;
    struct zone *z;

    if (highest_memmap_pfn < end_pfn - 1)
        highest_memmap_pfn = end_pfn - 1;

    z = &NODE_DATA(nid)->node_zones[zone];
    for (pfn = start_pfn; pfn < end_pfn; pfn++) {
        /*
         * There can be holes in boot-time mem_map[]s
         * handed to this function.  They do not
         * exist on hotplugged memory.
         */
        if (context == MEMMAP_EARLY) {
            if (!early_pfn_valid(pfn))
                continue;
            if (!early_pfn_in_nid(pfn, nid))
                continue;
        }
        page = pfn_to_page(pfn);

        /*
         * 下列操作设置 struct page 的各个成员:
         * flag 域的 zone , nodeid , memsection
         * _count = -1,
         * _mapcount = 1,
         * _last_cpuid = 0xff,
         * flag 域的 reserved 标志 
         */
        set_page_links(page, zone, nid, pfn);
        mminit_verify_page_links(page, zone, nid, pfn);
        init_page_count(page);
        page_mapcount_reset(page);
        page_cpupid_reset_last(page);
        SetPageReserved(page);

        if ((z->zone_start_pfn <= pfn)
            && (pfn < zone_end_pfn(z))
            && !(pfn & (pageblock_nr_pages - 1)))
            set_pageblock_migratetype(page, MIGRATE_MOVABLE);

        INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL    //x86 下未定义
        /* The shift won't overflow because ZONE_NORMAL is below 4G. */
        if (!is_highmem_idx(zone))
            set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
    }
}

4.3. free_all_bootmem

free_area 初始化的过程,是在 free_all_bootmem 函数,这个函数由 arch/x86/mm/init_64.c 中的 mem_init 调用。
x86下, CONFIG_NO_BOOTMEM=y ,即没有开启bootmem,因此采用 mm/nobootmem.c 中的定义,调用 free_low_memory_core_early 函数完成主要工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static unsigned long __init free_low_memory_core_early(void)
{
    unsigned long count = 0;
    phys_addr_t start, end;
    u64 i;

    for_each_free_mem_range(i, NUMA_NO_NODE, &start, &end, NULL)
        count += __free_memory_core(start, end);

#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK //x86下默认定义
    {
        phys_addr_t size;

        /* Free memblock.reserved array if it was allocated */
        size = get_allocated_memblock_reserved_regions_info(&start);
        if (size)
            count += __free_memory_core(start, start + size);

        /* Free memblock.memory array if it was allocated */
        size = get_allocated_memblock_memory_regions_info(&start);
        if (size)
            count += __free_memory_core(start, start + size);
    }
#endif

    return count;
}

函数主要通过 __free_memory_core 释放 memblock 中保存的内存块,后者调用 __free_pages_memory

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static void __init __free_pages_memory(unsigned long start, unsigned long end)
{
    int order;

    while (start < end) {
        /*
         MAX_ORDER = 11,__ffs返回start中最高位的索引,
         因此,start越大,order可能越大,但是最大不会
         超过10 */
        order = min(MAX_ORDER - 1UL, __ffs(start));
        /* 将order限制在合法的范围内 */
        while (start + (1UL << order) > end)
            order--;

        __free_pages_bootmem(pfn_to_page(start), order);

        start += (1UL << order);
    }
}

__free_pages_memory 最终会调用 mm/page_alloc.c 中的 __free_pages_bootmem ,传入起始页面和释放的 order :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void __init __free_pages_bootmem(struct page *page, unsigned int order)
{
    unsigned int nr_pages = 1 << order;
    struct page *p = page;
    unsigned int loop;

    prefetchw(p);
    for (loop = 0; loop < (nr_pages - 1); loop++, p++) {
        prefetchw(p + 1);
        __ClearPageReserved(p);
        set_page_count(p, 0);
    }
    __ClearPageReserved(p);
    set_page_count(p, 0);

    page_zone(page)->managed_pages += nr_pages;
    set_page_refcounted(page);
    __free_pages(page, order);
}

__free_pages_bootmem 清除之前的 reserved 标志,先将 _count 设置为 0 ,之后再设置为 1 ,然后调用 __free_pages,将 page 释放到 buddy 系统。

__free_pages 是伙伴系统释放内存页的函数,放在下一篇 buddy allocator 进行说明。

5. 总结

至此,伙伴系统分配页面所需的内存信息建立完成: zone , zonelist ,以及每个 zone 的 free_area 信息。
buddy allocator 的具体执行流程,在下一篇文章介绍。