Linux物理内存管理

物理内存管理涉及到物理内存的上报发现、物理内存到逻辑的映射、分配释放、反碎片、迁移等。
Linux Kernel中相关的概念有nodepglist_data, zone, memblockpagepage framemem_section,,等。

物理内存上报和发现

一般内存物理部署结构如下图,内存可能连在北桥上,也可能直接连到socket上,所有内存都能被socket共用。

physical memory arch

物理内存的描述一般用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发现物理内存大致流程:

FixedMap DTB Memoryblock

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

示例图如下:

memblock boot from acpi

fixmap

所谓fixmap,指的是虚拟地址中的一段区域,该区域中所有地址在编译阶段就已经确定好了,在Linux Kernel启动阶段,
内核会把这些虚拟地址映射到物理地址上。之所以引入fixmap,是因为在内核启动过程中,某些模块需要使用虚拟地址,
但是这时候内核还没有完全启动,并没有完成虚拟地址到物理地址映射这个复杂的过程,
所以一个简化的虚拟内存的分配和管理的机制就被引入了,也就是要说的fixmap

比如说启动的时候,虽然传递了DTB的物理地址,但Linux Kernel还没有建立物理地址到虚拟地址的映射,还不能读取,
这时候就可以通过fixmap机制,读取解析DTB文件,找到物理内存节点,建立struct memblock
整个流程可以参考DeviceTree章节中的流程图。还比如说早期的early_ioremap来访问外设的寄存器等。
fixmap中各个地址区间的定义可以参考代码fixmap.h

上面这个过程结束后,所有的物理内存都可以通过memblockmemblock_region等结构体呈现了,示意图如下:

memblock and region

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

memblock and region2

现在已经可以通过memblock_allock分配物理内存了,但是还不能通过虚拟地址来访问这块物理内存,而且cpu、numa和zone
的信息已经可以获取到,接下来就是建立虚拟地址到物理地址的互相映射了,并把物理页面挂到不同cpu节点的zone里面了。

物理内存到内核逻辑概念的映射

要想使用物理内存,自然就会想到把这段物理内存地址映射到虚拟地址,于是就有了页表的建立。
另外呢,物理内存一般被划分成page frame来管理,对应到内核中的概念struct page
一般一个虚拟地址其实是落到单个struct page中的offset,而物理地址同样也会落到单个page frame中,
所以虚拟地址到物理地址的转换,其实也可以复用到pagepage frame的映射。

page frame and page

虚拟地址和页表

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

page table walking

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

page table walking

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

page table walking

建立页表

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

具体流程如下:

page table directory create

page frame到page的映射

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

physical memory models

以ARM64为例,具体函数在bootmem_init,其中:

  • arm64_numa_init 负责建立numa的cpu节点
  • arm64_memory_prenset 负责把大的memblockmemblock_region拆成小的mem_section来管理,并和cpu节点关联起来
  • sparse_init 把物理page frame和mem_sectionstruct page关联起来,这样通过物理page frame number就可以找到具体的struct page
  • zone_sizes_init初始化各个cpu节点的zone信息。

这时候,memblockmemblock_regionmem_sectionstruct page关系如下:

physical memory model

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

physical frame to page

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

physical frame to page2

物理地址到page frame的转换实例

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

physical to pfn

virtual address到Physical address的映射

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

virtual address to physical address

为什么内核中有的物理地址对应到了2个虚拟地址

内核中虽然只有一份页表,但是有几种映射关系,这也是kernel logic address(kmalloc)和kernel virtual address(vmalloc),以及kmap之间的关系。

LDD3中也有描述:

address types used in linux

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

address types used in linux2

把page加到zone

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

physical memory model

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

physical memory model

运行实例图如下:

physical memory model

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

zone frame allocator

参考

知道是不会有人点的,但万一有人呢:)