目录
3.4 初始化内存管理
3.4.1 建立数据结构
3.4.2 特定于体系架构的设置
内核在内存中的布局
初始化步骤
分页机制的初始化
3.4.3 启动期间的内存管理
数据结构
初始化
与内核的接口
停用bootmem分配器
释放初始化数据
3.4 初始化内存管理
包括:
显式设置内存模型。
确认NUMA中内存总数量。
初始化内存节点。
3.4.1 建立数据结构
只有1个节点时(如UMA中),nid是伪参数。
#define NODE_DATA(nid) (&contig_page_data)
setup_arch()中内存相关有:
内存布局初始化:设置地址空间范围。
MMU配置:包括设置页表、页目录。
物理内存探测:解析启动参数或BIOS信息,获取内存总量和分布信息。
启用bootmem分配器,用于启动过程内存分配。
setup_per_cpu_areas:
bootmem分配器给每个CPU分配一块存储per-cpu变量的区域。
per-cpu变量保存在ELF文件单独的段中,如.data..percpu。
并加载到物理内存.data..percpu段中。
build_all_zonelists:
为每个CPU构建zonelist,其中zone按优先级排序。
可根据当前CPU的zonelis构快速找到最优的zone,以完成内存分配。
优点:优化内存访问,降低内存碎片、提高内存利用率。
mm_init:
初始化内核的页表。
停用bootmem分配器,迁移到伙伴系统。
初始化SLAB 分配器(后续章节讲)
预留内存页。
初始化内存统计和信息
setup_per_cpu_pageset:
给每个CPU设置缓存页的结构。
优点:避免使用伙伴系统分配,引发zone->lock锁竞争。
zonelist构建流程:
遍历所有内存节点ABCD,每个节点都建立各自zonelist。
如下图是节点C的zonelist,表示内存不足时尝试分配的优先次序。
C2表示:内存结点C的高端内存域。
D1表示:内存结点D的普通内存域
如想要从结点C分配一个高端内存域的内存,查看上图的2,优先从C2中分配,C2无法满足分配需求,依次尝试C1 C0 D2 ....
3.4.2 特定于体系架构的设置
本节所讲不同体系架构实现有差异。
2.6.24内核开始将IA32和AMD64统一为x86。
体系代码都迁移到目录arch/x86下。
内核在内存中的布局
先讲两个重要宏:
1. PHYS_OFFSET:内存在系统内的起始物理地址。
该值与平台相关,如项目中:
#define PHYS_OFFSET UL(CONFIG_DRAM_BASE)
CONFIG_DRAM_BASE=0x80000000
2. PAGE_OFFSET:内核空间的起始虚拟地址,划分了内核与用户空间。
32位系统为例:
若值为0xC0000000,即用户空间3GB,内核空间1GB。
若值为0x80000000,即用户空间和内核空间均为2GB。
cat /proc/iomem中可查看:
1. 系统RAM的起始物理地址。
2. 内核代码段和数据段的物理地址。
ZRELADDR:内核解压的运行物理地址。
我的项目中ZRELADDR地址为:
zreladdr-y := $(CONFIG_DRAM_BASE)+0x8000
CONFIG_DRAM_BASE是内存的起始物理地址,项目中值为0x80000000。
所以内核解压后运行地址是:内存起始偏移0x8000处,即0x80008000。
# cat /proc/iomem 可验证
80000000-83ffffff : System RAM
80008000-804cbb83 : Kernel code 内核的物理地址0x80008000
804ee000-8063b10f : Kernel data
内核编译链接时,vmlinux.ld.S确定内核的_text段,__edata等段,以及符号函数的虚拟地址,并存储在system.map文件。
一个内核符号的物理地址计算方法:
Physical Address = (Virtual Address - PAGE_OFFSET) + PHYS_OFFSET
PAGE_OFFSET:内核虚拟地址空间的的起始位置。
PHYS_OFFSET:内存的其实物理地址。
查看系统物理地址空间分布,包括所有硬件设备,如PCIE,RAM,SPI,PHY,RTC,flash等。
# cat /proc/iomem
00000000-00000000 : gdm-pmic
13200000-1320003f : gdm-spi
13380000-13381fff : gdm-rgmii
40000000-401fffff : gdm-sflash
80000000-83ffffff : System RAM
80008000-804628a7 : Kernel code
80484000-805daf4b : Kernel data
初始化步骤
Intel IA-32架构为例:
setup_arch中关于内存管理的部分如下:
parse_cmdline_early:解析cmdline中内存参数,如
mem=xxx[KMG]:
控制内核可用内存大小,例如只使用部分内存。
reserve=<start>,<size>
预留一块物理内存不使用。
cma=<start>,<size>
启动时预留一块连续的物理内存,例如给DMA设备或者图形硬件使用。
setup_memory
确定每个节点可用的物理内存页数目
初始化bootmem
分配各个内存区
paging_init
初始化内核页表。
根据编译选项,是否开启PAE特性。
zone_sizes_init
初始化所有内存结点的pgdata_t实例。
分页机制的初始化
先看几个地址空间分布图,以32位系统为例。
1. 内核虚拟地址空间分布:
2. 内核空间与物理内存映射:
直接映射区:即线性映射区。
范围:内核空间3G ~ 3G+896M,映射到物理内存DMA和NORMAL两个ZONE
直接映射区的虚拟地址转换为物理地址:
#define __pa(x) ((unsigned long) (x) - PAGE_OFFSET)
内存DMA和NORMAL的物理地址转换为虚拟地址:
#define __va(x) ((void *) ((unsigned long) (x) + PAGE_OFFSET))
注意:这两个函数只适用于直接映射区。
而高端内存区不是简单的线性映射,不适用。
内核空间最后128M,会映射到物理内存ZONE_HIGHMEM。有三种用途:
1. 固定映射: Fixed Mapping
编译时已映射到特定物理地址。
无需动态页表,快速访问的物理内存地址
用于:
映射硬件寄存器区和I/O。
起始处:FIXADDR_START
2. 永久/映射映射: pkmap:Persistent Kernel Mapping
将高端页帧长期映射到内核地址空间中,直到手动解除映射。
如kmap, kunmap函数。
3. vmalloc区:即动态内存映射区。
范围:VMALLOC_START ~ VMALLOC_END
vmalloc函数作用:
分配较大的连续虚拟内存,对应物理内存不连续。
paging_init:
划分内核空间与用户空间。
pagetable_init:
初始化系统PGD PUD PMD PTE页表。
将物理内存映射到虚拟地址PAGE_OFFSET处。
初始化固定映射。
load_cr3
将PGD页表(内核变量swapper_pg_dir)加载到CR3寄存器。
__flush_all_tlb:
刷出TLB,即清空页表缓存。
zone_pcp_init:
初始化PCP冷热页,包括初始化per_cpu_pageset实例。
计算batch值。
batch值作用:如果pcp没有空闲页,每次从zone中批量分配batch个页面。
batch大小:考虑让热缓存页有可能放置到CPU L2缓存中
3.4.3 启动期间的内存管理
内核启动过程中,内存管理子系统还未初始化,此时使用bootmem分配器进行内存分配。
bootmem:使用bitmap表示页空闲、已使用的页
分配时,遍历位图,找到所需连续空闲页。
缺点:每次分配都遍历位图,不高效。
数据结构
系统每个节点都有一个bootmem_data实例,在编译时分配。
struct bootmem_data {
unsigned long node_min_pfn;
起始物理页框号
unsigned long node_low_pfn;
可管理的物理内存最后一页编号,即ZONE_NORMAL最后一页
void *node_bootmem_map;
位图指针,表示页是否使用。
unsigned long last_end_off;
上次分配页的页内偏移,用于分配小于整页的内存
unsigned long hint_idx;
提供一个分配起点或最有位置的提示
struct list_head list;
链接所有结点的bootmem_data
} bootmem_data_t;
UMA系统只需一个sbootmem_data_t实例
contig_bootmem_data //contig,即contigous连续内存模型
初始化
ARM:setup_arch-> paging_init -> bootmem_init
而IA-32:set_memory 中进行bootmem初始化
bootmem:不使用内存的高端内存区域。
使用内存的normal区域,因为可通过固定的线性映射访问。访问简单。
bootmem初始化包括:
初始化bootmem_data结构体。
扫描内存,构建位图,表示对应内存页是否使用。
为设备驱动、DMA区域等预留内存区域。
与内核的接口
启动期间分配内存:
alloc_bootmem()/alloc_bootmem_pages()
从ZONE_NORMAL域内分配内存
alloc_bootmem_low()/alloc_bootmem_low_pages()
从ZONE_DMA域分配内存
alloc_bootmem_node()
NUMA中从指定节点的bootmme分配内存.
上述函数都调用alloc_bootmem_core(unsigned long size,
unsigned long align,
unsigned long goal,
unsigned long limit)
size:所需内存大小。
align:对齐方式,如按page对齐。
goal:目标地址,建议分配的内存地址
limit:确保不会分配超过这个地址的内存。
释放内存
free_bootmem(unsigned long addr, unsigned long size)
void __init free_bootmem_node(pg_data_t *pgdat, unsigned long physaddr, unsigned long size)
这两个函数使用较少,大部分分配的内存后续会一直使用,不用释放。
停用bootmem分配器
系统初始化到伙伴系统后,伙伴系统将负责内存分配工作,此时需停用bootmem分配器。
停用bootmem函数:
free_all_bootmem(void)
扫描bootmem位图,释放未使用页到伙伴系统。
释放初始化数据
__init和__initdata是GNU C编译器语句,编译后会将对应数据和函数放在内核镜像的特定段。
启动结束,可从内存完全删除对应数据和函数。
其实现为:
#define __init __attribute__(__section(.init.text)) __cold
#define __initdata __attribute__(__section(.init.data))
__attribute__:GCC关键字
__section:编译器将数据放入二进制文件.init.text和.inint.data中
__cold:该函数不会被经常调用
命令readelf可查看内核镜像的各个段,如:
readelf - sections vmlinux