1. 程式人生 > >啟動期間的記憶體管理之bootmem_init初始化記憶體管理–Linux記憶體管理(十二)

啟動期間的記憶體管理之bootmem_init初始化記憶體管理–Linux記憶體管理(十二)

1. 啟動過程中的記憶體初始化

首先我們來看看start_kernel是如何初始化系統的, start_kerne定義在init/main.c?v=4.7, line 479

其程式碼很複雜, 我們只截取出其中與記憶體管理初始化相關的部分, 如下所示

asmlinkage __visible void __init start_kernel(void)
{

    setup_arch(&command_line);
    mm_init_cpumask(&init_mm);

    setup_per_cpu_areas();


    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();
}
函式 功能
setup_arch 是一個特定於體系結構的設定函式, 其中一項任務是負責初始化自舉分配器
mm_init_cpumask 初始化CPU遮蔽字
setup_per_cpu_areas 函式(檢視定義)給每個CPU分配記憶體,並拷貝.data.percpu段的資料. 為系統中的每個CPU的per_cpu變數申請空間.
在SMP系統中, setup_per_cpu_areas初始化原始碼中(使用per_cpu巨集)定義的靜態per-cpu變數, 這種變數對系統中每個CPU都有一個獨立的副本.
此類變數儲存在核心二進位制影像的一個獨立的段中, setup_per_cpu_areas的目的就是為系統中各個CPU分別建立一份這些資料的副本
在非SMP系統中這是一個空操作
build_all_zonelists 建立並初始化結點和記憶體域的資料結構
mm_init 建立了核心的記憶體分配器,
其中通過mem_init停用bootmem分配器並遷移到實際的記憶體管理器(比如夥伴系統)
然後呼叫kmem_cache_init函式初始化核心內部用於小塊記憶體區的分配器
kmem_cache_init_late 在kmem_cache_init之後, 完善分配器的快取機制, 當前3個可用的核心記憶體分配器slab, slob, slub都會定義此函式 
kmemleak_init
Kmemleak工作於核心態,Kmemleak 提供了一種可選的核心洩漏檢測,其方法類似於跟蹤記憶體收集器。當獨立的物件沒有被釋放時,其報告記錄在 /sys/kernel/debug/kmemleak中, Kmemcheck能夠幫助定位大多數記憶體錯誤的上下文
setup_per_cpu_pageset 初始化CPU快取記憶體行, 為pagesets的第一個陣列元素分配記憶體, 換句話說, 其實就是第一個系統處理器分配
由於在分頁情況下,每次儲存器訪問都要存取多級頁表,這就大大降低了訪問速度。所以,為了提高速度,在CPU中設定一個最近存取頁面的快取記憶體硬體機制,當進行儲存器訪問時,先檢查要訪問的頁面是否在快取記憶體中.

1.1 setup_arch函式初始化記憶體流程

前面我們的核心從start_kernel開始, 進入setup_arch(), 並完成了早期記憶體分配器的初始化和設定工作.

void __init setup_arch(char **cmdline_p)
{
    /*  初始化memblock  */
    arm64_memblock_init( );

    /*  分頁機制初始化  */
    paging_init();

    bootmem_init();
}
流程 描述
arm64_memblock_init 初始化memblock記憶體分配器
paging_init 初始化分頁機制
bootmem_init 初始化記憶體管理

該函式主要執行了如下操作

  1. 使用arm64_memblock_init來完成memblock機制的初始化工作, 至此memblock分配器接受系統中系統中記憶體的分配工作
  2. 呼叫paging_init來完成系統分頁機制的初始化工作, 建立頁表, 從而核心可以完成虛擬記憶體的對映和轉換工作
  3. 最後呼叫bootmem_init來完成實現buddy記憶體管理所需要的工作

1.2 (第一階段)啟動過程中的記憶體分配器

在初始化過程中, 還必須建立記憶體管理的資料結構, 以及很多事務. 因為核心在記憶體管理完全初始化之前就需要使用記憶體. 在系統啟動過程期間, 使用了額外的簡化悉尼股市的記憶體管理模組, 然後在初始化完成後, 將舊的模組丟棄掉.

這個階段的記憶體分配其實很簡單, 因此我們往往稱之為記憶體分配器(而不是記憶體管理器), 早期的核心中記憶體分配器使用的bootmem引導分配器, 它基於一個記憶體點陣圖bitmap, 使用最優適配演算法來查詢記憶體, 但是這個分配器有很大的缺陷, 最嚴重的就是記憶體碎片的問題, 因此在後來的核心中將其捨棄《而使用了新的memblock機制. memblock機制的初始化在arm64上是通過arm64_memblock_init函式來實現的

