1. 程式人生 > >啟動期間的記憶體管理之build_zonelists初始化備用記憶體域列表zonelists--Linux記憶體管理(十三)

啟動期間的記憶體管理之build_zonelists初始化備用記憶體域列表zonelists--Linux記憶體管理(十三)

1. 今日內容(第二階段(二)–初始化備用記憶體域列表zonelists)

我們之前講了在memblock完成之後, 記憶體初始化開始進入第二階段, 第二階段是一個漫長的過程, 它執行了一系列複雜的操作, 從體系結構相關資訊的初始化慢慢向上層展開, 其主要執行了如下操作

特定於體系結構的設定

在完成了基礎的記憶體結點和記憶體域的初始化工作以後, 我們必須克服一些硬體的特殊設定

  • 在初始化記憶體的結點和記憶體區域之前, 核心先通過pagging_init初始化了核心的分頁機制, 這樣我們的虛擬執行空間就初步建立, 並可以完成實體地址到虛擬地址空間的對映工作.

在arm64架構下, 核心在start_kernel()

->setup_arch()中通過arm64_memblock_init( )完成了memblock的初始化之後, 接著通過setup_arch()->paging_init()開始初始化分頁機制

paging_init負責建立只能用於核心的頁表, 使用者空間是無法訪問的. 這對管理普通應用程式和核心訪問記憶體的方式,有深遠的影響

  • 在分頁機制完成後, 核心通過setup_arch()->bootmem_init開始進行記憶體基本資料結構(記憶體結點pg_data_t, 記憶體域zone和頁幀)的初始化工作, 就是在這個函式中, 核心開始從體系結構相關的部分逐漸展開到體系結構無關的部分, 在zone_sizes_init->free_area_init_node中開始, 核心開始進行記憶體基本資料結構的初始化, 也不再依賴於特定體系結構無關的層次
bootmem_init
始化記憶體資料結構包括記憶體節點, 記憶體域和頁幀page
|
|---->arm64_numa_init();
|     支援numa架構
|
|---->zone_sizes_init(min, max);
    來初始化節點和管理區的一些資料項
    |
    |---->free_area_init_node
    |   初始化記憶體節點
    |
        |---->free_area_init_core
            |   初始化zone
            |
            |---->memmap_init
            |   初始化page頁面
|
|---->memblock_dump_all();
|   初始化完成, 顯示memblock的保留的所有記憶體資訊

至此,bootmem_init已經完成了節點和管理區的關鍵資料已完成初始化, 核心在後面為記憶體管理做得一個準備工作就是將所有節點的管理區都鏈入到zonelist中,便於後面記憶體分配工作的進行.

核心在start_kernel()–>build_all_zonelist()中完成zonelist的初始化

2 後備記憶體域列表zonelists

核心setup_arch的最後通過bootmem_init中完成了記憶體資料結構的初始化(包括記憶體結點pg_data_t, 記憶體管理域zone和頁面資訊page), 資料結構已經基本準備好了, 在後面為記憶體管理做得一個準備工作就是將所有節點的管理區都鏈入到zonelist中, 便於後面記憶體分配工作的進行.

2.1 回到start_kernel函式(已經完成的工作)

asmlinkage __visible void __init start_kernel(void)
{

    setup_arch(&command_line);


    build_all_zonelists(NULL, NULL);
    page_alloc_init();


    /*
     * These use large bootmem allocations and must precede
     * mem_init();
     * kmem_cache_init();
     */
    mm_init();

    kmem_cache_init_late();

    kmemleak_init();
    setup_per_cpu_pageset();

    rest_init();
}

下面核心開始通過start_kernel()->build_all_zonelists來設計記憶體的組織形式

2.2 後備記憶體域列表zonelist

記憶體節點pg_data_t中將記憶體節點中的記憶體區域zone按照某種組織層次儲存在一個zonelist中, 即pglist_data->node_zonelists成員資訊

//  http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L626
typedef struct pglist_data
{
    struct zone node_zones[MAX_NR_ZONES];
    struct zonelist node_zonelists[MAX_ZONELISTS];
}

核心定義了記憶體的一個層次結構關係, 首先試圖分配廉價的記憶體,如果失敗,則根據訪問速度和容量,逐漸嘗試分配更昂貴的記憶體.

高階記憶體最廉價, 因為核心沒有任何部分依賴於從該記憶體域分配的記憶體, 如果高階記憶體用盡, 對核心沒有副作用, 所以優先分配高階記憶體

普通記憶體域的情況有所不同, 許多核心資料結構必須儲存在該記憶體域, 而不能放置到高階記憶體域, 因此如果普通記憶體域用盡, 那麼核心會面臨記憶體緊張的情況

DMA記憶體域最昂貴,因為它用於外設和系統之間的資料傳輸。

舉例來講,如果核心指定想要分配高階記憶體域。它首先在當前結點的高階記憶體域尋找適當的空閒記憶體段,如果失敗,則檢視該結點的普通記憶體域,如果還失敗,則試圖在該結點的DMA記憶體域分配。如果在3個本地記憶體域都無法找到空閒記憶體,則檢視其他結點。這種情況下,備選結點應該儘可能靠近主結點,以最小化訪問非本地記憶體引起的效能損失。

2.3 build_all_zonelists初始化zonelists

核心在start_kernel中通過build_all_zonelists完成了記憶體結點及其管理記憶體域的初始化工作, 呼叫如下

  build_all_zonelists(NULL, NULL);

build_all_zonelists建立記憶體管理結點及其記憶體域的組織形式, 將描述記憶體的資料結構(結點, 管理域, 頁幀)通過一定的演算法組織在一起, 方便以後記憶體管理工作的進行. 該函式定義在mm/page_alloc.c?v4.7, line 5029

2.4 build_all_zonelists函式

/*
 * Called with zonelists_mutex held always
 * unless system_state == SYSTEM_BOOTING.
 *
 * __ref due to (1) call of __meminit annotated setup_zone_pageset
 * [we're only called with non-NULL zone through __meminit paths] and
 * (2) call of __init annotated helper build_all_zonelists_init
 * [protected by SYSTEM_BOOTING].
 */
void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone)
{
    /*  設定zonelist中節點和記憶體域的組織形式
     *  current_zonelist_order變數標識了當前系統的記憶體組織形式
     *  zonelist_order_name以字串儲存了系統中記憶體組織形式的名稱  */
    set_zonelist_order();

    if (system_state == SYSTEM_BOOTING) {
        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();
    /*
     * 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))
        page_group_by_mobility_disabled = 1;
    else
        page_group_by_mobility_disabled = 0;

    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
}

3 設定結點初始化順序

在build_all_zonelists開始, 首先核心通過set_zonelist_order函式設定了zonelist_order,如下所示, 參見mm/page_alloc.c?v=4.7, line 5031

void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone)
{
    set_zonelist_order();
    /* .......  */
}

3.1 zonelist

前面我們講解記憶體管理域時候講解到, 系統中的所有管理域都儲存在一個多維的陣列zone_table. 核心在初始化記憶體管理區時, 必須要建立管理區表zone_table. 參見mm/page_alloc.c?v=2.4.37, line 38

/*
 *
 * The zone_table array is used to look up the address of the
 * struct zone corresponding to a given zone number (ZONE_DMA,
 * ZONE_NORMAL, or ZONE_HIGHMEM).
 */
zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES];
EXPORT_SYMBOL(zone_table);
  • MAX_NR_NODES為系統中記憶體結點的數目
  • MAX_NR_ZONES為系統中單個記憶體結點所擁有的最大記憶體區域數目

3.2 記憶體域初始化順序zonelist_order

NUMA系統中存在多個節點, 每個節點對應一個struct pglist_data結構, 每個結點中可以包含多個zone, 如: ZONE_DMA, ZONE_NORMAL, 這樣就產生幾種排列順序, 以2個節點2個zone為例(zone從高到低排列, ZONE_DMA0表示節點0的ZONE_DMA,其它類似).

  • Legacy方式, 每個節點只排列自己的zone;

  • Node方式, 按節點順序依次排列,先排列本地節點的所有zone,再排列其它節點的所有zone。

  • Zone方式, 按zone型別從高到低依次排列各節點的同相型別zone

