物理内存管理涉及到物理内存的上报发现、物理内存到逻辑的映射、分配释放、反碎片、迁移等。
Linux Kernel中相关的概念有node,pglist_data, zone, memblock,page,page frame,mem_section,,,等。
物理内存上报和发现
一般内存物理部署结构如下图,内存可能连在北桥上,也可能直接连到socket上,所有内存都能被socket共用。

物理内存的描述一般用DeviceTree,或者UEFI的get_memory_map以及SRAT表传递给Linux Kernel。
DeviceTree
如果是DeviceTree启动的话,一般通过memory节点描述物理内存信息,例子如下:
memory@80000000 {
device_type = "memory";
reg = <0x00000000 0x80000000 0 0x80000000>;
/* DRAM space - 1, size : 2 GB DRAM */
};
Linux Kernel启动过程中,DTB的物理地址作为参数传递给内核,我们先跳过fixmap的概念,了解下DTB发现物理内存大致流程:

UEFI/ACPI
如果通过UEFI启动的话,Linux Kernel会通过drivers/firmware/efi/libstub中的efi_boot_kernel函数调用UEFI的get_memory_map
找到物理内存相关信息,并按照UEFI的启动协议,准备或者更新DeviceTree,
Linux Kernel通过DTB中的uefi-mmap-start节点得到最终的物理内存信息,注意这个地址里面是一个链表,
存放着一组efi_memory_desc_t结构体,这个结构体中描述了每片物理内存的物理起始地址和大小,
更详细的可以参考UEFI代码,或者(QEMU代码)[https://github.com/qemu/qemu/blob/master/hw/arm/virt.c#L2322], 或者Linux Kernel的Stub代码。
UEFI启动流程,内存节点上报流程关键调用栈如下:
efi_boot_kernel
efi_bs_call get_memory_map
update_fdt
start_kernel
setup_arch
efi_init
reserve_regions
memblock_add
示例图如下:

fixmap
所谓fixmap,指的是虚拟地址中的一段区域,该区域中所有地址在编译阶段就已经确定好了,在Linux Kernel启动阶段,
内核会把这些虚拟地址映射到物理地址上。之所以引入fixmap,是因为在内核启动过程中,某些模块需要使用虚拟地址,
但是这时候内核还没有完全启动,并没有完成虚拟地址到物理地址映射这个复杂的过程,
所以一个简化的虚拟内存的分配和管理的机制就被引入了,也就是要说的fixmap。
比如说启动的时候,虽然传递了DTB的物理地址,但Linux Kernel还没有建立物理地址到虚拟地址的映射,还不能读取,
这时候就可以通过fixmap机制,读取解析DTB文件,找到物理内存节点,建立struct memblock,
整个流程可以参考DeviceTree章节中的流程图。还比如说早期的early_ioremap来访问外设的寄存器等。fixmap中各个地址区间的定义可以参考代码fixmap.h。
上面这个过程结束后,所有的物理内存都可以通过memblock、memblock_region等结构体呈现了,示意图如下:

配套的日志和debug接口信息如下:

现在已经可以通过memblock_allock分配物理内存了,但是还不能通过虚拟地址来访问这块物理内存,而且cpu、numa和zone
的信息已经可以获取到,接下来就是建立虚拟地址到物理地址的互相映射了,并把物理页面挂到不同cpu节点的zone里面了。
物理内存到内核逻辑概念的映射
要想使用物理内存,自然就会想到把这段物理内存地址映射到虚拟地址,于是就有了页表的建立。
另外呢,物理内存一般被划分成page frame来管理,对应到内核中的概念struct page。
一般一个虚拟地址其实是落到单个struct page中的offset,而物理地址同样也会落到单个page frame中,
所以虚拟地址到物理地址的转换,其实也可以复用到page到page frame的映射。

虚拟地址和页表
在ARM64平台上,以4KB页表页表寻址过程如下图:

其中table\block\page描述符和虚拟地址格式如下图:

以一个虚拟地址为例话,地址翻译的过程如下:

建立页表
建立页表的输入是memblock,也就是物理内存的起始地址和结束地址,输出是空的页表。
memblock是在2010年Yinghai提出的。有兴趣的可以看一下当时的邮件列表中的讨论。
建立页表这个过程发生在paging_init,map_kernel和map_mem几个函数中。
其中map_kenrel是完成kernel各个段的映射,虚拟地址的信息也可以从System.map或者vmlinux中查到。
而map_mem则完成前面发现的memregion的物理地址到虚拟地址的映射。这两个函数都是通过__create_pgd_mapping创建页表,建立的映射。
这时候最底层的页表并没有pte,pte的创建在发生缺页的时候建立。
具体流程如下:

arm64上页表内存属性设置
ARM上内存属性主要通过MAIR寄存器来获取。
dump内核态页表
kernel的页表,可以通过内核内置的”ptdump”功能导出,它依赖下面的config
CONFIG_PTDUMP=y
CONFIG_PTDUMP_DEBUGFS=y
编译好后,使用命令sudo mount -t debugfs none /sys/kernel/debug 挂载到debugfs之后,执行cat /sys/kernel/debug/kernel_page_tables命令会输入页表映射信息。

dump用户态指定进程页表
想要dump指定进程的页表,可以想办法获取进程的”struct mm”,或者解析/proc/pid下的maps、smaps和pagemap获取,结合/proc/kpageflags解析
在内核tools/mm下也内置了一个工具page-types,可以dump进程的页表,使用命令make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- O=../kernel-dev.build tools/mm 编译生成就好。
或者通过以下shell脚本解析也行。
#!/bin/bash
cat /proc/$1/maps | while read line
do
echo $1-${line}
echo $1-${line} | awk '{print $1}' | (
IFS=- read pid start end
start=$(( 0x${start} ))
end=$(( 0x${end} ))
addr=${start}
while [ ${addr} -lt ${end} ]
do
printf "%08x: " ${addr}
dd if=/proc/$pid/pagemap bs=8 skip=$(( addr / 4096 )) count=1 2>/dev/null | od -v -t x8 -A none
addr=$(( addr + 4096 ))
done
)
done

page frame到page的映射
页表框架搭起来之后,就到了把物理内存转换到内核物理内存逻辑概念的阶段,目前内核管理物理内存有四种模型,但主要使用sparse模型。

以ARM64为例,具体函数在bootmem_init,其中:
arm64_numa_init负责建立numa的cpu节点arm64_memory_prenset负责把大的memblock、memblock_region拆成小的mem_section来管理,并和cpu节点关联起来sparse_init把物理page frame和mem_section、struct page关联起来,这样通过物理page frame number就可以找到具体的struct pagezone_sizes_init初始化各个cpu节点的zone信息。
这时候,memblock、memblock_region、mem_section和struct page关系如下:

在这种模型下,物理page frame number转换到struct page的过程如下:

一个PFN到Page的运行实例如下图:

物理地址到page frame的转换实例
物理地址到PFN的转换按照前面的介绍,就是物理地址左移PAGE_SHIFT page的大小就可以了。以4K为例:

virtual address到Physical address的映射
一个virtual address到Physical address的运行实例如下图:

为什么内核中有的物理地址对应到了2个虚拟地址
内核中虽然只有一份页表,但是有几种映射关系,这也是kernel logic address(kmalloc)和kernel virtual address(vmalloc),以及kmap之间的关系。
在LDD3中也有描述:

以jiffies为例,在crash工具中,vtop通常返回kernel logic address,但是可以通过kmem看物理地址对应的虚拟地址。

把page加到zone
接下来就是把struct page放到各个cpu节点zone下面的free_area中,就到了zone page frame allocator阶段。
函数调用栈和实例如下:

到此,物理内存已经可以基于CPU的拓扑结构,在zone的基础上,基于page来分配了,结构如下:

运行实例图如下:

后面就是内存伙伴buddy系统的事情了。

参考
- linux内存管理
- 内存管理源码分析-内核页表的创建以及索引方式(基于ARM64以及4级页表)
- Linux内存管理(三):“看见”物理内存
- Linux内存管理(四):paging_init分析
- Linux物理内存初始化
- Linux Memory Managment Frequently Asked Questions
- Physical Memmory Management
- setup_arch:bootmem_init : sparse_init
- ARM64 Kernel Image Mapping的变化
- 内存开机在干嘛? -memblock
- Linux内存管理(四):paging_init分析
- Fix-Mapped Addresses
- linux-kernel-labs
- A little bit about a linux kernel
- d42_5_overview_of_the_vmsav8-64_address_translation
- Armv8-A Address Translation
- Linuxでのpage構造体群の配置
- Linux Kernel Memory Hacking
- 内存是怎么映射到物理地址空间的?内存是连续分布的吗?
- Linux crash工具结合/dev/mem任意修改内存
- ARMv8 MMU及Linux页表映射
- 用crash tool观察ARM64 Linux地址转换
- Memory Management in Linux
- Minimizing struct page overhead
- 内存管理源码分析-内核页表的创建以及索引方式(基于ARM64以及4级页表)
- Linux Memory Pressure - 1
- Linux Rootkits
- Linux Kernel中AEP的现状和发展
- 解决Linux内核问题实用技巧之-dev/mem的新玩法
- User-space control of memory management
- The 2023 LSFMM+BPF Summit
- Reducing page structures for huge pages
- 原始内存分配器–memblock
- mair_el1和页表关联
- MAIR_ELX总结
- armv8 cacheable/shareable
- AArch64 memory and paging
- page-table-kernel-exploitation
- 用户态进程如何得到虚拟地址对应的物理地址
- Memory Deep Dive Summary
- Dump PTE with SystemTap
- 面向应用开发者的系统指南》CPU篇之使用systemtap分析进程的行为
- 内存碎片指数