start_kernel()
    |---->page_address_init()
    |     考慮支援高階記憶體
    |     業務:初始化page_address_pool連結串列;
    |          將page_address_maps陣列元素按索引降序插入
    |          page_address_pool連結串列; 
    |          初始化page_address_htable陣列.
    | 
    |---->setup_arch(&command_line);
    |     初始化特定體系結構的內容
          |
          |---->arm64_memblock_init( );
          |     初始化引導階段的記憶體分配器memblock
          |
          |---->paging_init();
          |     分頁機制初始化
          |
          |---->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的保留的所有記憶體資訊
               |
    |---->build_all_zonelist()
    |     為系統中的zone建立後備zone的列表.
    |     所有zone的後備列表都在
    |     pglist_data->node_zonelists[0]中;
    |
    |     期間也對per-CPU變數boot_pageset做了初始化. 
    |

1.3 今日內容(第二階段(一)--初始化記憶體管理資料結構)

我們之前講了在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的保留的所有記憶體資訊

建立記憶體管理的資料結構

對相關資料結構的初始化是從全域性啟動函式start_kernel中開始的, 該函式在載入核心並激活各個子系統之後執行. 由於記憶體管理是核心一個非常重要的部分, 因此在特定體系結構的設定步驟中檢測並確定系統中記憶體的分配情況後, 會立即執行記憶體管理的初始化.

移交早期的分配器到記憶體管理器

最後我們的記憶體管理器已經初始化並設定完成, 可以投入運行了, 因此核心將記憶體管理的工作從早期的記憶體分配器(bootmem或者memblock)移交到我們的buddy夥伴系統.

2 初始化前的準備工作

2.1 回到setup_arch函式(當前已經完成的工作)

現在我們回到start_kernel()->setup_arch()函式

void __init setup_arch(char **cmdline_p)
{
    /*  初始化memblock  */
    arm64_memblock_init( );

    /*  分頁機制初始化  */
    paging_init();

    bootmem_init();
}

到目前位置我們已經完成了如下工作

  • memblock已經通過arm64_memblock_init完成了初始化, 至此係統中的記憶體可以通過memblock分配了

  • paging_init完成了分頁機制的初始化, 至此核心已經佈局了一套完整的虛擬記憶體空間

至此我們所有的記憶體都可以通過memblock機制來分配和釋放, 儘管它實現的笨拙而簡易, 但是已經足夠我們初始化階段使用了, 反正核心頁不可能指著它過一輩子, 而我們也通過pagging_init建立了頁表, 為核心提供了一套可供核心和程序執行的虛擬執行空間, 我們可以安全的進行記憶體的分配了

因此該是時候初始化我們強大的buddy系統了.

核心接著setup_arch()->bootmem_init()函式開始執行

體系結構相關的程式碼需要在啟動期間建立如下資訊

  • 系統中各個記憶體域的頁幀邊界,儲存在max_zone_pfn陣列

早期的核心還需記錄各結點頁幀的分配情況,儲存在全域性變數early_node_map中

image

核心提供了一個通用的框架, 用於將上述資訊轉換為夥伴系統預期的節點和記憶體域資料結構, 但是在此之前各個體系結構必須自行建立相關結構.

2.2 bootmem_init函式初始化記憶體結點和管理域

arm64架構下, 在setup_arch中通過paging_init函式初始化核心分頁機制之後, 核心通過bootmem_init()開始完成記憶體結點和記憶體區域的初始化工作, 該函式定義在arch/arm64/mm/init.c, line 306

void __init bootmem_init(void)
{
    unsigned long min, max;

    min = PFN_UP(memblock_start_of_DRAM());
    max = PFN_DOWN(memblock_end_of_DRAM());

    early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);

    max_pfn = max_low_pfn = max;

    arm64_numa_init();
    /*
     * Sparsemem tries to allocate bootmem in memory_present(), so must be
     * done after the fixed reservations.
     */
    arm64_memory_present();

    sparse_init();
    zone_sizes_init(min, max);

    high_memory = __va((max << PAGE_SHIFT) - 1) + 1;
    memblock_dump_all();
}

2.3 zone_sizes_init函式

在初始化記憶體結點和記憶體域之前, 核心首先通過setup_arch()-->bootmem_init()-->zone_sizes_init()來初始化節點和管理區的一些資料項, 其中關鍵的是初始化了系統中各個記憶體域的頁幀邊界,儲存在max_zone_pfn陣列.

