[内核学习]物理内存组织

/ 0评 / 0赞

从这一章起进内存管理.地基是物理内存怎么被组织和记账:每个物理页一张"户口页"struct page,按 zone 分区,按 node 分组,底层再用 SPARSEMEM 把不连续的物理内存缝成一张虚拟连续的账本.这篇把这些结构在板上逐个读出来,包括一个出乎意料的 zone 分布,和 1.4 那场"常量错位"事故的续集.

这篇回答几件事:

先看户口本.内核不直接管理字节,管理的是页:物理内存按 4KB 一格切开,每格配一个 struct page,这一整本数组叫 memmap.它自己就是一笔不小的开销,实测数据:

sizeof(struct page) = 64 字节
totalram_pages = 508954 (1988 MB)
memmap 开销 = 508954 × 64B = 31 MB

2GB 内存,31MB 拿去记账,约 1.56% — 这是无法关闭的固定税.struct page 里塞了什么:引用计数,映射计数,flags(脏,活跃,LRU 归属...),以及一大坨按用途复用的 union — 单独一页的"状态"居然要 64 字节,因为它身兼数职:可以是文件页,匿名页,slab 页,空闲页,每个角色要记的信息都挤在这一个结构里.这也是近两年 folio 改造的动机之一:把"至少一页"和"恰好一页"的概念分开,让大页不必伪装成一大群小页.

memmap 数组本身放在哪,引出 SPARSEMEM.老的 FLATMEM 假设物理内存连续,memmap 就是一根直线;真实 SoC 的内存洞多得是,还有内存热插拔,于是有了 SPARSEMEM:物理地址切成 mem_section 段,每段独立的 memmap,存在才登记.板上配置 SPARSEMEM_VMEMMAP=y,这是它的高级形态:把所有段的 memmap 再映射进一段虚拟连续区域,struct page 指针又能当数组用了 — 代价是 pfn_to_page 依赖一个固定的虚拟基址 vmemmap.

然后是 1.4 那场事故的续集.当时发现编译期 PAGE_OFFSET(52 位布局)和运行期线性映射(48 位)错位,模块里不能手拼地址.这次做 pfn 和 struct page 的往返,又撞上同款:vmemmap 的编译期常量 VMEMMAP_START 是 0xffffc1ffc0000000,运行期实测数组基址在 0xfffffdffc0000000 — 又错位.解法同款自校准:vmalloc_to_page 给 struct page 指针,vmalloc_to_pfn 给页号,一减就是运行期真基址.用它做往返验证,alloc_page 拿到 pfn 70514,自校准的 pfn_to_page 算出的指针和真实 struct page 差为零 — 自校准闭环.这条线的教训在模块开发里通用:这种"配置 52 运行 48"的内核上,布局常量一概不信,导出函数现算.

zone 是物理页的第二层组织,按"用途约束"分区.直觉上 2GB 内存应该落在 DMA32(4GB 以下),实测反直觉:

Node 0, zone DMA:     managed 508954 (全部!)
Node 0, zone DMA32:   managed 0
Node 0, zone Normal:  managed 0

全在 DMA.原因是 arm64 的 zone 上界跟平台走:ZONE_DMA 的边界取自设备实际能 DMA 到达的范围,RK3399 的设备 32 位全能摸到,内核就把整个 2GB 划进 DMA,DM A32 和 Normal 空着. x86 上 DMA 只有 16MB 是历史 ISA 的锅,arm 这边同名不同命 — 读 zoneinfo 别带 x86 的成见.分配器按 zone 从高往低降级:Normal 没有,要 GFP_HIGHUSER 的分配自动落 DMA32,再落 DMA;ZONE_MOVABLE 是给内存热插拔和巨型页预留的"可搬家区",没开就空.

node 是第三层,NUMA 的单位.板上没编 NUMA,/sys/devices/system/node 目录都不存在,但 zoneinfo 依然以 "Node 0" 组织 — 单节点是 NUMA 退化为一点的特殊情形,数据结构同一套.真 NUMA 机器上每个 node 一份 pglist_data,各自挂着 zone 链,跨 node 访存有距离税,调度器和分配器都有整套对策,这里立个牌子,后文碰到再说.

启动早期还有个临时工:memblock.内核刚起来时 buddy 和 memmap 都还没建,memblock 用最朴素的 region 数组把"固件报的内存"和"要保留的内存"(内核映像,DTB,initrd,固件保留区)先记下来 — 冷启动日志里 "Memory: 2026404K/2095104K available" 那行就是 memblock 完成使命前的对账.随后 memblock_free_all 把没被保留的区域整批灌进 buddy,自己功成身退,只留 dmesg 里的几行痕迹.调试启动期内存问题(比如固件偷偷保留了一大块)时,memblock=debug 参数能让它把每笔账吐出来.

最后留几个容易忽略的细节:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注