可通過啟動引數”numa_zonelist_order”來配置zonelist order,核心定義了3種配置, 這些順序定義在mm/page_alloc.c?v=4.7, line 4551

// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4551
/*
 *  zonelist_order:
 *  0 = automatic detection of better ordering.
 *  1 = order by ([node] distance, -zonetype)
 *  2 = order by (-zonetype, [node] distance)
 *
 *  If not NUMA, ZONELIST_ORDER_ZONE and ZONELIST_ORDER_NODE will create
 *  the same zonelist. So only NUMA can configure this param.
 */
#define ZONELIST_ORDER_DEFAULT  0 /* 智慧選擇Node或Zone方式 */

#define ZONELIST_ORDER_NODE     1 /* 對應Node方式 */

#define ZONELIST_ORDER_ZONE     2 /* 對應Zone方式 */

注意

在非NUMA系統中(比如UMA), 由於只有一個記憶體結點, 因此ZONELIST_ORDER_ZONE和ZONELIST_ORDER_NODE選項會配置相同的記憶體域排列方式, 因此, 只有NUMA可以配置這幾個引數

全域性的current_zonelist_order變數標識了系統中的當前使用的記憶體域排列方式, 預設配置為ZONELIST_ORDER_DEFAULT, 參見mm/page_alloc.c?v=4.7, line 4564

//  http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4564
/* zonelist order in the kernel.
 * set_zonelist_order() will set this to NODE or ZONE.
 */
static int current_zonelist_order = ZONELIST_ORDER_DEFAULT;
static char zonelist_order_name[3][8] = {"Default", "Node", "Zone"};

而zonelist_order_name方式分別對應了Legacy方式, Node方式和Zone方式. 其zonelist_order_name[current_zonelist_order]就標識了當前系統中所使用的記憶體域排列方式的名稱”Default”, “Node”, “Zone”.

巨集 zonelist_order_name巨集 排列方式 描述
ZONELIST_ORDER_DEFAULT Default 由系統智慧選擇Node或Zone方式
ZONELIST_ORDER_NODE Node Node方式 按節點順序依次排列,先排列本地節點的所有zone,再排列其它節點的所有zone
ZONELIST_ORDER_ZONE Zone Zone方式 按zone型別從高到低依次排列各節點的同相型別zone

3.3 set_zonelist_order設定排列方式

核心就通過通過set_zonelist_order函式設定當前系統的記憶體域排列方式current_zonelist_order, 其定義依據系統的NUMA結構還是UMA結構有很大的不同.

// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4571
#ifdef CONFIG_NUMA
/* The value user specified ....changed by config */
static int user_zonelist_order = ZONELIST_ORDER_DEFAULT;
/* string for sysctl */
#define NUMA_ZONELIST_ORDER_LEN 16
char numa_zonelist_order[16] = "default";


//  http://lxr.free-electrons.com/source/mm/page_alloc.c#L4571
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;
}


#else   /* CONFIG_NUMA */

//  http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4892
static void set_zonelist_order(void)
{
    current_zonelist_order = ZONELIST_ORDER_ZONE;
}

其設定的基本流程如下

  • 如果系統當前系統是非NUMA結構的, 則系統中只有一個結點, 配置ZONELIST_ORDER_NODE和ZONELIST_ORDER_ZONE結果相同. 那麼set_zonelist_order函式被定義為直接配置當前系統的記憶體域排列方式current_zonelist_order為ZONE方式(與NODE效果相同)
  • 如果系統是NUMA結構, 則設定為系統指定的方式即可
  1. 當前的排列方式為ZONELIST_ORDER_DEFAULT, 即系統預設方式, 則current_zonelist_order則由核心交給default_zonelist_order採用一定的演算法選擇一個最優的分配策略, 目前的系統中如果是32位則配置為ZONE方式, 而如果是64位系統則設定為NODE方式
  2. 當前的排列方式不是預設方式, 則設定為user_zonelist_order指定的記憶體域排列方式