[zone_sizes_init](zone_sizes_init函式定義在arch/arm64/mm/init.c?v=4.7, line 92, 由於arm64支援NUMA和UMA兩種儲存器架構, 因此該函式依照NUMA和UMA, 有兩種不同的實現.函式定義在arch/arm64/mm/init.c?v=4.7, line 92, 由於arm64支援NUMA和UMA兩種儲存器架構, 因此該函式依照NUMA和UMA, 有兩種不同的實現.

#ifdef CONFIG_NUMA

static void __init zone_sizes_init(unsigned long min, unsigned long max)
{
    unsigned long max_zone_pfns[MAX_NR_ZONES]  = {0};

    if (IS_ENABLED(CONFIG_ZONE_DMA))
        max_zone_pfns[ZONE_DMA] = PFN_DOWN(max_zone_dma_phys());
    max_zone_pfns[ZONE_NORMAL] = max;

    free_area_init_nodes(max_zone_pfns);
}

#else

static void __init zone_sizes_init(unsigned long min, unsigned long max)
{
    struct memblock_region *reg;
    unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];
    unsigned long max_dma = min;

    memset(zone_size, 0, sizeof(zone_size));

    /* 4GB maximum for 32-bit only capable devices */
#ifdef CONFIG_ZONE_DMA
    max_dma = PFN_DOWN(arm64_dma_phys_limit);
    zone_size[ZONE_DMA] = max_dma - min;
#endif
    zone_size[ZONE_NORMAL] = max - max_dma;

    memcpy(zhole_size, zone_size, sizeof(zhole_size));

    for_each_memblock(memory, reg) {
        unsigned long start = memblock_region_memory_base_pfn(reg);
        unsigned long end = memblock_region_memory_end_pfn(reg);

        if (start >= max)
            continue;

#ifdef CONFIG_ZONE_DMA
        if (start < max_dma) {
            unsigned long dma_end = min(end, max_dma);
            zhole_size[ZONE_DMA] -= dma_end - start;
        }
#endif
        if (end > max_dma) {
            unsigned long normal_end = min(end, max);
            unsigned long normal_start = max(start, max_dma);
            zhole_size[ZONE_NORMAL] -= normal_end - normal_start;
        }
    }

    free_area_init_node(0, zone_size, min, zhole_size);
}

#endif /* CONFIG_NUMA */

在獲取了三個管理區的頁面數後, NUMA架構下通過free_area_init_nodes()來完成後續工作, 其中核心函式為free_area_init_node(),用來針對特定的節點進行初始化, 由於UMA架構下只有一個記憶體結點, 因此直接通過free_area_init_node來完成記憶體結點的初始化

截至到目前為止, 體系結構相關的部分已經結束了, 各個體系結構已經自行建立了自己所需的一些底層資料結構, 這些結構建立好以後, 核心將繁重的記憶體資料結構建立和初始化的工作交給free_area_init_node(s)函式來完成,

3 free_area_init_nodes初始化NUMA管理資料結構

注意

此部分內容參照

Linux記憶體管理夥伴演算法

linux 記憶體管理 - paging_init 函式

free_area_init_nodes初始化了NUMA系統中所有結點的pg_data_t和zone、page的資料, 並列印了管理區資訊, 該函式定義在mm/page_alloc.c?v=4.7, line 6460

3.1 程式碼註釋

