1. 程式人生 > >Linux內存初始化(三) 內存布局

Linux內存初始化(三) 內存布局

也會 mat 註冊 情況 align else if mod 而在 ech

一、前言

同樣的,本文是內存初始化文章的一份補充文檔,希望能夠通過這樣的一份文檔,細致的展示在初始化階段,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的數組,我們就知道了操作系統需要管理的所有系統內存的布局情況。

Linux內存初始化(三) 內存布局