参考了 《打通 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 如何寻址,如何管理,以及页表的映射和使用的过程,这里不多赘述,感兴趣的可以找操作系统相关课程学习,或者找 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.hstruct 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_NUMAint 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.cvoid __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:
-
setup_initial_init_mm- 初始化内核的第一个内存描述符
init_mm。 - 主要用于设置内核代码 (
_stext,_etext) 和数据段 (_edata,_end) 的内存范围。
- 初始化内核的第一个内存描述符
-
*cmdline_p = boot_command_line- 将系统启动时传递的命令行参数 (
boot_command_line) 赋值给cmdline_p指针。 - 这使得后续的内核组件能够访问和解析启动参数。
- 将系统启动时传递的命令行参数 (
-
early_fixmap_init- 初始化早期固定映射(fixmap)区域。
- 用于在启动初期为特定的、固定地址的内存区域建立虚拟地址映射,通常用于访问一些关键的硬件寄存器或数据结构。
-
early_ioremap_init- 初始化早期 I/O 内存重映射机制。
- 允许内核在启动初期安全地访问和映射设备 I/O 空间,例如外设控制器的寄存器。
-
setup_machine_fdt(__fdt_pointer)- 根据设备树(Flattened Device Tree, FDT)设置机器相关的参数和配置。
- 解析由
__fdt_pointer指向的设备树,从中获取硬件信息、设备配置等,以初始化系统。
-
arm64_memblock_init- 初始化 ARM64 架构的内存块(memblock)管理机制。
- 用于在内核启动早期跟踪和管理物理内存区域,包括可用内存和保留内存。
-
paging_init- 初始化页表和内存分页机制。
- 建立将物理内存映射到虚拟地址空间的页表结构,这是现代操作系统内存管理的基础。
-
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()进行映射,其中包括pgd、pud、pte等映射(书中这部分是否漏了pmd?),当映射完成后会返回dt_virt,并通过memblock_reserve()添加到memblock.reserved中。 early_init_dt_scan()通过解析DTB文件的memory节点获得可用物理内存的起始地址和大小,并通过类memblock_add的 API 向memory.regions数组添加一个memblock.region实例,用于管理这个物理内存的区域。
接着是 arm64_memblock_init 函数,其主要工作是将物理内存进行整理,将一些特殊区域添加到 reserved 内存中,主要是设备树中的:chosen, chosen(cma), reserved-memory, /memreserve, chosen(initrd) 节点。
这部分的代码工作大体将物理内存进行了分区和简单的管理,后续需要进行重要的 内存页表映射 完成物理地址到虚拟地址的映射,书中说系统完成初始化之后,所有的工作会移交给 Buddy 系统来进行内存管理。
—— juezhong 乙巳年丙戌月戊辰日 戌时