参考了 《打通 Linux 操作系统和芯片开发》 书籍的内容,实际也可以说是完全参照加上了个人的拙见或者是读书记录。
和我上一篇说的一样,我依然还是一个初学者,记录这些是自己梳理,以及想让文字发挥一些作用和意义。涉及到代码的部分实在是非常非常的枯燥无味和无聊,并且由于 Linux 中函数的分层很多,call stack 特别深,函数名称特别相似,
非常容易头晕眼花了,所以还是应该采取从宏观角度的,抓住关键的函数调用链来进行分析和理解,不应该要求细节到某一行代码的程度,
否则陷入难解的困境了。
这应该是一个很经典的问题,内存池 (Memory Pool) 也可以认为是一种内存管理的方式,所以关于内存管理四个字有点像谜底就在谜面上,更多的只是你如何管理的方式。
比如 FreeRTOS 中的好几种分配方式,常用的只是 heap_4.c 的方式,这种用在 MCU 上的方式可以比较简单,而对于现代的 2025 年的 MCU 可能依然还是比较小的内存,
至少没有上升到 4GB ,至少我还没接触到。并且芯片性能可能不强,无法负责和管理这么多的内存(后面出现了一个东西专门辅助此工作),所以操作系统采取了其他的方式来管理。
MCU 跑的都是在很小的内存中,大部分直接都是访问了物理地址,
所以简单的说为什么的原因,就是为了更好的利用和使用内存 这个相对比较快速的可以存数据的东西,才出现了内存管理的各种方式,一般在操作系统课程中都会提到在演进中出现的:
两种经典的方式,也可能听到 段页式 就是两种合并在一起说的。
但 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)两个动作。
这部分有比较多的图和代码,太麻烦了,尝试通过文字来简单的叙述看看
Linux 中通过 Buddy 伙伴系统和 slab 分配器来分配和管理内存的,但是在此之前不可用的阶段,就由 memblock 来承担了初始化和管理的工作,所以自然的就想到这个阶段的 memblock 直接就是访问和管理的物理地址,
memblock 是唯一能做早期启动阶段管理内存的内存分配器,由此出现了 early boot memory 阶段的名称,是系统启动中间阶段的内存管理,这里涉及的内存模型不多赘述。
书籍解析了代码的结构,只是简单解析了各部分的字段含义,详细可以直接让 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:
setup_initial_init_mm
init_mm。_stext, _etext) 和数据段 (_edata, _end) 的内存范围。*cmdline_p = boot_command_line
boot_command_line) 赋值给 cmdline_p 指针。early_fixmap_init
early_ioremap_init
setup_machine_fdt(__fdt_pointer)
__fdt_pointer 指向的设备树,从中获取硬件信息、设备配置等,以初始化系统。arm64_memblock_init
paging_init
bootmem_init
接着继续分析了 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 sizen"
"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: %sn", name);
dump_stack_set_arch_desc("%s (DT)", name);
}
该函数主要功能是:
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 乙巳年丙戌月戊辰日 戌时
「夫人神好清,而心擾之,人心好靜,而慾牽之。」
本文来自博客园,作者:纵然似梦,转载请注明原文链接:https://www.cnblogs.com/juezhong/p/19167298
登录查看全部
参与评论
手机查看
返回顶部