Linux 的memblock 內存管理器

Linux 的memblock 內存管理器

最近接觸到了linux 在啓動階段的內存管理器memblock, 它是bootmem 的後續者。 本來想自己寫一篇關於memblock的文章的, 但看到了這篇文章, 就把它翻譯過來了:https://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-1.html# 。

簡介

內存管理是操作系統最複雜的子系統之一(而我認爲不需要加之一)。 在 kernel 入口前的最後準備 一節中, 我們在start_kernel 之前停了下來。 你可能還記得我們在啓動階段創建早期的page tables,identity page tables 和 fixmap page tables。複雜的內存管理還沒有工作。 當start_kernel 開始運行時, 我們將看到向更復雜的數據結構和技術的轉變。爲了更好地瞭解內核的初始化過程, 我們需要對這些技術有一個清晰的理解。 本章首先從memblock開始, 詳細介紹Linux 內存管理的框架及其API。

Memblcok

在自舉階段, 通用的內存管理器還沒有建立起來的時候, memblock 是管理內存區域的方法之一。 原先它叫做 Logical Memory Block, 經過了Yinghai Lu 的補丁之後, 它被命名爲memblock。 由於linux x86_64 內核使用該技術, 我們已經在 kernel 入口前的最後準備 中遇到過它了。 現在我們更仔細地考察它是如何實現的。

我們先從memblock 相關的數據結構開始 。 它們定義在頭文件 include/linux/memblock.h 裏:

第一個結構是 memblock:

struct memblock {
         bool bottom_up;
         phys_addr_t current_limit;
         struct memblock_type memory;   --> array of memblock_region
         struct memblock_type reserved; --> array of memblock_region
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
         struct memblock_type physmem;
#endif
};

該結構包含5個成員: bootom_up 爲 true 的時候表明自底向上分配內存。 current_limit 是 memory block 的限制尺寸。 接下來的三個成員表示memory block的類型, 它們可以是: memory, reserved 和 physical memory(當CONFIG_HAVE_MEMBLOCK_PHYS_MAP 使能時)。 然後我們看另一個數據結構 - memblock_type:

struct memblock_type {
    unsigned long cnt;
    unsigned long max;
    phys_addr_t total_size;
    struct memblock_region *regions;
};

該結構提供關於內存類型的信息。 它包含成員描述memory region 在當前memory block裏的個數和尺寸, 以及指向memblock_region 的指針。 而memblock_region 結構描述一塊內存區域:

struct memblock_region {
        phys_addr_t base;
        phys_addr_t size;
        unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
        int nid;
#endif
};

它提供了內存區域的基地址和大小, 還有一個標誌域, 可能的值有:

enum {
    MEMBLOCK_NONE    = 0x0,    /* No special request */
    MEMBLOCK_HOTPLUG    = 0x1,    /* hotpluggable region */
    MEMBLOCK_MIRROR    = 0x2,    /* mirrored region */
    MEMBLOCK_NOMAP    = 0x4,    /* don't add to kernel direct mapping */
};

如果CONFIG_HAVE_MEMBLOCK_NODE_MAP定義了, 還有一個 numa 節點選擇項 nid。

我們可以用下圖表示以上三個數據結構的關係:

+---------------------------+   +---------------------------+
|         memblock          |   |                           |
|  _______________________  |   |                           |
| |        memory         | |   |       Array of the        |
| |      memblock_type    |-|-->|      memblock_region      |
| |_______________________| |   |                           |
|                           |   +---------------------------+
|  _______________________  |   +---------------------------+
| |       reserved        | |   |                           |
| |      memblock_type    |-|-->|       Array of the        |
| |_______________________| |   |      memblock_region      |
|                           |   |                           |
+---------------------------+   +---------------------------+

Memblock 的初始化

memblock 所有的API都定義在頭文件 include/linux/memblock.h裏, 而它們的實現都在文件 mm/memblock.c 裏。 在該文件的開始處, 我們看到 memblock 結構的初始化:

struct memblock memblock __initdata_memblock = {
    .memory.regions        = memblock_memory_init_regions,
    .memory.cnt            = 1,
    .memory.max            = INIT_MEMBLOCK_REGIONS,

    .reserved.regions    = memblock_reserved_init_regions,
    .reserved.cnt        = 1,
    .reserved.max        = INIT_MEMBLOCK_REGIONS,

#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    .physmem.regions    = memblock_physmem_init_regions,
    .physmem.cnt        = 1,
    .physmem.max        = INIT_PHYSMEM_REGIONS,
#endif
    .bottom_up            = false,
    .current_limit        = MEMBLOCK_ALLOC_ANYWHERE,
};

此處的變量 memblock 與數據結構同名。首先注意到 __initdata_memblock 的定義是:

#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK
    #define __init_memblock __meminit
    #define __initdata_memblock __meminitdata
#else
    #define __init_memblock
    #define __initdata_memblock
#endif

它取決於 CONFIG_ARCH_DISCARD_MEMBLOCK 。 如果該設置使能了, 則memblock 的代碼和數據會放到 .init section 裏去,它們佔用的內存在內核完成啓動後被釋放。 然後我們考察memblock 的成員 memblock_type memory, memblock_type reserved 和 memblock_type physmem 的初始化, 我們只對memblock_type.regions 的初始化過程感興趣。 注意到每個 memblock_type.regions 被賦值爲一個 memblock_regions 數組:

static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif

每個數組包含128個memory regions。 因爲 INIT_MEMBLOCK_REGIONS 默認定義爲 128:

#define INIT_MEMBLOCK_REGIONS   128

而且所有的數組也有 __initdata_memblock 宏,說明它們能在內核啓動結束後被釋放。

memblock 的最後兩個成員 bottom_up 被設置爲false, 而當前的memblock 的上限爲:

#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0)

即 0xffffffffffffffff 。

這樣memblock的初始化就完成了,接下來我們看memblock API。

Memblock API

爲了更好地理解 memblock 是怎樣實現和工作的,我們先看一下它的使用。 在Linux 內核裏有好些地方使用memblock。 比如, 以 arch/x86/kernel/e820.c 裏的 memblock_x86_fill 爲例, 該函數遍歷e820提供的內存塊,調用memblock_add 函數,把內核要保留的memory region 加到memblock裏去。 該函數接收一個物理基地址和memory region 的大小作爲參數, 它其實並不做什麼, 僅調用

memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);

我們傳進 memblock type - memory, 物理基地址和尺寸, 和最大的node 數目: 如果 CONFIG_NODES_SHIFT 設置了, 則爲1;如果了,則爲 1 << CONFIG_NODES_SHIFT 。 memblock_add_range 函數把一個新的memory region 加到memory block裏。 它先檢查給定memory region 的大小,如果爲0則返回。 然後它檢查相應的memblock_type 裏是否有memory region, 如果沒有,我們就用給定的參數填充一個新的 memory_region 並返回(我們在 First touch of the linux kernel memory manager framework 裏已經看到過). 如果 memblock_type 不爲空, 我們開始往給定的memblock_type 里加一塊memory region。 首先, 我們獲取結束地址:

phys_addr_t end = base + memblock_cap_size(base, &size);

memblock_cap_size 調整 size 使得 base + size 不會溢出。 它的實現很簡單:

static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size)
{
    return *size = min(*size, (phys_addr_t)ULLONG_MAX - base);
}

它返回 size 和 UULONG_MAX - base 兩者中的最小值。

然後我們獲得了新memory region 的結束地址。 memblock_add_range 檢查與已經加進的memory region 的重疊和合並的條件。 插入新的memory region 包含兩步:

  • 把不重疊部分作爲單獨的region加進去
  • 合併相鄰的region

我們遍歷所有已加進的 memory region 並檢查與新來的region 的重疊情況:

    for (i = 0; i < type->cnt; i++) {
        struct memblock_region *rgn = &type->regions[i];
        phys_addr_t rbase = rgn->base;
        phys_addr_t rend = rbase + rgn->size;

        if (rbase >= end)
            break;
        if (rend <= base)
            continue;
        ...
        ...
        ...
    }

如果新的memory region 與現有的region 不重疊,則插入memblock, 這是第一步。 我們檢查它能否合適memblock, 否則就調用 memblock_double_array:

while (type->cnt + nr_new > type->max)
    if (memblock_double_array(type, obase, size) < 0)
        return -ENOMEM;
    insert = true;
    goto repeat;

它把給定的region array 擴大一倍, 接着我們置 insert 爲 true 並跳轉到標號 repeat 。 第二步, 從repeat 處, 我們走相同的循環, 調用 memblock_insert_region 把region 插入到memory block裏:

    if (base < end) {
        nr_new++;
        if (insert)
            memblock_insert_region(type, i, base, end - base,
                           nid, flags);
    }

由於insert 已經置爲true, memblock_insert_region 會被調用。 它的實現跟我們以前見過的插入空白的memblock_type幾乎一樣。 它先獲取最後一個region:

struct memblock_region *rgn = &type->regions[idx];

並拷貝這些區域:

memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));

然後填充memblock_region 的base, szie 等信息, 並增加memblock_type 的大小。 在結束運行前, memblock_add_range 調用 memblock_merge_regions 來合併相鄰的可兼容的region。

在第二階段, 新的region 可能與現有的region 重疊。 比如, 我們已經有了 region1:

0                    0x1000
+-----------------------+
|                       |
|                       |
|        region1        |
|                       |
|                       |
+-----------------------+