//  初始化各個節點的所有pg_data_t和zone、page的資料
void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
    unsigned long start_pfn, end_pfn;
    int i, nid;

    /* Record where the zone boundaries are
     * 全域性陣列arch_zone_lowest_possible_pfn
     * 用來儲存各個記憶體域可使用的最低記憶體頁幀編號   */
    memset(arch_zone_lowest_possible_pfn, 0,
                sizeof(arch_zone_lowest_possible_pfn));

    /* 全域性陣列arch_zone_highest_possible_pfn
     * 用來儲存各個記憶體域可使用的最高記憶體頁幀編號   */
    memset(arch_zone_highest_possible_pfn, 0,
                sizeof(arch_zone_highest_possible_pfn));

    /* 輔助函式find_min_pfn_with_active_regions
     * 用於找到註冊的最低記憶體域中可用的編號最小的頁幀 */
    arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions();

    /*  max_zone_pfn記錄了各個記憶體域包含的最大頁幀號  */
    arch_zone_highest_possible_pfn[0] = max_zone_pfn[0];

    /*  依次遍歷,確定各個記憶體域的邊界    */
    for (i = 1; i < MAX_NR_ZONES; i++) {
        /*  由於ZONE_MOVABLE是一個虛擬記憶體域
         *  不與真正的硬體記憶體域關聯
         *  該記憶體域的邊界總是設定為0 */
        if (i == ZONE_MOVABLE)
            continue;
        /*  第n個記憶體域的最小頁幀
         *  即前一個(第n-1個)記憶體域的最大頁幀  */
        arch_zone_lowest_possible_pfn[i] =
            arch_zone_highest_possible_pfn[i-1];
        /*  不出意外,當前記憶體域的最大頁幀
         *  由max_zone_pfn給出  */
        arch_zone_highest_possible_pfn[i] =
            max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
    }
    arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
    arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;

    /* Find the PFNs that ZONE_MOVABLE begins at in each node */
    memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
    /*  用於計算進入ZONE_MOVABLE的記憶體數量  */
    find_zone_movable_pfns_for_nodes();

    /* Print out the zone ranges
     * 將各個記憶體域的最大、最小頁幀號顯示出來  */
    pr_info("Zone ranges:\n");
    for (i = 0; i < MAX_NR_ZONES; i++) {
        if (i == ZONE_MOVABLE)
            continue;
        pr_info("  %-8s ", zone_names[i]);
        if (arch_zone_lowest_possible_pfn[i] ==
                arch_zone_highest_possible_pfn[i])
            pr_cont("empty\n");
        else
            pr_cont("[mem %#018Lx-%#018Lx]\n",
                (u64)arch_zone_lowest_possible_pfn[i]
                    << PAGE_SHIFT,
                ((u64)arch_zone_highest_possible_pfn[i]
                    << PAGE_SHIFT) - 1);
    }

    /* Print out the PFNs ZONE_MOVABLE begins at in each node */
    pr_info("Movable zone start for each node\n");
    for (i = 0; i < MAX_NUMNODES; i++) {
        /*  對每個結點來說,zone_movable_pfn[node_id]
         *  表示ZONE_MOVABLE在movable_zone記憶體域中所取得記憶體的起始地址
         *  核心確保這些頁將用於滿足符合ZONE_MOVABLE職責的記憶體分配 */
        if (zone_movable_pfn[i])
        {
            /*  顯示各個記憶體域的分配情況  */
            pr_info("  Node %d: %#018Lx\n", i,
                   (u64)zone_movable_pfn[i] << PAGE_SHIFT);
        }
    }

    /* Print out the early node map */
    pr_info("Early memory node ranges\n");
    for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid)
        pr_info("  node %3d: [mem %#018Lx-%#018Lx]\n", nid,
            (u64)start_pfn << PAGE_SHIFT,
            ((u64)end_pfn << PAGE_SHIFT) - 1);

    /* Initialise every node */
    mminit_verify_pageflags_layout();
    setup_nr_node_ids();

    /*  程式碼遍歷所有的活動結點,
     *  並分別對各個結點呼叫free_area_init_node建立資料結構,
     *  該函式需要結點第一個可用的頁幀作為一個引數,
     *  而find_min_pfn_for_node則從early_node_map陣列提取該資訊   */
    for_each_online_node(nid) {
        pg_data_t *pgdat = NODE_DATA(nid);
        free_area_init_node(nid, NULL,
                find_min_pfn_for_node(nid), NULL);

        /* Any memory on that node
         * 根據node_present_pages欄位判斷結點具有記憶體
         * 則在結點點陣圖中設定N_HIGH_MEMORY標誌
         * 該標誌只表示結點上存在普通或高階記憶體
         * 因此check_for_regular_memory
         * 進一步檢查低於ZONE_HIGHMEM的記憶體域中是否有記憶體
         * 並據此在結點點陣圖中相應地設定N_NORMAL_MEMORY   */
        if (pgdat->node_present_pages)
            node_set_state(nid, N_MEMORY);
        check_for_memory(pgdat, nid);
    }
}

3.2 設定可使用的頁幀編號

free_area_init_nodes首先必須分析並改寫特定於體系結構的程式碼提供的資訊。其中,需要對照在zone_max_pfn和zone_min_pfn中指定的記憶體域的邊界,計算各個記憶體域可使用的最低和最高的頁幀編號。使用了兩個全域性陣列來儲存這些資訊:

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

static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES];
static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];

通過max_zone_pfn傳遞給free_area_init_nodes的資訊記錄了各個記憶體域包含的最大頁幀號。 free_area_init_nodes將該資訊轉換為一種更方便的表示形式,即以[low, high]形式描述各個內 存域的頁幀區間,儲存在前述的全域性變數中(我省去了對這些變數填充位元組0的初始化過程):

void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
    /*  ......  */
    arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
    arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;

    /* Find the PFNs that ZONE_MOVABLE begins at in each node */
    memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
    /*  用於計算進入ZONE_MOVABLE的記憶體數量  */
    find_zone_movable_pfns_for_nodes();
    /*  依次遍歷,確定各個記憶體域的邊界    */
    for (i = 1; i < MAX_NR_ZONES; i++) {
        /*  由於ZONE_MOVABLE是一個虛擬記憶體域
         *  不與真正的硬體記憶體域關聯
         *  該記憶體域的邊界總是設定為0 */
        if (i == ZONE_MOVABLE)
            continue;
        /*  第n個記憶體域的最小頁幀
         *  即前一個(第n-1個)記憶體域的最大頁幀  */
        arch_zone_lowest_possible_pfn[i] =
            arch_zone_highest_possible_pfn[i-1];
        /*  不出意外,當前記憶體域的最大頁幀
         *  由max_zone_pfn給出  */
        arch_zone_highest_possible_pfn[i] =
            max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
    }

    /*  ......  */
}

輔助函式find_min_pfn_with_active_regions用於找到註冊的最低記憶體域中可用的編號最小的頁幀。該記憶體域不必一定是ZONE_DMA,例如,在計算機不需要DMA記憶體的情況下也可以是ZONE_NORMAL。最低記憶體域的最大頁幀號可以從max_zone_pfn提供的資訊直接獲得。

3.3 構建其他記憶體域的頁幀區間

接下來構建其他記憶體域的頁幀區間,方法很直接:第n個記憶體域的最小頁幀,即前一個(第n-1個)記憶體域的最大頁幀。當前記憶體域的最大頁幀由max_zone_pfn給出

void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
    /*  ......  */

    arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
    arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;

    /* Find the PFNs that ZONE_MOVABLE begins at in each node */
    memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
    /*  用於計算進入ZONE_MOVABLE的記憶體數量  */
    find_zone_movable_pfns_for_nodes();

    /*  ......  */
}

由於ZONE_MOVABLE是一個虛擬記憶體域,不與真正的硬體記憶體域關聯,該記憶體域的邊界總是設定為0。回憶前文,可知只有在指定了核心命令列引數kernelcore或movablecore之一時,該記憶體域才會存在. 該記憶體域一般開始於各個結點的某個特定記憶體域的某一頁幀號。相應的編號在find_zone_movable_pfns_for_nodes裡計算。

現在可以向用戶提供一些有關已確定的頁幀區間的資訊。舉例來說,其中可能包括下列內容(輸出取自AMD64系統,有4 GiB實體記憶體):

> dmesg

Zone PFN ranges:
DMA 0 0 -> 4096
DMA32 4096 -> 1048576
Normal 1048576 -> 1245184

3.4 建立結點資料結構

free_area_init_nodes剩餘的部分遍歷所有結點,分別建立其資料結構

void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
    /*  輸出有關記憶體域的資訊  */
    /*  ......  */

    /*  程式碼遍歷所有的活動結點,
     *  並分別對各個結點呼叫free_area_init_node建立資料結構,
     *  該函式需要結點第一個可用的頁幀作為一個引數,
     *  而find_min_pfn_for_node則從early_node_map陣列提取該資訊   */
    for_each_online_node(nid) {
        pg_data_t *pgdat = NODE_DATA(nid);
        free_area_init_node(nid, NULL,
                find_min_pfn_for_node(nid), NULL);

        /* Any memory on that node
         * 根據node_present_pages欄位判斷結點具有記憶體
         * 則在結點點陣圖中設定N_HIGH_MEMORY標誌
         * 該標誌只表示結點上存在普通或高階記憶體
         * 因此check_for_regular_memory
         * 進一步檢查低於ZONE_HIGHMEM的記憶體域中是否有記憶體
         * 並據此在結點點陣圖中相應地設定N_NORMAL_MEMORY   */
        if (pgdat->node_present_pages)
            node_set_state(nid, N_MEMORY);
        check_for_memory(pgdat, nid);
    }

    /*  ......  */
}

