核心早期記憶體分配器:memblock

伯樂線上讀者發表於2016-11-24

本文來自 程雪濤 的自薦投稿

Linux核心使用夥伴系統管理記憶體,那麼在夥伴系統工作前,如何管理記憶體?答案是memblock。

memblock在系統啟動階段進行簡單的記憶體管理,記錄實體記憶體的使用情況。

在進一步介紹memblock之前,有必要先了解下系統記憶體的使用情況:

  • 首先,記憶體中的某些部分是永久的分配給核心的,比如核心程式碼段和資料段,ramdisk和fdt佔用的空間等,它們是系統記憶體的一部分,但是不能被侵佔,也不參與記憶體分配,稱之為靜態記憶體;
  • 其次,GPU,Camera等都需要預留大量連續記憶體,這部分記憶體平時不用,但是系統必須提前預留好,稱之為預留記憶體;
  • 最後,記憶體的其餘部分稱之為動態記憶體,是需要核心管理的寶貴資源。

memblock把實體記憶體劃分為若干記憶體區,按使用型別分別放在memory和reserved兩個集合(陣列)中,memory即動態記憶體的集合,reserved集合包括靜態記憶體和預留記憶體。

1. memblock關鍵資料結構

memblock資料結構定義如下:

memblock相關資料結構十分的簡單,核心還為memblock定義了一個全域性變數,併為其賦初值,如下:

memory型別的記憶體集合指向memblock_memory_init_regions陣列,最多可以記錄128個記憶體區。

reserved型別的記憶體集合指向memblock_reserved_init_regions陣列,最多可以記錄128個記憶體區。

注:核心程式碼經常用到類似”__initdata_memblock”的巨集定義,通常用來指定變數或函式所在的section,該巨集的定義如下:

2. memblock基本操作

1) 新增記憶體區

分別為memory和reserved集合新增記憶體區,如果新加入的記憶體區與原有記憶體區重疊,則合併到原有記憶體區,否則插入新記憶體區。

實際工作由memblock_add_range()完成,type引數指定記憶體集合型別。

 

需要注意的是該函式內部會執行兩次:

第一次計算需要插入幾個記憶體區,如果超過允許的最大記憶體區個數,則double記憶體區陣列;
第二次執行記憶體區的實際插入與合併操作。

2) 移除記憶體區

從memory集合移除給定實體地址所指的記憶體區,如果是記憶體區域的一部分,則涉及到調整region大小,或者將一個region拆分成兩個region。

系統將不會為移除的記憶體區建立記憶體對映,這部分記憶體區後續應該由DMA或CMA管理。

3) 分配記憶體

使用該函式向kernel申請一塊可用的實體記憶體,memblock使用自頂向下(取決於bottom_up的值)的方式查詢空閒記憶體,實際操作是在memory region中查詢合適的記憶體,並加入到reserved region中以標記這塊記憶體已經被使用。

4) 釋放記憶體

使用該函式來釋放由memblock_alloc申請到的實體記憶體。

3. 探測系統可用記憶體

核心是如何知曉實體記憶體的拓撲結構呢?相信很多人都有過類似的疑問。

通過DDR的模式暫存器(MR8),可以很容易獲得記憶體密度,進而推斷出記憶體容量,這部分工作通常由bootloader完成,然後使用fdt或者atag等方式傳遞給核心。

以fdt為例,核心解析memory節點,取得實體記憶體的拓撲結構(起始地址及大小),並新增到memblock中,程式碼如下:

該函式掃描memory節點,並解析reg屬性,注意此時DeviceTree還沒有執行unflattern操作,需要使用”fdt”型別介面解析dtb。

以4G DDR為例,輸出的除錯資訊如下:

reg屬性由addr和size組成,分別佔用2個cell(u32型別資料),上面的reg data可以看成:“0 00000080 0 00000080, 01000000 0 0 00557e”。

dtb使用big endian方式儲存資料,需要轉換成cpu位元組序。

