本文來自 程雪濤 的自薦投稿
Linux核心使用夥伴系統管理記憶體,那麼在夥伴系統工作前,如何管理記憶體?答案是memblock。
memblock在系統啟動階段進行簡單的記憶體管理,記錄實體記憶體的使用情況。
在進一步介紹memblock之前,有必要先了解下系統記憶體的使用情況:
- 首先,記憶體中的某些部分是永久的分配給核心的,比如核心程式碼段和資料段,ramdisk和fdt佔用的空間等,它們是系統記憶體的一部分,但是不能被侵佔,也不參與記憶體分配,稱之為靜態記憶體;
- 其次,GPU,Camera等都需要預留大量連續記憶體,這部分記憶體平時不用,但是系統必須提前預留好,稱之為預留記憶體;
- 最後,記憶體的其餘部分稱之為動態記憶體,是需要核心管理的寶貴資源。
memblock把實體記憶體劃分為若干記憶體區,按使用型別分別放在memory和reserved兩個集合(陣列)中,memory即動態記憶體的集合,reserved集合包括靜態記憶體和預留記憶體。
1. memblock關鍵資料結構
memblock資料結構定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct memblock { bool bottom_up; phys_addr_t current_limit; struct memblock_type memory; struct memblock_type reserved; }; struct memblock_type { unsigned long cnt; /* number of regions */ unsigned long max; /* size of the allocated array */ phys_addr_t total_size; /* size of all regions */ struct memblock_region *regions; }; struct memblock_region { phys_addr_t base; phys_addr_t size; unsigned long flags; }; |
memblock相關資料結構十分的簡單,核心還為memblock定義了一個全域性變數,併為其賦初值,如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct memblock memblock __initdata_memblock = { .memory.regions = memblock_memory_init_regions, .memory.cnt = 1, /* empty dummy entry */ .memory.max = INIT_MEMBLOCK_REGIONS, .reserved.regions = memblock_reserved_init_regions, .reserved.cnt = 1, /* empty dummy entry */ .reserved.max = INIT_MEMBLOCK_REGIONS, .bottom_up = false, .current_limit = MEMBLOCK_ALLOC_ANYWHERE, }; |
memory型別的記憶體集合指向memblock_memory_init_regions陣列,最多可以記錄128個記憶體區。
reserved型別的記憶體集合指向memblock_reserved_init_regions陣列,最多可以記錄128個記憶體區。
注:核心程式碼經常用到類似”__initdata_memblock”的巨集定義,通常用來指定變數或函式所在的section,該巨集的定義如下:
1 |
#define __meminitdata __attribute__ ((__section__(".meminit.data"))) |
2. memblock基本操作
1) 新增記憶體區
1 2 |
int memblock_add(phys_addr_t base, phys_addr_t size); int memblock_reserve(phys_addr_t base, phys_addr_t size); |
分別為memory和reserved集合新增記憶體區,如果新加入的記憶體區與原有記憶體區重疊,則合併到原有記憶體區,否則插入新記憶體區。
實際工作由memblock_add_range()完成,type引數指定記憶體集合型別。
需要注意的是該函式內部會執行兩次:
第一次計算需要插入幾個記憶體區,如果超過允許的最大記憶體區個數,則double記憶體區陣列;
第二次執行記憶體區的實際插入與合併操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
int memblock_add_range(struct memblock_type *type, phys_addr_t base, phys_addr_t size, int nid, unsigned long flags) { bool insert = false; ... ... /* 特例: 如果記憶體集合為空,則不需要執行插入或合併操作,直接插入新的記憶體區就可以了 */ if (type->regions[0].size == 0) { WARN_ON(type->cnt != 1 || type->total_size); type->regions[0].base = base; type->regions[0].size = size; type->regions[0].flags = flags; memblock_set_region_node(&type->regions[0], nid); type->total_size = size; return 0; } repeat: base = obase; nr_new = 0; 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; /* 如果記憶體區重疊,則先插入低地址部分[base~rbase],然後重新計算base地址 */ if (rbase > base) { nr_new++; if (insert) memblock_insert_region(type, i++, base, rbase - base, nid, flags); } /* area below @rend is dealt with, forget about it */ base = min(rend, end); } /* 插入記憶體區[base~end] */ if (base < end) { nr_new++; if (insert) memblock_insert_region(type, i, base, end - base, nid, flags); } /* 第一次執行檢查是否需要調整記憶體區陣列大小,第二次執行合併操作 */ if (!insert) { while (type->cnt + nr_new > type->max) if (memblock_double_array(type, obase, size) < 0) return -ENOMEM; insert = true; goto repeat; } else { memblock_merge_regions(type); return 0; } } |
2) 移除記憶體區
1 |
int memblock_remove(phys_addr_t base, phys_addr_t size); |
從memory集合移除給定實體地址所指的記憶體區,如果是記憶體區域的一部分,則涉及到調整region大小,或者將一個region拆分成兩個region。
系統將不會為移除的記憶體區建立記憶體對映,這部分記憶體區後續應該由DMA或CMA管理。
3) 分配記憶體
1 2 |
phys_addr_t memblock_alloc(phys_addr_t size, phys_addr_t align); phys_addr_t memblock_alloc_range(phys_addr_t size, phys_addr_t align, phys_addr_t start, phys_addr_t end); |
使用該函式向kernel申請一塊可用的實體記憶體,memblock使用自頂向下(取決於bottom_up的值)的方式查詢空閒記憶體,實際操作是在memory region中查詢合適的記憶體,並加入到reserved region中以標記這塊記憶體已經被使用。
4) 釋放記憶體
1 |
int memblock_free(phys_addr_t base, phys_addr_t size); |
使用該函式來釋放由memblock_alloc申請到的實體記憶體。
3. 探測系統可用記憶體
核心是如何知曉實體記憶體的拓撲結構呢?相信很多人都有過類似的疑問。
通過DDR的模式暫存器(MR8),可以很容易獲得記憶體密度,進而推斷出記憶體容量,這部分工作通常由bootloader完成,然後使用fdt或者atag等方式傳遞給核心。
以fdt為例,核心解析memory節點,取得實體記憶體的拓撲結構(起始地址及大小),並新增到memblock中,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
setup_arch()->setup_machine_fdt()->early_init_dt_scan()->early_init_dt_scan_memory() { ... ... reg = of_get_flat_dt_prop(node, "reg", &l); ... ... while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) { u64 base, size; base = dt_mem_next_cell(dt_root_addr_cells, ®); size = dt_mem_next_cell(dt_root_size_cells, ®); ... ... early_init_dt_add_memory_arch(base, size); } } |
該函式掃描memory節點,並解析reg屬性,注意此時DeviceTree還沒有執行unflattern操作,需要使用”fdt”型別介面解析dtb。
以4G DDR為例,輸出的除錯資訊如下:
1 2 3 |
[ 0.000000] memory scan node memory, reg size 32, data: 0 80 0 80, 1000000 0 0 557e [ 0.000000] - 80000000 , 80000000 [ 0.000000] - 100000000 , 7e550000 |
reg屬性由addr和size組成,分別佔用2個cell(u32型別資料),上面的reg data可以看成:“0 00000080 0 00000080, 01000000 0 0 00557e”。
dtb使用big endian方式儲存資料,需要轉換成cpu位元組序。
解析出來的記憶體包含兩個Rank,起始地址分別是0x80000000和0x100000000,這是系統的可用記憶體,用來初始化memory region。
1 2 3 4 5 6 7 8 9 10 11 12 |
void early_init_dt_add_memory_arch(base, size) { const u64 phys_offset = __pa(PAGE_OFFSET); ... ... if (base < phys_offset) { pr_warning("Ignoring memory range 0x%llx - 0x%llx\n", base, phys_offset); size -= phys_offset - base; base = phys_offset; } memblock_add(base, size); } |
從fdt解析的記憶體資訊是否可信呢?核心有自己的判斷,在啟動階段,核心會根據自身的執行地址計算記憶體基地址,即PHYS_OFFSET。
如果base地址小於phys_offset,則核心使用可信的phys_offset做為主存的基地址。
這裡要注意區分PHYS_OFFSET, PAGE_OFFSET:
PAGE_OFFSET是核心虛擬地址空間的起始地址,PHYS_OFFSET是RAM在物理空間的起始地址,核心空間的地址對映通常具有固定的偏移量,即:
1 |
#define __virt_to_phys(x) (((phys_addr_t)(x) - PAGE_OFFSET + PHYS_OFFSET)) |
4. 記錄系統預留記憶體
這裡說的系統預留記憶體,包括靜態記憶體(核心Image,ramdisk,fdt等佔用空間),以及系統為Camera,Display等子系統預留的大量連續記憶體。
另外,高通平臺通常包含多核,還需要為Modem,TZ/TA等預留執行空間,這部分空間類似靜態記憶體,都是永久分配給其它核心使用,根據節點屬性,通常由DMA管理。
arm64_memblock_init()函式初始化系統預留記憶體,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
setup_arch()->arm64_memblock_init() { // 1) 保留核心映像所在的記憶體空間(0x80080000~?) memblock_reserve(__pa(_text), _end - _text); // 2) 保留Ramdisk所在的記憶體空間(0x82200000~?) memblock_reserve(__virt_to_phys(initrd_start), initrd_end - initrd_start); // 3) 保留fdt所在記憶體空間(0x82000000~?),掃描reserved-memory節點,解析其子節點包含的預留記憶體資訊; early_init_fdt_scan_reserved_mem(); /* 4GB maximum for 32-bit only capable devices */ if (IS_ENABLED(CONFIG_ZONE_DMA)) dma_phys_limit = max_zone_dma_phys(); // 4) 初始化Global CMA dma_contiguous_reserve(dma_phys_limit); memblock_allow_resize(); memblock_dump_all(); } void early_init_fdt_scan_reserved_mem(void) { ... ... /* 1) 保留啟動引數所在的記憶體空間,這裡通常指的就是fdt佔用的記憶體空間 */ early_init_dt_reserve_memory_arch(__pa(initial_boot_params), fdt_totalsize(initial_boot_params), 0); ... ... /* 2) 掃描reserved-memory節點 */ of_scan_flat_dt(__fdt_scan_reserved_mem, NULL); /* 3) 為預留記憶體執行初始化操作 */ fdt_init_reserved_mem(); } |
“no-map”屬性決定向reserved region新增記憶體區,還是從memory region移除記憶體區,二者差別在於核心不會給”no-map”屬性的記憶體區建立記憶體對映,即該記憶體區不在動態記憶體管理範圍。
預留記憶體還會被新增到reserved_mem陣列,為後續的初始化做準備,”reg”屬性指定記憶體區的起始地址和大小,如果沒有”reg”屬性,還需要為記憶體區分配空間。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
__fdt_scan_reserved_mem()->__reserved_mem_reserve_reg() { ... ... prop = of_get_flat_dt_prop(node, "reg", &len); if (!prop) return -ENOENT; ... ... nomap = of_get_flat_dt_prop(node, "no-map", NULL) != NULL; while (len >= t_len) { base = dt_mem_next_cell(dt_root_addr_cells, &prop); size = dt_mem_next_cell(dt_root_size_cells, &prop); if (size && early_init_dt_reserve_memory_arch(base, size, nomap) == 0) pr_info("Reserved memory: reserved region for node '%s': base %pa, size %ld MiB\n", uname, &base, (unsigned long)size / SZ_1M); else pr_info("Reserved memory: failed to reserve memory for node '%s': base %pa, size %ld MiB\n", uname, &base, (unsigned long)size / SZ_1M); len -= t_len; if (first) { fdt_reserved_mem_save_node(node, uname, base, size); first = 0; } } return 0; } |
至此,memblock的初始化工作已經基本完成了,主要是記錄系統記憶體的使用情況:
memory region記錄系統了所有可用的動態記憶體;
reserved region記錄了系統預留記憶體,這部分記憶體通常由CMA管理,也屬於動態記憶體範疇;
reserved_mem陣列則記錄系統所有預留記憶體,包括”no-map”屬性的記憶體區,為後續進一步初始化工作做準備。
5. 初始化預留記憶體區
記憶體向來是系統的寶貴資源,預留記憶體如果僅做為子系統的專用記憶體,就有點浪費了。
Linux核心引入CMA(Contiguous Memory Allocator,連續記憶體分配器)。
其工作原理是:為驅動預留一段記憶體,當驅動不用時,Memory Allocator(Buddy System)可以分配給使用者程式使用;而當驅動需要使用時,就將程式佔用的記憶體通過回收或者遷移的方式騰出來,供驅動使用。
但是並不是所有的預留記憶體都由CMA管理,像Modem,TA等永久分配給其它核心使用的記憶體空間,核心並不為這部分空間建立記憶體對映,而是交由DMA管理。
通過上面的分析,我們看到所有預留記憶體資訊都記錄在reserved_mem陣列,下面先看看該結構體的定義:
1 2 3 4 5 6 7 8 9 10 11 |
static struct reserved_mem reserved_mem[MAX_RESERVED_REGIONS]; struct reserved_mem { const char *name; unsigned long fdt_node; unsigned long phandle; const struct reserved_mem_ops *ops; phys_addr_t base; phys_addr_t size; void *priv; int fixup; }; |
reserved-memory子節點包含預留記憶體屬性,典型定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; removed_regions: removed_regions@85800000 { compatible = "removed-dma-pool"; no-map; reg = <0 0x85800000 0 0x3000000>; }; adsp_mem: adsp_region { compatible = "shared-dma-pool"; alloc-ranges = <0 0x00000000 0 0xffffffff>; reusable; alignment = <0 0x100000>; size = <0 0x400000>; }; ... ... }; |
“reg”和”no-map”屬性前面介紹過,詳細可以參考reserved-memory.txt,”compatible”屬性使用標準定義,核心註冊兩種不同的處理方法:
1 2 |
RESERVEDMEM_OF_DECLARE(dma, "removed-dma-pool", removed_dma_setup); RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup); |
“removed-dma-pool”表示該記憶體區位於DMA管理區,核心不可見(沒有頁表)。
“shared-dma-pool”表示該記憶體區位於CMA管理區,平時是可用的,只有需要時才分配給驅動使用。
如果沒有”reg”屬性,即沒有指定預留記憶體的起始地址,則需要由系統分配預留記憶體,然後初始化reserved_mem的ops成員:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
arm64_memblock_init()->early_init_fdt_scan_reserved_mem()->fdt_init_reserved_mem() { ... ... for (i = 0; i < reserved_mem_count; i++) { struct reserved_mem *rmem = &reserved_mem[i]; ... ... if (rmem->size == 0) err = __reserved_mem_alloc_size(node, rmem->name, &rmem->base, &rmem->size); if (err == 0) __reserved_mem_init_node(rmem); } } |
以”shared-dma-pool”為例,它的初始化函式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static int rmem_cma_setup(struct reserved_mem *rmem) { struct cma *cma; ... ... err = cma_init_reserved_mem(rmem->base, rmem->size, 0, &cma); if (err) { pr_err("Reserved memory: unable to setup CMA region\n"); return err; } ... .... rmem->ops = &rmem_cma_ops; rmem->priv = cma; ... ... return 0; } |
此處為”shared-dma-pool”型別的記憶體註冊操作方法,cma_init_reserved_mem初始化cma_area(CMA管理區)。
reserved_mem的ops成員包括init和release兩個操作方法:
1 2 3 4 |
struct reserved_mem_ops { int (*device_init)(struct reserved_mem *rmem, struct device *dev); void (*device_release)(struct reserved_mem *rmem, struct device *dev); }; |
驅動註冊預留記憶體區時呼叫device_init方法,為裝置指定預留記憶體操作方法(DMA)或預留記憶體區域(CMA),這些方法包括預留記憶體的申請,釋放和mmap等。
6. 連續記憶體分配器(CMA)
CMA的初始化必須在buddy系統工作之前和memblock分配器初始化完成之後。
在ARM中,初始化CMA的介面是:
1 |
dma_contiguous_reserve(phys_addr_t limit); |
@limit: 指定CMA區域的上限,在64位系統上@limit的值通常是0x100000000。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
arm64_memblock_init()->dma_contiguous_reserve(phys_addr_t limit) { ... ... /* 可以在cmdline或者kernel config中配置global cma size */ if (selected_size && !dma_contiguous_default_area) { pr_debug("%s: reserving %ld MiB for global area\n", __func__, (unsigned long)selected_size / SZ_1M); dma_contiguous_reserve_area(selected_size, selected_base, selected_limit, &dma_contiguous_default_area, fixed); } } |
以命令列引數”cma=32M@0-0xfffffff”為例: size_cmdline = 32M, base_cmdline = 0x0, limit_cmdline = 0xffffffff 。
計算好CMA的size等值以後就進入cma_declare_contiguous中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
dma_contiguous_reserve_area()->cma_declare_contiguous() { ... ... /* Reserve memory */ if (fixed) { if (memblock_is_region_reserved(base, size) || memblock_reserve(base, size) < 0) { ret = -EBUSY; goto err; } } else { phys_addr_t addr = 0; ... ... if (!addr) { addr = memblock_alloc_range(size, alignment, base, limit); if (!addr) { ret = -ENOMEM; goto err; } } ... ... base = addr; } ret = cma_init_reserved_mem(base, size, order_per_bit, res_cma); ... ... return 0; } |
dma_contiguous_default_are是使用者自定義CMA管理區,定義如下:
1 2 3 4 5 6 7 8 |
struct cma *dma_contiguous_default_area; struct cma { unsigned long base_pfn; unsigned long count; unsigned long *bitmap; unsigned int order_per_bit; /* Order of pages represented by one bit */ struct mutex lock; }; |
下面進入cma_init_reserved_mem初始化使用者自定義CMA管理區:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int __init cma_init_reserved_mem(phys_addr_t base, phys_addr_t size, int order_per_bit, struct cma **res_cma) { ... ... /* Sanity checks */ if (cma_area_count == ARRAY_SIZE(cma_areas)) { pr_err("Not enough slots for CMA reserved regions!\n"); return -ENOSPC; } ... ... /* * Each reserved area must be initialised later, when more kernel * subsystems (like slab allocator) are available. */ cma = &cma_areas[cma_area_count]; cma->base_pfn = PFN_DOWN(base); cma->count = size >> PAGE_SHIFT; cma->order_per_bit = order_per_bit; *res_cma = cma; cma_area_count++; return 0; } |
以上只是將CMA區域預留下來,並記錄到相關陣列,進一步初始化和使用需要等slab等子系統初始化完成後了。
CMA並不直接開放給驅動開發人員,在註冊裝置時可以使用”memory-region”屬性指定要操作的記憶體區域,需要分配DMA記憶體時,呼叫DMA相關函式就可以了。
例如dma_alloc_coherent(),最終DMA相關的分配函式會到達CMA的分配函式dma_alloc_from_contiguous()。