程式碼遍歷所有活動結點,並分別對各個結點呼叫free_area_init_node建立資料結構。該函式需要結點第一個可用的頁幀作為一個引數,而find_min_pfn_for_node則從early_node_map陣列提取該資訊。

如果根據node_present_pages欄位判斷結點具有記憶體,則在結點點陣圖中設定N_HIGH_MEMORY標誌。我們知道該標誌只表示結點上存在普通或高階記憶體,因此check_for_regular_memory進一步檢查低於ZONE_HIGHMEM的記憶體域中是否有記憶體,並據此在結點點陣圖中相應地設定N_NORMAL_MEMORY標誌

4 free_area_init_node初始化UMA記憶體結點

free_area_init_nodes函式初始化所有結點的pg_data_t和zone、page的資料,並列印了管理區資訊.

4.1 free_area_init_node函式註釋

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

void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
        unsigned long node_start_pfn, unsigned long *zholes_size)
{
    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 */
    WARN_ON(pgdat->nr_zones || pgdat->classzone_idx);

    reset_deferred_meminit(pgdat);
    pgdat->node_id = nid;
    pgdat->node_start_pfn = node_start_pfn;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
    get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);
    pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid,
        (u64)start_pfn << PAGE_SHIFT,
        end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0);
#else
    start_pfn = node_start_pfn;
#endif
    /*  首先累計各個記憶體域的頁數
     *  計算結點中頁的總數
     *  對連續記憶體模型而言
     *  這可以通過zone_sizes_init完成
     *  但calculate_node_totalpages還考慮了記憶體空洞 */
    calculate_node_totalpages(pgdat, start_pfn, end_pfn,
                  zones_size, zholes_size);
    /*  分配了該節點的頁面描述符陣列
     *  [pgdat->node_mem_map陣列的記憶體分配  */
    alloc_node_mem_map(pgdat);
#ifdef CONFIG_FLAT_NODE_MEM_MAP
    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

    /*  對該節點的每個區[DMA,NORMAL,HIGH]的的結構進行初始化  */
    free_area_init_core(pgdat);
}

4.2 流程分析

  • calculate_node_totalpages函式累計各個記憶體域的頁數,計算結點中頁的總數。對連續記憶體模型而言,這可以通過zone_sizes_init完成,但calculate_node_totalpages還考慮了記憶體空洞,該函式定義在mm/page_alloc.c, line 5789

以下例子取自一個UMA系統, 具有512 MiB實體記憶體。

> dmesg
...
On node 0 totalpages: 131056
  • alloc_node_mem_map(pgdat)函式分配了該節點的頁面描述符陣列[pgdat->node_mem_map陣列的記憶體分配.
  • 繼續呼叫free_area_init_core函式,繼續初始化該節點的pg_data_t結構,初始化zone以及page結構 , free_area_init_core函式是初始化zone的核心

4.3 alloc_node_mem_map函式

alloc_node_mem_map負責初始化一個簡單但非常重要的資料結構。如上所述,系統中的各個實體記憶體頁,都對應著一個struct page例項。該結構的初始化由alloc_node_mem_map執行

static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat)
{
    unsigned long __maybe_unused start = 0;
    unsigned long __maybe_unused offset = 0;

    /* Skip empty nodes */
    if (!pgdat->node_spanned_pages)
        return;

#ifdef CONFIG_FLAT_NODE_MEM_MAP
    start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
    offset = pgdat->node_start_pfn - start;
    /* ia64 gets its own node_mem_map, before this, without bootmem */
    if (!pgdat->node_mem_map) {
        unsigned long size, end;
        struct page *map;

        /*
         * The zone's endpoints aren't required to be MAX_ORDER
         * aligned but the node_mem_map endpoints must be in order
         * for the buddy allocator to function correctly.
         */
        end = pgdat_end_pfn(pgdat);
        end = ALIGN(end, MAX_ORDER_NR_PAGES);
        size =  (end - start) * sizeof(struct page);
        map = alloc_remap(pgdat->node_id, size);
        if (!map)
            map = memblock_virt_alloc_node_nopanic(size,
                                   pgdat->node_id);
        pgdat->node_mem_map = map + offset;
    }
#ifndef CONFIG_NEED_MULTIPLE_NODES
    /*
     * With no DISCONTIG, the global mem_map is just set as node 0's
     */
    if (pgdat == NODE_DATA(0)) {
        mem_map = NODE_DATA(0)->node_mem_map;
#if defined(CONFIG_HAVE_MEMBLOCK_NODE_MAP) || defined(CONFIG_FLATMEM)
        if (page_to_pfn(mem_map) != pgdat->node_start_pfn)
            mem_map -= offset;
#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */
    }
#endif
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}