解析出來的記憶體包含兩個Rank,起始地址分別是0x800000000x100000000,這是系統的可用記憶體,用來初始化memory region。

從fdt解析的記憶體資訊是否可信呢?核心有自己的判斷,在啟動階段,核心會根據自身的執行地址計算記憶體基地址,即PHYS_OFFSET。

如果base地址小於phys_offset,則核心使用可信的phys_offset做為主存的基地址。

 

這裡要注意區分PHYS_OFFSET, PAGE_OFFSET:

PAGE_OFFSET是核心虛擬地址空間的起始地址,PHYS_OFFSET是RAM在物理空間的起始地址,核心空間的地址對映通常具有固定的偏移量,即:

4. 記錄系統預留記憶體

這裡說的系統預留記憶體,包括靜態記憶體(核心Image,ramdisk,fdt等佔用空間),以及系統為Camera,Display等子系統預留的大量連續記憶體。

另外,高通平臺通常包含多核,還需要為Modem,TZ/TA等預留執行空間,這部分空間類似靜態記憶體,都是永久分配給其它核心使用,根據節點屬性,通常由DMA管理。

arm64_memblock_init()函式初始化系統預留記憶體,程式碼如下:

“no-map”屬性決定向reserved region新增記憶體區,還是從memory region移除記憶體區,二者差別在於核心不會給”no-map”屬性的記憶體區建立記憶體對映,即該記憶體區不在動態記憶體管理範圍。

預留記憶體還會被新增到reserved_mem陣列,為後續的初始化做準備,”reg”屬性指定記憶體區的起始地址和大小,如果沒有”reg”屬性,還需要為記憶體區分配空間。

至此,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陣列,下面先看看該結構體的定義:

reserved-memory子節點包含預留記憶體屬性,典型定義如下:

“reg”和”no-map”屬性前面介紹過,詳細可以參考reserved-memory.txt,”compatible”屬性使用標準定義,核心註冊兩種不同的處理方法:

“removed-dma-pool”表示該記憶體區位於DMA管理區,核心不可見(沒有頁表)。

“shared-dma-pool”表示該記憶體區位於CMA管理區,平時是可用的,只有需要時才分配給驅動使用。

如果沒有”reg”屬性,即沒有指定預留記憶體的起始地址,則需要由系統分配預留記憶體,然後初始化reserved_mem的ops成員:

以”shared-dma-pool”為例,它的初始化函式如下:

此處為”shared-dma-pool”型別的記憶體註冊操作方法,cma_init_reserved_mem初始化cma_area(CMA管理區)。

reserved_mem的ops成員包括init和release兩個操作方法:

驅動註冊預留記憶體區時呼叫device_init方法,為裝置指定預留記憶體操作方法(DMA)或預留記憶體區域(CMA),這些方法包括預留記憶體的申請,釋放和mmap等。

6. 連續記憶體分配器(CMA)

CMA的初始化必須在buddy系統工作之前和memblock分配器初始化完成之後。
在ARM中,初始化CMA的介面是:

@limit: 指定CMA區域的上限,在64位系統上@limit的值通常是0x100000000。

以命令列引數”cma=32M@0-0xfffffff”為例: size_cmdline = 32M, base_cmdline = 0x0, limit_cmdline = 0xffffffff 。

計算好CMA的size等值以後就進入cma_declare_contiguous中:

dma_contiguous_default_are是使用者自定義CMA管理區,定義如下:

下面進入cma_init_reserved_mem初始化使用者自定義CMA管理區:

以上只是將CMA區域預留下來,並記錄到相關陣列,進一步初始化和使用需要等slab等子系統初始化完成後了。

CMA並不直接開放給驅動開發人員,在註冊裝置時可以使用”memory-region”屬性指定要操作的記憶體區域,需要分配DMA記憶體時,呼叫DMA相關函式就可以了。

例如dma_alloc_coherent(),最終DMA相關的分配函式會到達CMA的分配函式dma_alloc_from_contiguous()。

7. 進階閱讀

相關文章