Stories

Detail Return Return

Chapter-1 Memory Management (section 1.1-1.5) - Stories Detail

參考了 《打通 Linux 操作系統和芯片開發》 書籍的內容,實際也可以説是完全參照加上了個人的拙見或者是讀書記錄。
和我上一篇説的一樣,我依然還是一個初學者,記錄這些是自己梳理,以及想讓文字發揮一些作用和意義。

涉及到代碼的部分實在是非常非常的枯燥無味和無聊,並且由於 Linux 中函數的分層很多,call stack 特別深,函數名稱特別相似,
非常容易頭暈眼花了,所以還是應該採取從宏觀角度的,抓住關鍵的函數調用鏈來進行分析和理解,不應該要求細節到某一行代碼的程度,
否則陷入難解的困境了。

目錄
  • 操作系統為什麼需要內存管理?
  • 一級、二級頁表映射過程
  • memblock 物理內存的初始化
    • memblock 的作用
    • memblock 的數據結構及代碼分析

操作系統為什麼需要內存管理?

這應該是一個很經典的問題,內存池 (Memory Pool) 也可以認為是一種內存管理的方式,所以關於內存管理四個字有點像謎底就在謎面上,更多的只是你如何管理的方式。
比如 FreeRTOS 中的好幾種分配方式,常用的只是 heap_4.c 的方式,這種用在 MCU 上的方式可以比較簡單,而對於現代的 2025 年的 MCU 可能依然還是比較小的內存,
至少沒有上升到 4GB ,至少我還沒接觸到。並且芯片性能可能不強,無法負責和管理這麼多的內存(後面出現了一個東西專門輔助此工作),所以操作系統採取了其他的方式來管理。

MCU 跑的都是在很小的內存中,大部分直接都是訪問了物理地址,
所以簡單的説為什麼的原因,就是為了更好的利用和使用內存 這個相對比較快速的可以存數據的東西,才出現了內存管理的各種方式,一般在操作系統課程中都會提到在演進中出現的:

  • 分段機制(Segmentation)
  • 分頁機制(Paging)

兩種經典的方式,也可能聽到 段頁式 就是兩種合併在一起説的。

Linux 採取的是分頁的機制,同時根據書中描述是 四級頁表 的形式,關於分段、分頁的一些説明和概述及原理性這裏就不詳細説明,可以詢問 ChatGPT 或是查看其他的文章,
這裏僅添加一些可能重要的名詞:

名詞 翻譯
換入 Swapping In
換出 Swapping Out
頁面 Page
物理頁面 Physical Page
頁幀 Page Frame
頁幀號 Page Frame Number (PFN)
虛擬頁幀號 Virtual Page Frame Number (VPN)
物理頁幀號 Physical Page Frame Number (PFN)
虛擬頁面 Virtual Page
頁表 Page Table (PT)
頁表項 Page Table Entry (PTE)
內存管理單元 Memory Management Unit (MMU)
虛擬地址 Virtual Address
物理地址 Physical Address
轉譯後備緩衝器(快表) Translation Lookaside Buffer (TLB)
頁全局目錄 Page Global Direcotry (PGD)
頁上級目錄 Page Upper Directory (PUD)
頁中間目錄 Page Middle Direcotry (PMD)
頁表 Page Table (PTE)
上面出現了但含義不一樣
這裏主要指Linux中多級頁表的
頁內偏移 Page offset