3.4 default_zonelist_order函式選擇最優的配置

在UMA結構下, 記憶體域使用NODE和ZONE兩個排列方式會產生相同的效果, 因此係統不用特殊指定, 直接通過set_zonelist_order函式, 將當前系統的記憶體域排列方式current_zonelist_order配置為為ZONE方式(與NODE效果相同)即可

但是NUMA結構下, 預設情況下(當配置了ZONELIST_ORDER_DEFAULT), 系統需要根據系統自身的環境資訊選擇一個最優的配置(NODE或者ZONE方式), 這個工作就由default_zonelist_order函數了來完成. 其定義在mm/page_alloc.c?v=4.7, line 4789

#if defined(CONFIG_64BIT)
/*
 * Devices that require DMA32/DMA are relatively rare and do not justify a
 * penalty to every machine in case the specialised case applies. Default
 * to Node-ordering on 64-bit NUMA machines
 */
static int default_zonelist_order(void)
{
    return ZONELIST_ORDER_NODE;
}
#else
/*
 * On 32-bit, the Normal zone needs to be preserved for allocations accessible
 * by the kernel. If processes running on node 0 deplete the low memory zone
 * then reclaim will occur more frequency increasing stalls and potentially
 * be easier to OOM if a large percentage of the zone is under writeback or
 * dirty. The problem is significantly worse if CONFIG_HIGHPTE is not set.
 * Hence, default to zone ordering on 32-bit.
 */
static int default_zonelist_order(void)
{
    return ZONELIST_ORDER_ZONE;
}
#endif /* CONFIG_64BIT */

3.5 user_zonelist_order使用者指定排列方式

在NUMA結構下, 系統支援使用者指定記憶體域的排列方式, 使用者以字串的形式操作numa_zonelist_order(default, node和zone), 最終被核心轉換為user_zonelist_order, 這個變數被指定為字串numa_zonelist_order指定的排列方式, 他們定義在mm/page_alloc.c?v4.7, line 4573, 注意只有在NUMA結構中才需要這個配置資訊.

#ifdef CONFIG_NUMA
/* The value user specified ....changed by config */
static int user_zonelist_order = ZONELIST_ORDER_DEFAULT;
/* string for sysctl */
#define NUMA_ZONELIST_ORDER_LEN 16
char numa_zonelist_order[16] = "default";

#else
/* ......*/
#endif

而接受和處理使用者配置的工作, 自然是交給我們強大的proc檔案系統來完成的, 可以通過/proc/sys/vm/numa_zonelist_order動態改變zonelist order的分配方式。

核心通過setup_numa_zonelist_order讀取並處理使用者寫入的配置資訊

  • 接收到使用者的資訊後用__parse_numa_zonelist_order處理接收的引數
  • 如果前面用__parse_numa_zonelist_order處理的資訊串成功, 則將對用的設定資訊寫入到字串numa_zonelist_order中

參見mm/page_alloc.c?v=4.7, line 4578

/*
 * interface for configure zonelist ordering.
 * command line option "numa_zonelist_order"
 *      = "[dD]efault   - default, automatic configuration.
 *      = "[nN]ode      - order by node locality, then by zone within node
 *      = "[zZ]one      - order by zone, then by locality within zone
 */

static int __parse_numa_zonelist_order(char *s)
{
    if (*s == 'd' || *s == 'D') {
        user_zonelist_order = ZONELIST_ORDER_DEFAULT;
    } else if (*s == 'n' || *s == 'N') {
        user_zonelist_order = ZONELIST_ORDER_NODE;
    } else if (*s == 'z' || *s == 'Z') {
        user_zonelist_order = ZONELIST_ORDER_ZONE;
    } else {
        pr_warn("Ignoring invalid numa_zonelist_order value:  %s\n", s);
        return -EINVAL;
    }
    return 0;
}

static __init int setup_numa_zonelist_order(char *s)
{
    int ret;

    if (!s)
        return 0;

    ret = __parse_numa_zonelist_order(s);
    if (ret == 0)
        strlcpy(numa_zonelist_order, s, NUMA_ZONELIST_ORDER_LEN);

    return ret;
}
early_param("numa_zonelist_order", setup_numa_zonelist_order);