沒有頁的空結點顯然可以跳過。如果特定於體系結構的程式碼尚未建立記憶體對映(這是可能的,例如,在IA-64系統上),則必須分配與該結點關聯的所有struct page例項所需的記憶體。各個體系結構可以為此提供一個特定的函式。但目前只有在IA-32系統上使用不連續記憶體配置時是這樣。在所有其他的配置上,則使用普通的自舉記憶體分配器進行分配。請注意,程式碼將記憶體對映對齊到夥伴系統的最大分配階,因為要使所有的計算都工作正常,這是必需的。

指向該空間的指標不僅儲存在pglist_data例項中,還儲存在全域性變數mem_map中,前提是當前考察的結點是系統的第0個結點(如果系統只有一個記憶體結點,則總是這樣)。mem_map是一個全域性陣列,在講解記憶體管理時,我們會經常遇到, 定義在mm/memory.c?v=4.7, line 85

struct page *mem_map;

然後在free_area_init_node函式的最後, 通過free_area_init_core來完成記憶體域zone的初始化

5 free_area_init_core初始化記憶體域zone

初始化記憶體域資料結構涉及的繁重工作由free_area_init_core執行,它會依次遍歷結點的所有記憶體域, 該函式定義在mm/page_alloc.c?v=4.7, line 5932

5.1 free_area_init_core函式程式碼註釋

/*
 * Set up the zone data structures:
 *   - mark all pages reserved
 *   - mark all memory queues empty
 *   - clear the memory bitmaps
 *
 * NOTE: pgdat should get zeroed by caller.
 */
static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
    enum zone_type j;
    int nid = pgdat->node_id;
    int ret;

    /*  初始化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
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
    spin_lock_init(&pgdat->split_queue_lock);
    INIT_LIST_HEAD(&pgdat->split_queue);
    pgdat->split_queue_len = 0;
#endif

    /*  初始化pgdat->kswapd_wait等待佇列  */
    init_waitqueue_head(&pgdat->kswapd_wait);
    /*  初始化頁換出守護程序建立空閒塊的大小
     *  為2^kswapd_max_order  */
    init_waitqueue_head(&pgdat->pfmemalloc_wait);
#ifdef CONFIG_COMPACTION
    init_waitqueue_head(&pgdat->kcompactd_wait);
#endif
    pgdat_page_ext_init(pgdat);

    /* 遍歷每個管理區 */
    for (j = 0; j < MAX_NR_ZONES; j++) {
        struct zone *zone = pgdat->node_zones + j;
        unsigned long size, realsize, freesize, memmap_pages;
        unsigned long zone_start_pfn = zone->zone_start_pfn;

        /*  size為該管理區中的頁框數,包括洞 */
        size = zone->spanned_pages;
         /* realsize為管理區中的頁框數,不包括洞  /
        realsize = freesize = zone->present_pages;

        /*
         * 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
         * 調整realsize的大小,即減去page結構體佔用的記憶體大小  */
        /*  memmap_pags為包括洞的所有頁框的page結構體所佔的大小  */
        memmap_pages = calc_memmap_size(size, realsize);
        if (!is_highmem_idx(j)) {
            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  /*  記憶體不夠存放page結構體  */
                pr_warn("  %s zone: %lu pages exceeds freesize %lu\n",
                    zone_names[j], memmap_pages, freesize);
        }

        /* Account for reserved pages
         * 調整realsize的大小,即減去DMA保留頁的大小  */
        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;

        /*
         * 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->spanned_pages為包括洞的頁框數  */
        zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
#ifdef CONFIG_NUMA
        /* 設定zone中的節點識別符號 */
        zone->node = nid;
        /* 設定可回收頁面比率 */
        zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio)
                        / 100;
        /* 設定slab回收快取頁的比率 */
        zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100;