上面提到了一個專門輔助 Linux 做內存管理的東西就是 MMU 了(用來將虛擬地址轉換成物理地址),現代的芯片一般都是將 MMU 內置在芯片中了,這是 Linux 運行的必備條件,所以 區別一個芯片能不能運行 Linux 系統,就是芯片有沒有 MMU 這個模塊了。 (當然現在也可以不用 MMU 也能運行 Linux 了只不過是功能受限

關於 MMU 如何尋址,如何管理,以及頁表的映射和使用的過程,這裏不多贅述,感興趣的可以找操作系統相關課程學習,或者找 408 相關的學習視頻參考。
另外重點是 Linux 一個頁面的大小為 4KB 至於為什麼是 4KB 的大小,AI 給出的答案是歷史原因;還有就是想要運行 Linux 就需要 MMU 這個模塊;

Linux 內存架構和模型略過,對理解關係不大不太重要

一級、二級頁表映射過程

感覺實在是有必要的畫一個一級、二級的頁表映射的過程

二級頁表只是在一級的基礎上做了修改,添加了一個對一級頁表的索引表,這樣能對應的一級頁表項就會更多了,而 Linux 用了四級來管理,數量就不計算了,同時這只是大致的示意,不代表就是這樣的尋址。

對於 Linux 的多級頁表管理不準備畫圖了,只不過是更多級,更復雜,更多控制位,更長的地址長度,但基本的方式是一樣的。
簡單説明就是首先將一個包含了虛擬頁幀號的虛擬地址(線性地址)通過一系列查表的方式(查表的這個動作也可以是 MMU 在執行)轉換成一個包含了物理頁幀號的物理地址,(這裏假設用到了 TLB ) 然後 MMU 通過查 TLB 快速的知道了物理頁幀號與內存的某一塊位置的對應關係,
然後就使用一下偏移量,在這一頁中的偏移多少,就知道了這個虛擬地址的數據內容是多少了。
另外在這個過程中可能會產生一個 缺頁中斷 (Page Fault) ,簡單説明即是在代碼中使用 malloc 申請內存空間是在虛擬地址空間中,此時隨便申請,實際上物理的內存條上對應映射的空間並不存在,或者説並沒有數據內容,
或者當訪問的頁不在任何一個頁表中,這個時候出現了缺頁中斷,此時才會從存儲中加載到內存中,或者是正式的分配內存,這時候內存條上就有了空間和數據內容了,那麼這正好也有頁的換入(Swaping In)和換出(Swaping Out)兩個動作。

memblock 物理內存的初始化

這部分有比較多的圖和代碼,太麻煩了,嘗試通過文字來簡單的敍述看看

memblock 的作用

Linux 中通過 Buddy 夥伴系統和 slab 分配器來分配和管理內存的,但是在此之前不可用的階段,就由 memblock 來承擔了初始化和管理的工作,所以自然的就想到這個階段的 memblock 直接就是訪問和管理的物理地址,
memblock 是唯一能做早期啓動階段管理內存的內存分配器,由此出現了 early boot memory 階段的名稱,是系統啓動中間階段的內存管理,這裏涉及的內存模型不多贅述。

memblock 的數據結構及代碼分析

書籍解析了代碼的結構,只是簡單解析了各部分的字段含義,詳細可以直接讓 AI 生成,註釋內容 Gemini 2.5 Flash 生成:

include/linux/memblock.h

struct memblock {
	bool bottom_up;					/* 是否是自底向上? */
	phys_addr_t current_limit;		/* 當前限制地址 */
	struct memblock_type memory;	/* 可用內存區域 */
	struct memblock_type reserved;	/* 保留內存區域 */
};

struct memblock_type {
	unsigned long cnt; 				 /* 區域計數 */
	unsigned long max; 				 /* 最大區域數 */
	phys_addr_t total_size; 		 /* 總大小 */
	struct memblock_region *regions; /* 區域數組 */
	char *name;						 /* 類型名稱 */
};

struct memblock_region {
	phys_addr_t base;		 	/* 區域基地址 */
	phys_addr_t size; 			/* 區域大小 */
	enum memblock_flags flags; 	/* 區域標誌 */
#ifdef CONFIG_NUMA
	int nid; /* NUMA節點ID */
#endif
};

enum memblock_flags {
	MEMBLOCK_NONE			= 0x0,	/* 無特殊請求 */
	MEMBLOCK_HOTPLUG		= 0x1,	/* 可熱插拔區域 */
	MEMBLOCK_MIRROR			= 0x2,	/* 鏡像區域 */
	MEMBLOCK_NOMAP			= 0x4,	/* 不添加到內核直接映射 */
	MEMBLOCK_DRIVER_MANAGED = 0x8,	/* 總是通過驅動檢測 */
	MEMBLOCK_RSRV_NOINIT	= 0x10,	/* 不初始化struct pages */
};

接着從 stark_kernel 入手,主要關注了 setup_arch 函數,參數是 command_line,貼出函數:

// 只給出相對重要的函數調用
arch/arm64/kernel/setup.c

void __init __no_sanitize_address setup_arch(char **cmdline_p)
{
	setup_initial_init_mm(_stext, _etext, _edata, _end);

	*cmdline_p = boot_command_line;

	... ...
	early_fixmap_init();
	early_ioremap_init();

	setup_machine_fdt(__fdt_pointer);
	... ...

	arm64_memblock_init();

	paging_init();
    ... ...
	bootmem_init();

	... ...
}

Gemini 2.5 Flash:

  1. setup_initial_init_mm

    • 初始化內核的第一個內存描述符 init_mm
    • 主要用於設置內核代碼 (_stext, _etext) 和數據段 (_edata, _end) 的內存範圍。
  2. *cmdline_p = boot_command_line

    • 將系統啓動時傳遞的命令行參數 (boot_command_line) 賦值給 cmdline_p 指針。
    • 這使得後續的內核組件能夠訪問和解析啓動參數。
  3. early_fixmap_init

    • 初始化早期固定映射(fixmap)區域。
    • 用於在啓動初期為特定的、固定地址的內存區域建立虛擬地址映射,通常用於訪問一些關鍵的硬件寄存器或數據結構。
  4. early_ioremap_init

    • 初始化早期 I/O 內存重映射機制。
    • 允許內核在啓動初期安全地訪問和映射設備 I/O 空間,例如外設控制器的寄存器。
  5. setup_machine_fdt(__fdt_pointer)

    • 根據設備樹(Flattened Device Tree, FDT)設置機器相關的參數和配置。
    • 解析由 __fdt_pointer 指向的設備樹,從中獲取硬件信息、設備配置等,以初始化系統。
  6. arm64_memblock_init

    • 初始化 ARM64 架構的內存塊(memblock)管理機制。
    • 用於在內核啓動早期跟蹤和管理物理內存區域,包括可用內存和保留內存。
  7. paging_init

    • 初始化頁表和內存分頁機制。
    • 建立將物理內存映射到虛擬地址空間的頁表結構,這是現代操作系統內存管理的基礎。
  8. bootmem_init

    • 初始化 bootmem 分配器。
    • 這是一個簡單的物理內存分配器,在內核啓動的早期階段(分頁機制剛建立,但更復雜的內存管理系統尚未完全初始化時)用於分配物理內存。

接着繼續分析了 setup_machine_fdt 函數:

static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
	int size;
	void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
	const char *name;

	if (dt_virt)
		memblock_reserve(dt_phys, size);

	if (!early_init_dt_scan(dt_virt, dt_phys)) {
		pr_crit("\n"
			"Error: invalid device tree blob at physical address %pa (virtual address 0x%px)\n"
			"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
			"\nPlease check your bootloader.",
			&dt_phys, dt_virt);

		while (true)
			cpu_relax();
	}

	/* Early fixups are done, map the FDT as read-only now */
	fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);

	name = of_flat_dt_get_machine_name();
	if (!name)
		return;

	pr_info("Machine model: %s\n", name);
	dump_stack_set_arch_desc("%s (DT)", name);
}

該函數主要功能是:

  • 拿到 DTB 的物理地址後,會通過 fixmap_remap_fdt() 進行映射,其中包括 pgdpudpte 等映射(書中這部分是否漏了 pmd ?),當映射完成後會返回 dt_virt,並通過 memblock_reserve() 添加到 memblock.reserved 中。
  • early_init_dt_scan() 通過解析 DTB 文件的 memory 節點獲得可用物理內存的起始地址和大小,並通過類 memblock_add 的 API 向 memory.regions 數組添加一個 memblock.region 實例,用於管理這個物理內存的區域。

接着是 arm64_memblock_init 函數,其主要工作是將物理內存進行整理,將一些特殊區域添加到 reserved 內存中,主要是設備樹中的:chosenchosen(cma)reserved-memory/memreservechosen(initrd) 節點。

這部分的代碼工作大體將物理內存進行了分區和簡單的管理,後續需要進行重要的 內存頁表映射 完成物理地址到虛擬地址的映射,書中説系統完成初始化之後,所有的工作會移交給 Buddy 系統來進行內存管理。

—— juezhong 乙巳年丙戌月戊辰日 戌時

Add a new Comments

Some HTML is okay.