4 build_all_zonelists_init完成記憶體域zonelists的初始化

build_all_zonelists函式在通過set_zonelist_order設定了zonelists中結點的組織順序後, 首先檢查了ssytem_state標識. 如果當前系統處於boot階段(SYSTEM_BOOTING), 就開始通過build_all_zonelists_init函式初始化zonelist

build_all_zonelists(pg_data_t *pgdat, struct zone *zone)
{
    /*  設定zonelist中節點和記憶體域的組織形式
     *  current_zonelist_order變數標識了當前系統的記憶體組織形式
     *  zonelist_order_name以字串儲存了系統中記憶體組織形式的名稱  */
    set_zonelist_order();

    if (system_state == SYSTEM_BOOTING) {
        build_all_zonelists_init();

4.1 system_state系統狀態標識

其中system_state變數是一個系統全域性定義的用來表示系統當前執行狀態的列舉變數, 其定義在include/linux/kernel.h?v=4.7, line 487

/* Values used for system_state */
extern enum system_states
{
    SYSTEM_BOOTING,
    SYSTEM_RUNNING,
    SYSTEM_HALT,
    SYSTEM_POWER_OFF,
    SYSTEM_RESTART,
} system_state;
  • 如果系統system_state是SYSTEM_BOOTING, 則呼叫build_all_zonelists_init初始化所有的記憶體結點
  • 否則的話如果定義了冷熱頁CONFIG_MEMORY_HOTPLUG且引數zone(待初始化的記憶體管理域zone)不為NULL, 則呼叫setup_zone_pageset設定冷熱頁
if (system_state == SYSTEM_BOOTING)
{
    build_all_zonelists_init();
}
else
{
#ifdef CONFIG_MEMORY_HOTPLUG
    if (zone)
        setup_zone_pageset(zone);
#endif

4.2 build_all_zonelists_init函式

build_all_zonelists函式在如果當前系統處於boot階段(system_state == SYSTEM_BOOTING), 就開始通過build_all_zonelists_init函式初始化zonelist

build_all_zonelists_init函式定義在mm/page_alloc.c?v=4.7, line 5013

static noinline void __init
build_all_zonelists_init(void)
{
    __build_all_zonelists(NULL);
    mminit_verify_zonelist();
    cpuset_init_current_mems_allowed();
}

build_all_zonelists_init將將所有工作都委託給__build_all_zonelists完成了zonelists的初始化工作, 後者又對系統中的各個NUMA結點分別呼叫build_zonelists.

函式__build_all_zonelists定義在mm/page_alloc.c?v=4.7, line 4959

/* return values int ....just for stop_machine() */
static int __build_all_zonelists(void *data)
{
    int nid;
    int cpu;
    pg_data_t *self = data;

    /*  ......  */

    for_each_online_node(nid) {
        pg_data_t *pgdat = NODE_DATA(nid);

        build_zonelists(pgdat);
    }
    /*  ......  */
}

for_each_online_node遍歷了系統中所有的活動結點.

由於UMA系統只有一個結點,build_zonelists只調用了一次, 就對所有的記憶體建立了記憶體域列表.

NUMA系統呼叫該函式的次數等同於結點的數目. 每次呼叫對一個不同結點生成記憶體域資料

4.3 build_zonelists初始化每個記憶體結點的zonelists

build_zonelists(pg_data_t *pgdat)完成了節點pgdat上zonelists的初始化工作, 它建立了備用層次結構zonelists. 由於UMA和NUMA架構下結點的層次結構有很大的區別, 因此核心分別提供了兩套不同的介面.

如下所示

// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7
4571 #ifdef CONFIG_NUMA

4586 static int __parse_numa_zonelist_order(char *s)

4601 static __init int setup_numa_zonelist_order(char *s)

4619 int numa_zonelist_order_handler(struct ctl_table *table, int write,
4620                 void __user *buffer, size_t *length,

4678 static int find_next_best_node(int node, nodemask_t *used_node_mask)

4730 static void build_zonelists_in_node_order(pg_data_t *pgdat, int node)

4746 static void build_thisnode_zonelists(pg_data_t *pgdat)

4765 static void build_zonelists_in_zone_order(pg_data_t *pgdat, int nr_nodes)

4789 #if defined(CONFIG_64BIT)

4795 static int default_zonelist_order(void)
4799 #else
4808 static int default_zonelist_order(void)
4812 #endif /* CONFIG_64BIT */

4822 static void build_zonelists(pg_data_t *pgdat)

4872 #ifdef CONFIG_HAVE_MEMORYLESS_NODES
4879 int local_memory_node(int node)
4888 #endif

4890 #else   /* CONFIG_NUMA */

4897 static void build_zonelists(pg_data_t *pgdat)

4892 static void set_zonelist_order(void)

4931 #endif  /* CONFIG_NUMA */

header 1 | header 2

row 1 col 1 | row 1 col 2
row 2 col 1 | row 2 col 2

函式 NUMA UMA
build_zonelists build_zonelists -=> mm/page_alloc.c?v=4.7, line 4822 build_zonelists -=> mm/page_alloc.c?v=4.7, line 4897
build_zonelists_node -=> mm/page_alloc.c?v=4.7, line 4531

我們以UMA結構下的build_zonelists為例, 來講講核心是怎麼初始化備用記憶體域層次結構的, UMA結構下的build_zonelists函式定義在mm/page_alloc.c?v=4.7, line 4897, 如下所示

node_zonelists的陣列元素通過指標操作定址, 這在C語言中是完全合法的慣例。實際工作則委託給build_zonelist_node。在呼叫時,它首先生成本地結點內分配記憶體時的備用次

核心在build_zonelists中按分配代價從昂貴到低廉的次序, 迭代了結點中所有的記憶體域. 而在build_zonelists_node中, 則按照分配代價從低廉到昂貴的次序, 迭代了分配代價不低於當前記憶體域的記憶體域.

首先我們來看看build_zonelists_node函式, 該函式定義在mm/page_alloc.c?v=4.7, line 4531

/*
 * Builds allocation fallback zone lists.
 *
 * Add all populated zones of a node to the zonelist.
 */
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;
}

備用列表zonelists的各項是藉助於zone_type引數排序的, 該引數指定了最優先選擇哪個記憶體域, 該引數的初始值是外層迴圈的控制變數i.

我們知道其值可能是ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA或ZONE_DMA32之一.

nr_zones表示從備用列表中的哪個位置開始填充新項. 由於列表中尚沒有項, 因此呼叫者傳遞了0.

核心在build_zonelists中按分配代價從昂貴到低廉的次序, 迭代了結點中所有的記憶體域. 而在build_zonelists_node中, 則按照分配代價從低廉到昂貴的次序, 迭代了分配代價不低於當前記憶體域的記憶體域.

在build_zonelists_node的每一步中, 都對所選的記憶體域呼叫populated_zone, 確認zone->present_pages大於0, 即確認記憶體域中確實有頁存在. 倘若如此, 則將指向zone例項的指標新增到zonelist->zones中的當前位置. 後備列表的當前位置儲存在nr_zones.

在每一步結束時, 都將記憶體域型別zone_type減1.換句話說, 設定為一個更昂貴的記憶體域型別. 例如, 如果開始的記憶體域是ZONE_HIGHMEM, 減1後下一個記憶體域型別是ZONE_NORMAL.

考慮一個系統, 有記憶體域ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA。在第一次執行build_zonelists_node時, 實際上會執行下列賦值

zonelist->zones[0] = ZONE_HIGHMEM;
zonelist->zones[1] = ZONE_NORMAL;
zonelist->zones[2] = ZONE_DMA;

我們以某個系統為例, 圖中示範了一個備用列表在多次迴圈中不斷填充的過程. 系統中共有四個結點

其中
A=(NUMA)結點0 0=DMA記憶體域
B=(NUMA)結點1 1=普通記憶體域
C=(NUMA)結點2 2=高階記憶體域
D=(NUMA)結點3

第一步之後, 列表中的分配目標是高階記憶體, 接下來是第二個結點的普通和DMA記憶體域.

核心接下來必須確立次序, 以便將系統中其他結點的記憶體域按照次序加入到備用列表.

現在我們回到build_zonelists函式, UMA架構下該函式定義在mm/page_alloc.c?v=4.7, line 4897, 如下所示

static void build_zonelists(pg_data_t *pgdat)
{
    int node, local_node;
    enum zone_type j;
    struct zonelist *zonelist;

    /*  ......  */

    for (node = local_node + 1; node < MAX_NUMNODES; node++) {
        if (!node_online(node))
            continue;
        j = build_zonelists_node(NODE_DATA(node), zonelist, j);
    }
    for (node = 0; node < local_node; node++) {
        if (!node_online(node))
            continue;
        j = build_zonelists_node(NODE_DATA(node), zonelist, j);
    }

    zonelist->_zonerefs[j].zone = NULL;
    zonelist->_zonerefs[j].zone_idx = 0;
}

第一個迴圈依次迭代大於當前結點編號的所有結點. 在我們的例子中,有4個結點編號副本為0、1、2、3,此時只剩下結點3。新的項通過build_zonelists_node被加到備用列表。此時j的作用就體現出來了。在本地結點的備用目標找到之後,該變數的值是3。該值用作新項的起始位置。如果結點3也由3個記憶體域組成,備用列表在第二個迴圈之後的情況如圖3-9的第二步所示

第二個for迴圈接下來對所有編號小於當前結點的結點生成備用列表項。在我們的例子中,這些結點的編號為0和1。 如果這些結點也有3個記憶體域,則迴圈完畢之後備用列表的情況如下圖下半部分所示

備用列表中項的數目一般無法準確知道,因為系統中不同結點的記憶體域配置可能並不相同。因此 列表的最後一項賦值為空指標,顯式標記列表結束。

對總數N個結點中的結點m來說,核心生成備用列表時,選擇備用結點的順序總是:m、m+1、 m+2、…、N1、0、1、…、m1。這確保了不過度使用任何結點。例如,對照情況是:使用一個獨立 於m、不變的備用列表

4.4 setup_pageset初始化per_cpu快取

前面講解記憶體管理域zone的時候, 提到了per-CPU快取, 即冷熱頁. 在組織每個節點的zonelist的過程中, setup_pageset初始化了per-CPU快取(冷熱頁面)

static void setup_pageset(struct per_cpu_pageset *p, unsigned long batch)
{
    pageset_init(p);
    pageset_set_batch(p, batch);
}

在此之前free_area_init_node初始化記憶體結點的時候, 核心就輸出了冷熱頁的一些資訊, 該工作由zone_pcp_init完成, 該函式定義在mm/page_alloc.c?v=4.7, line 5029

static __meminit void zone_pcp_init(struct zone *zone)
{
    /*
     * per cpu subsystem is not up at this point. The following code
     * relies on the ability of the linker to provide the
     * offset of a (static) per cpu variable into the per cpu area.
     */
    zone->pageset = &boot_pageset;

    if (populated_zone(zone))
        printk(KERN_DEBUG "  %s zone: %lu pages, LIFO batch:%u\n",
            zone->name, zone->present_pages,
                     zone_batchsize(zone));
}

5 總結

5.1 start_kernel啟動流程

start_kernel()
    |---->page_address_init()
    |     考慮支援高階記憶體
    |     業務:初始化page_address_pool連結串列;
    |          將page_address_maps陣列元素按索引降序插入
    |          page_address_pool連結串列; 
    |          初始化page_address_htable陣列.
    | 
    |---->setup_arch(&command_line);
    |
    |---->setup_per_cpu_areas();
    |     為per-CPU變數分配空間
    |
    |---->build_all_zonelist()
    |     為系統中的zone建立後備zone的列表.
    |     所有zone的後備列表都在
    |     pglist_data->node_zonelists[0]中;
    |
    |     期間也對per-CPU變數boot_pageset做了初始化. 
    |
    |---->page_alloc_init()
         |---->hotcpu_notifier(page_alloc_cpu_notifier, 0);
         |     不考慮熱插拔CPU 
         |
    |---->pidhash_init()
    |     詳見下文.
    |     根據低端記憶體頁數和雜湊度,分配hash空間,並賦予pid_hash
    |
    |---->vfs_caches_init_early()
          |---->dcache_init_early()
          |     dentry_hashtable空間,d_hash_shift, h_hash_mask賦值;
          |     同pidhash_init();
          |     區別:
          |         雜湊度變化了(13 - PAGE_SHIFT);
          |         傳入alloc_large_system_hash的最後引數值為0;
          |
          |---->inode_init_early()
          |     inode_hashtable空間,i_hash_shift, i_hash_mask賦值;
          |     同pidhash_init();
          |     區別:
          |         雜湊度變化了(14 - PAGE_SHIFT);
          |         傳入alloc_large_system_hash的最後引數值為0;
          |

5.2 pidhash_init配置高階記憶體

void pidhash_init(void)
    |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 
    |         0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096);
    |     根據nr_kernel_pages(低端記憶體的頁數),分配雜湊陣列,以及各個雜湊
    |     陣列元素下的雜湊連結串列的空間,原理如下:
    |     number = nr_kernel_pages; 
    |     number >= (18 - PAGE_SHIFT) 根據雜湊度獲得陣列元素個數
    |     number = roundup_pow_of_two(number);
    |     pidhash_shift = max{x | 2**x <= number}
    |     size = number * sizeof(*pid_hash);
    |     使用點陣圖分配器分配size空間,將返回值付給pid_hash;
    |
    |---->pidhash_size = 1 << pidhash_shift;
    |
    |---->for(i = 0; i < pidhash_size; i++)
    |         INIT_HLIST_HEAD(&pid_hash[i]);

5.3 build_all_zonelists初始化每個記憶體節點的zonelists

void build_all_zonelists(void)
    |---->set_zonelist_order()
         |---->current_zonelist_order = ZONELIST_ORDER_ZONE;
    |
    |---->__build_all_zonelists(NULL);
    |    Memory不支援熱插拔, 為每個zone建立後備的zone,
    |    每個zone及自己後備的zone,形成zonelist
        |
        |---->pg_data_t *pgdat = NULL;
        |     pgdat = &contig_page_data;(單node)
        |
        |---->build_zonelists(pgdat);
        |     為每個zone建立後備zone的列表
            |
            |---->struct zonelist *zonelist = NULL;
            |     enum zone_type j;
            |     zonelist = &pgdat->node_zonelists[0];
            |
            |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1);
            |     為pgdat->node_zones[0]建立後備的zone,node_zones[0]後備的zone
            |     儲存在node_zonelist[0]內,對於node_zone[0]的後備zone,其後備的zone
            |     連結串列如下(只考慮UMA體系,而且不考慮ZONE_DMA):
            |     node_zonelist[0]._zonerefs[0].zone = &node_zones[2];
            |     node_zonelist[0]._zonerefs[0].zone_idx = 2;
            |     node_zonelist[0]._zonerefs[1].zone = &node_zones[1];
            |     node_zonelist[0]._zonerefs[1].zone_idx = 1;
            |     node_zonelist[0]._zonerefs[2].zone = &node_zones[0];
            |     node_zonelist[0]._zonerefs[2].zone_idx = 0;
            |
            |     zonelist->_zonerefs[3].zone = NULL;
            |     zonelist->_zonerefs[3].zone_idx = 0;
        |
        |---->build_zonelist_cache(pgdat);
              |---->pdat->node_zonelists[0].zlcache_ptr = NULL;
              |     UMA體系結構
              |
        |---->for_each_possible_cpu(cpu)
        |     setup_pageset(&per_cpu(boot_pageset, cpu), 0);
              |詳見下文
    |---->vm_total_pages = nr_free_pagecache_pages();
    |    業務:獲得所有zone中的present_pages總和.
    |
    |---->page_group_by_mobility_disabled = 0;
    |     對於程式碼中的判斷條件一般不會成立,因為頁數會最夠多(記憶體較大)