啓動期間的內存管理之bootmem_init初始化內存管理–Linux內存管理(十二)

1. 啓動過程當中的內存初始化

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

其代碼很複雜, 咱們只截取出其中與內存管理初始化相關的部分, 以下所示linux

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 初始化內存管理

該函數主要執行了以下操做c#

  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負責創建只能用於內核的頁表, 用戶空間是沒法訪問的. 這對管理普通應用程序和內核訪問內存的方式,有深遠的影響app

  • 在分頁機制完成後, 內核經過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;
    |     對於代碼中的判斷條件通常不會成立,由於頁數會最夠多(內存較大)
相關文章
相關標籤/搜索