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 還支持 debugfs。 如果內核運行在非 x86 的架構上, 你可以存取
- /sys/kernel/debug/memblock/memory
- /sys/kernel/debug/memblock/reserved
- /sys/kernel/debug/memblock/physmem
來獲取 memblock 的內容。
小結
這是Linux 內核內存管理的第一部分。 如果你有任何問題或建議,請用以下方法聯繫我: