启动前期物理内存 在Linux启动时,物理内存整个系统是怎么被建立起来的。
比如一块ARM开发板配有1GB DDR,Linux内核也只是一个通用程序, 不能提前指导硬件内存布局,这部分由Bootloader / Firmware通过设备树将 硬件内存信息传递给内核。
当系统正常运行后,物理页主要由Buddy System管理。 这里就有一个问题,像初始化Buddy系统,必须指导物理内存布局;像用Buddy系统初始化物理内存,Buddy还没初始化。 于是又在早期引入memblock,负责Linux启动早期的物理地址区间管理器,同时提供简单的早期内存分配能力。 随后以页为单位管理物理内存。最后进入Buddy System。
内核是以页为基本单位对物理内存进行管理的,每页大小为 4K。一页大小的内存块在内核中用 struct page 结构体来进行管理,struct page 中封装了每页内存块的状态信息,比如:组织结构,使用信息,统计信息,以及与其他结构的关联映射信息等。
而为了快速索引到具体的物理内存页,内核为每个物理页 struct page 结构体定义了一个索引编号:PFN(Page Frame Number)。PFN 与 struct page 是一一对应的关系。
现在DTB里面携带了内存信息,Bootloader怎样把DTB交给ARM64 Linux? 不同的架构都有自己的head.S,看arm结构的
1 arch/arm64/kernel/head.S
代码注释里面有对应的解释
1 x0 = physical address to the FDT blob
x0存放的就是Bootloader阶段传过来的设备树存放的地址。
整个传递初始化流程如下
memblock早期管理内存系统 内核初始化完成后,物理内存的分配和回收都是由buddy系统和slab分类器管理, 在初始化阶段物理内存的分配和释放都是由memblock管理的。
1 2 3 4 5 6 7 struct memblock { bool bottom_up; phys_addr_t current_limit; struct memblock_type memory ; struct memblock_type reserved ; };extern struct memblock memblock ;
从定义就看出来分为两大部分。有了对象接下来就是一些操作函数了。 并且定义了一个全局变量。
1 2 3 4 5 6 7 struct memblock_type { unsigned long cnt; unsigned long max; phys_addr_t total_size; struct memblock_region *regions ; char *name; };
一个memblock_type类型包含多个regions,对应物理内存区域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct memblock_region { phys_addr_t base; phys_addr_t size; enum memblock_flags flags ; #ifdef CONFIG_NUMA int nid; #endif };
用于描述一段连续的物理内存分布,通过base+size的形式描述。也就是memblock管理的最小系统。
根据上面的数据结构,memblock通过memblock_type管理物理内存,每个memblock_type内存又很多memblock_region, 记录一段物理内存的信息,包括起始地址,大小和属性等,接下来看一些操作。首先是对象初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static struct memblock_region memblock_memory_init_regions [INIT_MEMBLOCK_MEMORY_REGIONS ] __initdata_memblock ;static struct memblock_region memblock_reserved_init_regions [INIT_MEMBLOCK_RESERVED_REGIONS ] __initdata_memblock ;#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP static struct memblock_region memblock_physmem_init_regions [INIT_PHYSMEM_REGIONS ]; struct memblock memblock __initdata_memblock = { .memory.regions = memblock_memory_init_regions, .memory.cnt = 1 , .memory.max = INIT_MEMBLOCK_MEMORY_REGIONS, .memory.name = "memory" , .reserved.regions = memblock_reserved_init_regions, .reserved.cnt = 1 , .reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS, .reserved.name = "reserved" , .bottom_up = false , .current_limit = MEMBLOCK_ALLOC_ANYWHERE, };
默认regions数量为128,即最大内存块个数。
在进行管理的时候就是对物理内存进行添加和移除。 memblock_add_range将进行一块物理内存的添加注册。 memblock_reserve将一块内存保留。
上面说了一大堆,都是在描述物理内存的分布和信息,真正执行物理内存申请的是memblock_alloc。 先看怎么使用的
1 2 3 4 5 6 void *ptr; ptr = memblock_alloc(16 * 1024 , 4096 );if (!ptr) { }
上面的意思就是分配一段至少16 KiB的连续物理内存,并且起始地址必须按照4 KiB对齐。
1 2 3 4 5 6 7 8 static __always_inline void *memblock_alloc (phys_addr_t size, phys_addr_t align) { return memblock_alloc_try_nid(size, align, MEMBLOCK_LOW_LIMIT, MEMBLOCK_ALLOC_ACCESSIBLE, NUMA_NO_NODE); }
会继续调用memblock_alloc_try_nid,memblock_find_in_range_node等包装函数。
与分配对应的就是memblock_free。
struct page 前面描述的主要是物理内存的分布,后面主要进入怎么从物理内存进入单页。 之前说过,内核以页为单位进行物理内存管理,将物理内存划分为一块一块的物理页, 每个页的大小为4KB。也就是我们的管理方式从region下降到page frame。 为了快速定位具体的物理页,每个物理页都有一个PFN(Page Frame Number),类似数组一样。
假设页大小是 4 KiB,那么每个物理页框中有4096个字节,使用页内偏移指向页内具体的字节。
两者的转换关系如下
具体转换过程受物理内存模型影响,物理内存模型表示内核中组织管理这些物理内存页 struct page 的方式。
FLATMEM平坦内存模型 特点:物理内存连续。 既然物理内存连续,那么物理地址也连续,使用一个数组就能维护。 示意如下:
内核中使用struct page *mem_map进行这些物理内存的组织,数据的序号就是物理页对应的PFN。
1 2 3 4 #if defined(CONFIG_FLATMEM) #define __pfn_to_page(pfn) (mem_map + ((pfn)-ARCH_PFN_OFFSET)) #define __page_to_pfn(page) ((unsigned long)((page)-mem_map) + ARCH_PFN_OFFSET) #endif
ARCH_PFN_OFFSET 是 PFN 的起始偏移量
普通SPARSEMEM模型 物理内存往往是不连续的,不连续的物理内存会形成内存空洞,如果为内存空洞也分配 struct page就会咋成很大的浪费。采用另一种管理方式,就是把连续的部分抽离出来 表示为一个section进行管理,示意图如下:
SPARSEMEM_VMEMMAP模型 用一块连续的虚拟地址空间vmemmap管理struct page
上面都是从软件角度看的,我们看一下真实的物理内存情况。
上面的图表示的一致性内存访问 UMA(Uniform Memory Access),所有的 CPU 访问内存都要过总线,而且距离都是一样的,由于所有 CPU 对内存的访问距离都是一样的,所以在 UMA 架构下所有 CPU 访问内存的速度都是一样的。这种访问模式称为 SMP(Symmetric multiprocessing),即对称多处理器。
相对的另一种模式非一致性内存访问 NUMA 架构
内核通过struct pglist_data数据结构描述NUMA节点,并使用一个全局数组管理所有的Node节点。 数组大小为MAX_NUMNODES 。
1 extern struct pglist_data *node_data [];
整个数据结构的核心如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 typedef struct pglist_data { struct zone node_zones [MAX_NR_ZONES ]; struct zonelist node_zonelists [MAX_ZONELISTS ]; int nr_zones;#ifdef CONFIG_FLATMEM struct page *node_mem_map ;#endif unsigned long node_start_pfn; unsigned long node_present_pages; unsigned long node_spanned_pages; int node_id; wait_queue_head_t kswapd_wait; struct task_struct *kswapd ; int kswapd_order; enum zone_type kswapd_highest_zoneidx ; struct lruvec __lruvec ; } pg_data_t ;
pg_data_t是Linux用于描述一个内存Node的核心数据结构。
一个 Node 中的物理内存,被划分为多个Zone
每个Zone都维护自己的Buddy空闲链表。
其分类如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 enum zone_type {#ifdef CONFIG_ZONE_DMA ZONE_DMA, #endif #ifdef CONFIG_ZONE_DMA32 ZONE_DMA32, #endif ZONE_NORMAL, #ifdef CONFIG_HIGHMEM ZONE_HIGHMEM, #endif ZONE_MOVABLE, #ifdef CONFIG_ZONE_DEVICE ZONE_DEVICE, #endif __MAX_NR_ZONES };
说了这么多不过是把物理内存分为不同的粒度进行管理 Node,Zone和Page的关系如下
Node:决定内存离CPU的距离
Zone:决定内存的限制
Page:具体的物理页
在代码上的关系
一个Zone管理一段PFN范围,源码
1 2 3 4 5 6 7 8 9 10 11 struct zone { ... unsigned long zone_start_pfn; atomic_long_t managed_pages; unsigned long spanned_pages; unsigned long present_pages; ... };
空闲页的管理受结构体影响free_area
1 2 3 4 5 struct zone { ... struct free_area free_area [MAX_ORDER ]; ... };
数组中每一个元素管理统一大小页的空闲块,是Buddy System的核心数据结构。 数据下标记为order,则每个元素管理的空闲块的大小为2^order个页面。 每个空闲快用链表链接。
Buddy System的核心职责是: 按照不同大小,组织每个Zone中的连续空闲物理页块。 它不是把所有空闲页都看成相同的单页,而是组织成2^order页。
1 2 3 4 5 6 7 8 struct zone └── free_area[MAX_ORDER] ├── free_area[0] → 1 页连续块: [页] [页] ... ├── free_area[1] → 2 页连续块: [页][页] ... ├── free_area[2] → 4 页连续块: [页][页][页][页] ... ├── free_area[3] → 8 页连续块: [页][页][页][页][页][页][页][页] ... └── ... free_area[order] → 每块有 2^order 个连续物理页