#endif
        /*  設定zone的名稱  */
        zone->name = zone_names[j];

        /* 初始化各種鎖 */
        spin_lock_init(&zone->lock);
        spin_lock_init(&zone->lru_lock);
        zone_seqlock_init(zone);
        /* 設定管理區屬於的節點對應的pg_data_t結構 */
        zone->zone_pgdat = pgdat;
        /* 初始化cpu的頁面快取 */
        zone_pcp_init(zone);

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

        /* 初始化lru相關成員 */
        lruvec_init(&zone->lruvec);
        if (!size)
            continue;

        set_pageblock_order();
        /* 定義了CONFIG_SPARSEMEM該函式為空 */
        setup_usemap(pgdat, zone, zone_start_pfn, size);
        /* 設定pgdat->nr_zones和zone->zone_start_pfn成員
         * 初始化zone->free_area成員
         * 初始化zone->wait_table相關成員
         */
         ret = init_currently_empty_zone(zone, zone_start_pfn, size);
        BUG_ON(ret);
        /* 初始化該zone對應的page結構 */
        memmap_init(size, nid, j, zone_start_pfn);
    }
    /*  ......  */
}

5.2 流程講解

初始化記憶體域資料結構涉及的繁重工作由free_area_init_core執行,它會依次遍歷結點的所有記憶體域

static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
    enum zone_type j;
    int nid = pgdat->node_id;
    int ret;

    /*  ......  */
    /* 遍歷每個管理區 */
    for (j = 0; j < MAX_NR_ZONES; j++) {
        struct zone *zone = pgdat->node_zones + j;
        unsigned long size, realsize, freesize, memmap_pages;
        unsigned long zone_start_pfn = zone->zone_start_pfn;

        /*  size為該管理區中的頁框數,包括洞 */
        size = zone->spanned_pages;
         /* realsize為管理區中的頁框數,不包括洞  /
        realsize = freesize = zone->present_pages;

        /*  ......  */
}

記憶體域的真實長度,可通過跨越的頁數減去空洞覆蓋的頁數而得到。這兩個值是通過兩個輔助函式計算的,我不會更詳細地討論了。其複雜性實質上取決於記憶體模型和所選定的配置選項,但所有變體最終都沒有什麼意外之處

static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
        /*  ......  */
        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;

        /*
         * 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->spanned_pages為包括洞的頁框數  */
        zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
#ifdef CONFIG_NUMA
        /* 設定zone中的節點識別符號 */
        zone->node = nid;
        /* 設定可回收頁面比率 */
        zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio)
                        / 100;
        /* 設定slab回收快取頁的比率 */
        zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100;
#endif
        /*  設定zone的名稱  */
        zone->name = zone_names[j];

        /* 初始化各種鎖 */
        spin_lock_init(&zone->lock);
        spin_lock_init(&zone->lru_lock);
        zone_seqlock_init(zone);
        /* 設定管理區屬於的節點對應的pg_data_t結構 */
        zone->zone_pgdat = pgdat;
        /*  ......  */
}

核心使用兩個全域性變數跟蹤系統中的頁數。nr_kernel_pages統計所有一致對映的頁,而nr_all_pages還包括高階記憶體頁在內free_area_init_core始化為0

我們比較感興趣的是呼叫的兩個輔助函式

  • zone_pcp_init嘗試初始化該記憶體域的per-CPU快取, 定義在mm/page_alloc.c?v=4.7, line 5443
  • init_currently_empty_zone初始化free_area列表,並將屬於該記憶體域的所有page例項都設定為初始預設值。正如前文的討論,呼叫了memmap_init_zone來初始化記憶體域的頁, 定義在mm/page_alloc.c?v=4.7, line 5458

我們還可以回想前文提到的,所有頁屬性起初都設定MIGRATE_MOVABLE。 此外,空閒列表是在zone_init_free_lists中初始化的

static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
    /*  ......  */
    {
        /* 初始化cpu的頁面快取 */
        zone_pcp_init(zone);

        /* 設定pgdat->nr_zones和zone->zone_start_pfn成員
         * 初始化zone->free_area成員
         * 初始化zone->wait_table相關成員
         */
         ret = init_currently_empty_zone(zone, zone_start_pfn, size);
        BUG_ON(ret);
        /* 初始化該zone對應的page結構 */
        memmap_init(size, nid, j, zone_start_pfn);
    }
    /*  ......  */
}

6 memmap_init初始化page頁面

在free_area_init_core初始化記憶體管理區zone的過程中, 通過memmap_init函式對每個記憶體管理區zone的page記憶體進行了初始化

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

#ifndef __HAVE_ARCH_MEMMAP_INIT
#define memmap_init(size, nid, zone, start_pfn) \
    memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY)
#endif

memmap_init_zone函式完成了page的初始化工作, 該函式定義在mm/page_alloc.c?v=4.7, line 5139

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

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

7 總結

7.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;
          |

7.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]);

7.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;
    |     對於程式碼中的判斷條件一般不會成立,因為頁數會最夠多(記憶體較大)