而我們想加入region2,它的base address 和size 是這樣的:

0x100                 0x2000
+-----------------------+
|                       |
|                       |
|        region2        |
|                       |
|                       |
+-----------------------+

這樣新 region 的基地址是:

base = min(rend, end);

在我們的例子裏,就是0x1000。 然後就像以前做過的那樣插入該 region :

if (base < end) {
    nr_new++;
    if (insert)
        memblock_insert_region(type, i, base, end - base, nid, flags);
}

此時我們僅插入重疊的部分(我們只插入高端部分, 因爲低端部分已經在重疊的memory region 裏了), 然後用memblock_merge_regions 把相鄰部分合並起來。 它遍歷給定的memblock_type, 取兩個相鄰的region: type->regions[i] 和 type->regions[i+1], 檢查它們是否有相同的標誌、屬於相同的node、而且第一個region 的結束地址不等於第二個的開始地址:

while (i < type->cnt - 1) {
    struct memblock_region *this = &type->regions[i];
    struct memblock_region *next = &type->regions[i + 1];
    if (this->base + this->size != next->base ||
        memblock_get_region_node(this) !=
        memblock_get_region_node(next) ||
        this->flags != next->flags) {
        BUG_ON(this->base + this->size > next->base);
        i++;
        continue;
    }

如果以上條件都滿足, 我們用第二個region的尺寸來更新第一個region的尺寸:

this->size += next->size;

然後我們把第二個region 後面的區域都往前挪一位:

memmove(next, next + 1, (type->cnt - (i + 2)) * sizeof(*next));

memmove 函數把next 後面的所有region 移到next 原來所佔的位置。 最後我們減小memblock_type 的region 數量:

type->cnt--;

最後,我們得到合併成一塊的memory region:

0                                             0x2000
+------------------------------------------------+
|                                                |
|                                                |
|                   region1                      |
|                                                |
|                                                |
+------------------------------------------------+

總結一下, 我們減小了memblock的region 的數量, 增加了第一塊region的尺寸, 並移動第二塊之後的所有region到第二塊的位置, 這就是 memblock_add_range 的主要工作。

memblock_reserve 函數做跟 memblock_add 相同的工作,不過它操作的是 memblock 的 memblock_type.reserved 成員。 當然這不是全部的API。 Memblock 還提供:

  • memblock_remove - 從 memblock 中刪除 memory region
  • memblock_find_in_range - 在給定範圍內尋找空閒區域
  • memblock_free - 釋放memblock中的memory region
  • for_each_mem_range - 遍歷memblock的所有區域

獲取memory region 的信息

Memblock 還提供API獲取memory region 的信息, 它分爲兩部分:

  • get_allocated_memblock_memory_regions_info - 獲取memory region 的信息
  • get_allocated_memblock_reserved_regions_info - 獲取 reserved region 的信息

這些函數的實現很簡單。 以get_allocated_memblock_reserved_regions_info 爲例:

phys_addr_t __init_memblock get_allocated_memblock_reserved_regions_info(
                    phys_addr_t *addr)
{
    if (memblock.reserved.regions == memblock_reserved_init_regions)
        return 0;

    *addr = __pa(memblock.reserved.regions);

    return PAGE_ALIGN(sizeof(struct memblock_region) *
              memblock.reserved.max);
}

首先該函數檢查 memblock 是否含有 reserved memory region,如果沒有,它返回0。 否則我們把reserved memory region 的物理地址寫到 addr 裏, 返回分配的數組的大小並且以PAGE對齊。 注意宏PAGE_ALIGN 用來對齊,它跟PAGE SIZE 相關:

#define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)

get_allocated_memblock_memory_regions_info 的實現是一樣的,只不過它使用memblock_type.memory 而不是memblock_type.reserved 。

Memblock debugging

Memblock 中有許多對 memblock_dbg 的調用。 如果你傳送內核參數 memblock=debug,該函數就會被調用。 其實memblock_dbg 是擴展爲printk 的宏:

#define memblock_dbg(fmt, ...) \
         if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

例如你在memblock_reserve 函數裏看到了如下調用:

memblock_dbg("memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n",
             (unsigned long long)base,
             (unsigned long long)base + size - 1,
             flags, (void *)_RET_IP_);

你會看到這樣的輸出:

memblock debug

Memblock 還支持 debugfs。 如果內核運行在非 x86 的架構上, 你可以存取

  • /sys/kernel/debug/memblock/memory
  • /sys/kernel/debug/memblock/reserved
  • /sys/kernel/debug/memblock/physmem

來獲取 memblock 的內容。

小結

這是Linux 內核內存管理的第一部分。 如果你有任何問題或建議,請用以下方法聯繫我:

鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章