一、前言
同樣的,本文是內存初始化文章的一份補充文檔,希望能夠通過這樣的一份文檔,細致的展示在初始化階段,Linux 4.4.6內核如何從device tree中提取信息,完成內存布局的任務。具體的cpu體系結構選擇的是ARM64。
二、memory type region的構建
memory type是一個memblock模塊(內核初始化階段的內存管理模塊)的術語,memblock將內存塊分成兩種類型:一種是memory type,另外一種是reserved type,分別用數組來管理系統中的兩種類型的memory region。本小節描述的是系統如何在初始化階段構建memory type的數組。
1、掃描device tree
在完成fdt內存區域的地址映射之后(fixmap_remap_fdt),內核會對fdt進行掃描,以便完成memory type數組的構建。具體代碼位於setup_machine_fdt--->early_init_dt_scan--->early_init_dt_scan_nodes中:
void __init early_init_dt_scan_nodes(void)
{
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); ------(1)
of_scan_flat_dt(early_init_dt_scan_root, NULL);
of_scan_flat_dt(early_init_dt_scan_memory, NULL);-------------(2)
}
(1)of_scan_flat_dt函數是用來scan整個device tree,針對每一個node調用callback函數,因此,這里實際上是針對設備樹中的每一個節點調用early_init_dt_scan_chosen函數。之所以這么做是因為device tree blob剛剛完成地址映射,還沒有展開,我們只能使用這種比較笨的辦法。這句代碼主要是尋址chosen node,並解析,將相關數據放入到boot_command_line。
(2)概念同上,不過是針對memory node進行scan。
2、傳統的命令行參數解析
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname, int depth, void *data)
{
int l;
const char *p;if (depth != 1 || !data ||
(strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
return 0; -------------------------------(1)early_init_dt_check_for_initrd(node); --------------------(2)
/* Retrieve command line */
p = of_get_flat_dt_prop(node, "bootargs", &l);
if (p != NULL && l > 0)
strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE)); ------------(3)
#ifdef CONFIG_CMDLINE
#ifndef CONFIG_CMDLINE_FORCE
if (!((char *)data)[0])
#endif
strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif /* CONFIG_CMDLINE */ -----------------------(4)
return 1;
}
(1)上面我們說過,early_init_dt_scan_chosen會為device tree中的每一個node而調用一次,因此,為了效率,不是chosen node的節點我們必須趕緊閃人。由於chosen node是root node的子節點,因此其depth必須是1。這里depth不是1的節點,節點名字不是"chosen"或者chosen@0和我們毫無關系,立刻返回。
(2)解析chosen node中的initrd的信息
(3)解析chosen node中的bootargs(命令行參數)並將其copy到boot_command_line。
(4)一般而言,內核有可能會定義一個default command line string(CONFIG_CMDLINE),如果bootloader沒有通過device tree傳遞命令行參數過來,那么可以考慮使用default參數。如果系統定義了CONFIG_CMDLINE_FORCE,那么系統強制使用缺省命令行參數,bootloader傳遞過來的是無效的。
3、memory node解析
int __init early_init_dt_scan_memory(unsigned long node, const char *uname, int depth, void *data)
{
const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
const __be32 *reg, *endp;
int l;
if (type == NULL) {
if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
return 0;
} else if (strcmp(type, "memory") != 0)
return 0; -----------------------------(1)reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
if (reg == NULL)
reg = of_get_flat_dt_prop(node, "reg", &l);---------------(2)
if (reg == NULL)
return 0;endp = reg + (l / sizeof(__be32)); --------------------(3)
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, ®); ----------(4)early_init_dt_add_memory_arch(base, size);--------------(5)
}return 0;
}
(1)如果該memory node是root node的子節點的話,那么它一定是有device_type屬性並且其值是字符串”memory”。不是的話就可以返回了。不過node沒有定義device_type屬性怎么辦?大部分的平台都可以直接返回了,除了PPC32,對於這個平台,如果memory node是更深層次的節點的話,那么它是沒有device_type屬性的,這時候可以根據node name來判斷。當然,目標都是一致的,不是自己關注的node就趕緊閃人。
(2)該memory node的物理地址信息保存在"linux,usable-memory"或者"reg"屬性中(reg是我們常用的)
(3)l / sizeof(__be32)是reg屬性值的cell數目,reg指向第一個cell,endp指向最后一個cell。
(4)memory node的reg屬性值其實就是一個數組,數組中的每一個entry都是base address和size的二元組。解析reg屬性需要兩個參數,dt_root_addr_cells和dt_root_size_cells,這兩個參數分別定義了root節點的子節點(比如說memory node)reg屬性中base address和size的cell數目,如果等於1,基地址(或者size)用一個32-bit的cell表示。對於ARMv8,一般dt_root_addr_cells和dt_root_size_cells等於2,表示基地址(或者size)用兩個32-bit的cell表示。
注:dt_root_addr_cells和dt_root_size_cells這兩個參數的解析在early_init_dt_scan_root中完成。
(5)針對該memory mode中的每一個memory region,調用early_init_dt_add_memory_arch向系統注冊memory type的內存區域(實際上是通過memblock_add完成的)。
4、解析memory相關的early option
setup_arch--->parse_early_param函數中會對early options解析解析,這會導致下面代碼的執行:
static int __init early_mem(char *p)
{memory_limit = memparse(p, &p) & PAGE_MASK;
return 0;
}
early_param("mem", early_mem);
在過去,沒有device tree的時代,mem這個命令行參數傳遞了memory bank的信息,內核根據這個信息來創建系統內存的初始布局。在ARM64中,由於強制使用device tree,因此mem這個啟動參數失去了本來的意義,現在它只是定義了memory的上限(最大的系統內存地址),可以限制DTS傳遞過來的內存參數。
三、reserved type region的構建
保留內存的定義主要在fixmap_remap_fdt和arm64_memblock_init函數中進行,我們會按照代碼順序逐一進行各種各樣reserved type的memory region的構建。
1、保留fdt占用的內存,代碼如下:
void *__init fixmap_remap_fdt(phys_addr_t dt_phys)
{……memblock_reserve(dt_phys, size);
……}
fixmap_remap_fdt主要是為fdt建立地址映射,在該函數的最后,順便就調用memblock_reserve保留了該段內存。
2、保留內核和initrd占用的內容,代碼如下:
void __init arm64_memblock_init(void)
{
memblock_enforce_memory_limit(memory_limit); ----------------(1)
memblock_reserve(__pa(_text), _end - _text);------------------(2)
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start)
memblock_reserve(__virt_to_phys(initrd_start), initrd_end - initrd_start);------(3)
#endif
……}
(1)我們前面解析了DTS的memory節點,已經向系統加入了不少的memory type的region,當然reserved memory block也會有一些,例如DTB對應的memory就是reserved。memory_limit可以對這些DTS的設定給出上限,memblock_enforce_memory_limit函數會根據這個上限,修改各個memory region的base和size,此外還將大於memory_limit的memory block(包括memory type和reserved type)從列表中刪掉。
(2)reserve內核代碼、數據區等(_text到_end那一段,具體的內容可以參考內核鏈接腳本)
(3)保留initital ramdisk image區域(從initrd_start到initrd_end區域)
3、通過early_init_fdt_scan_reserved_mem函數來分析dts中的節點,從而進行保留內存的動作,代碼如下:
void __init early_init_fdt_scan_reserved_mem(void)
{
int n;
u64 base, size;if (!initial_boot_params)------------------------(1)
return;/* Process header /memreserve/ fields */
for (n = 0; ; n++) {
fdt_get_mem_rsv(initial_boot_params, n, &base, &size);--------(2)
if (!size)
break;
early_init_dt_reserve_memory_arch(base, size, 0);-----------(3)
}of_scan_flat_dt(__fdt_scan_reserved_mem, NULL);------------(4)
fdt_init_reserved_mem();
}
(1)initial_boot_params實際上就是fdt對應的虛擬地址。在early_init_dt_verify中設定的。如果系統中都沒有有效的fdt,那么沒有什么可以scan的,return,走人。
(2)分析fdt中的 /memreserve/ fields ,進行內存的保留。在fdt的header中定義了一組memory reserve參數,其具體的位置是fdt base address + off_mem_rsvmap。off_mem_rsvmap是fdt header中的一個成員,如下:
struct fdt_header {
……
fdt32_t off_mem_rsvmap;------/memreserve/ fields offset
……};
fdt header中的memreserve可以定義多個,每個都是(address,size)二元組,最后以0,0結束。
(3)保留每一個/memreserve/ fields定義的memory region,底層是通過memblock_reserve接口函數實現的。
(4)對fdt中的每一個節點調用__fdt_scan_reserved_mem函數,進行reserved-memory節點的掃描,之后調用fdt_init_reserved_mem函數進行內存預留的動作,具體參考下一小節描述。
4、解析reserved-memory節點的內存,代碼如下:
static int __init __fdt_scan_reserved_mem(unsigned long node, const char *uname,
int depth, void *data)
{
static int found;
const char *status;
int err;if (!found && depth == 1 && strcmp(uname, "reserved-memory") == 0) { -------(1)
if (__reserved_mem_check_root(node) != 0) {
pr_err("Reserved memory: unsupported node format, ignoring\n");
return 1;
}
found = 1; ---------------------------------(2)
return 0;
} else if (!found) {
return 0; ----------------------------------(3)
} else if (found && depth < 2) { -------------------------(4)
return 1;
}status = of_get_flat_dt_prop(node, "status", NULL); ----------------(5)
if (status && strcmp(status, "okay") != 0 && strcmp(status, "ok") != 0)
return 0;err = __reserved_mem_reserve_reg(node, uname); ----------------(6)
if (err == -ENOENT && of_get_flat_dt_prop(node, "size", NULL))
fdt_reserved_mem_save_node(node, uname, 0, 0); ---------------(7)/* scan next node */
return 0;
}
(1)found 變量記錄了是否搜索到一個reserved-memory節點,如果沒有,我們的首要目標是找到一個reserved-memory節點。reserved-memory節點的特點包括:是root node的子節點(depth == 1),node name是"reserved-memory",這可以過濾掉一大票無關節點,從而加快搜索速度。
(2)reserved-memory節點應該包括#address-cells、#size-cells和range屬性,並且#address-cells和#size-cells的屬性值應該等於根節點對應的屬性值,如果檢查通過(__reserved_mem_check_root),那么說明找到了一個正確的reserved-memory節點,可以去往下一個節點了。當然,下一個節點往往是reserved-memory節點的subnode,也就是真正的定義各段保留內存的節點。更詳細的關於reserved-memory的設備樹定義可以參考Documentation\devicetree\bindings\reserved-memory\reserved-memory.txt文件。
(3)沒有找到reserved-memory節點之前,of_scan_flat_dt會不斷的遍歷下一個節點,而在__fdt_scan_reserved_mem函數中返回0表示讓搜索繼續,如果返回1,表示搜索停止。
(4)如果找到了一個reserved-memory節點,並且完成了對其所有subnode的scan,那么是退出整個reserved memory的scan過程了。
(5)如果定義了status屬性,那么要求其值必須要是ok或者okay,當然,你也可以不定義該屬性(這是一般的做法)。
(6)定義reserved memory有兩種方法,一種是靜態定義,也就是定義了reg屬性,這時候,可以通過調用__reserved_mem_reserve_reg函數解析reg的(address,size)的二元數組,逐一對每一個定義的memory region進行預留。實際的預留內存動作可以調用memblock_reserve或者memblock_remove,具體調用哪一個是和該節點是否定義no-map屬性相關,如果定義了no-map屬性,那么說明這段內存操作系統根本不需要進行地址映射,也就是說這塊內存是不歸操作系統內存管理模塊來管理的,而是歸於具體的驅動使用(在device tree中,設備節點可以定義memory-region節點來引用在memory node中定義的保留內存,具體可以參考reserved-memory.txt文件)。
(7)另外一種定義reserved memory的方法是動態定義,也就是說定義了該內存區域的size(也可以定義alignment或者alloc-range進一步約定動態分配的reserved memory屬性,不過這些屬性都是option的),但是不指定具體的基地址,讓操作系統自己來分配這段memory。
5、預留reserved-memory節點的內存
device tree中的reserved-memory節點及其子節點靜態或者動態定義了若干的reserved memory region,靜態定義的memory region起始地址和size都是確定的,因此可以立刻調用memblock的模塊進行內存區域的預留,但是對於動態定義的memory region,__fdt_scan_reserved_mem只是將信息保存在了reserved_mem全局變量中,並沒有進行實際的內存預留動作,具體的操作在fdt_init_reserved_mem函數中,代碼如下:
void __init fdt_init_reserved_mem(void)
{
int i;__rmem_check_for_overlap(); -------------------------(1)
for (i = 0; i < reserved_mem_count; i++) {--遍歷每一個reserved memory region
struct reserved_mem *rmem = &reserved_mem[i];
unsigned long node = rmem->fdt_node;
int len;
const __be32 *prop;
int err = 0;prop = of_get_flat_dt_prop(node, "phandle", &len);---------------(2)
if (!prop)
prop = of_get_flat_dt_prop(node, "linux,phandle", &len);
if (prop)
rmem->phandle = of_read_number(prop, len/4);if (rmem->size == 0)----------------------------(3)
err = __reserved_mem_alloc_size(node, rmem->name,
&rmem->base, &rmem->size);
if (err == 0)
__reserved_mem_init_node(rmem);--------------------(4)
}
}
(1)檢查靜態定義的 reserved memory region之間是否有重疊區域,如果有重疊,這里並不會對reserved memory region的base和size進行調整,只是打印出錯信息而已。
(2)每一個需要被其他node引用的node都需要定義"phandle", 或者"linux,phandle"。雖然在實際的device tree source中看不到這個屬性,實際上dtc會完美的處理這一切的。
(3)size等於0的memory region表示這是一個動態分配region,base address尚未定義,因此我們需要通過__reserved_mem_alloc_size函數對節點進行分析(size、alignment等屬性),然后調用memblock的alloc接口函數進行memory block的分配,最終的結果是確定base address和size,並將這段memory region從memory type的數組中移到reserved type的數組中。當然,如果定義了no-map屬性,那么這段memory會從系統中之間刪除(memory type和reserved type數組中都沒有這段memory的定義)。
(4)保留內存有兩種使用場景,一種是被特定的驅動使用,這時候在特定驅動的初始化函數(probe函數)中自然會進行處理。還有一種場景就是被所有驅動或者內核模塊使用,例如CMA,per-device Coherent DMA的分配等,這時候,我們需要借用device tree的匹配機制進行這段保留內存的初始化動作。有興趣的話可以看看RESERVEDMEM_OF_DECLARE的定義,這里就不再描述了。
6、通過命令行參數保留CMA內存
arm64_memblock_init--->dma_contiguous_reserve函數中會根據命令行參數進行CMA內存的保留,本文暫不描述之,留給CMA文檔吧。
四、總結
物理內存布局是歸於memblock模塊進行管理的,該模塊定義了struct memblock memblock這樣的一個全局變量保存了memory type和reserved type的memory region list。而通過這兩個memory region的數組,我們就知道了操作系統需要管理的所有系統內存的布局情況。