目录
1. 前言
本专题文章承接之前《kernel启动流程-start_kernel的执行》专题文章,本专题我们开始学习内存管理部分,本文是概述部分。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
kernel版本:5.10
平台:arm64
2. 地址空间映射
在《kernel启动流程-start_kernel的执行_2.setup_arch》我们已经看到如下图的地址空间映射:
也即是说在内存初始化阶段已经为物理地址空间创建了映射关系,包括memblock.memory区域,kernel image, DTB,swapper_pg页表区域。
如上图,其中:PAGE_OFFSET决定了用户空间与内核空间的划分界限;
关于物理地址空间分布可以通过cat /proc/iomem查看如下:
/ # cat /proc/iomem
00000000-03ffffff : 0.flash flash@0
04000000-07ffffff : 0.flash flash@0
09000000-09000fff : pl011@9000000
09000000-09000fff : 9000000.pl011 pl011@9000000
09010000-09010fff : pl031@9010000
09030000-09030fff : pl061@9030000
09030000-09030fff : 9030000.pl061 pl061@9030000
0a003e00-0a003fff : a003e00.virtio_mmio virtio_mmio@a003e00
10000000-3efeffff : pcie@10000000
10000000-1003ffff : 0000:00:01.0
10040000-10040fff : 0000:00:01.0
40000000-7fffffff : System RAM
40200000-4138ffff : Kernel code
41390000-4192ffff : reserved
41930000-4212ffff : Kernel data
48000000-480fffff : reserved
7b000000-7bffffff : reserved
7cc3d000-7cdeafff : reserved
7cded000-7cdeefff : reserved
7cdef000-7cdeffff : reserved
7cdf0000-7cdf5fff : reserved
7cdf6000-7cdfefff : reserved
7cdff000-7fffffff : reserved
4010000000-401fffffff : PCI ECAM
8000000000-ffffffffff : pcie@10000000
8000000000-8000003fff : 0000:00:01.0
8000000000-8000003fff : virtio-pci-modern
从以上可知
40000000-7fffffff : System RAM
40200000-4138ffff : Kernel code
41930000-4212ffff : Kernel data
关于虚拟地址的划分可以通过kernel_page_tables 查看:
/sys/kernel/debug # cat kernel_page_tables
0x0000000000000000-0xffff000000000000
0xffff000000000000-0xffff000000200000
0xffff000000200000-0xffff000001200000
0xffff000001200000-0xffff000001340000
0xffff000001340000-0xffff000002643000
0xffff000002643000-0xffff000002644000
0xffff000002644000-0xffff000040000000
0xffff000040000000-0xffff008000000000
0xffff008000000000-0xffff800000000000
---[ Linear Mapping end ]---
---[ BPF start ]---
0xffff800000000000-0xffff800008000000
---[ BPF end ]---
---[ Modules start ]---
0xffff800008000000-0xffff800010000000
---[ Modules end ]---
---[ vmalloc() area ]---
0xffff800010000000-0xfffffdffbfff0000
---[ vmalloc() end ]---
0xfffffdffbfff0000-0xfffffdffc0000000
0xfffffdffc0000000-0xfffffdfffe400000
0xfffffdfffe400000-0xfffffdfffe5f9000
---[ Fixmap start ]---
0xfffffdfffe5f9000-0xfffffdfffea00000
---[ Fixmap end ]---
0xfffffdfffea00000-0xfffffdfffec00000
---[ PCI I/O start ]---
0xfffffdfffec00000-0xfffffdffffc00000
---[ PCI I/O end ]---
0xfffffdffffc00000-0xfffffdffffe00000
---[ vmemmap start ]---
0xfffffdffffe00000-0x0000000000000000
3. 内存管理总体框图
-
用户空间层
主要是一些libc库封装的API,他们通过系统调用访问内核空间,这些常用的API包括malloc, mmap, mlock, madvise, mremap等。 -
内核空间层
内核空间层主要提供了系统调用给到用户空间,相关系统调用包括sys_brk, sys_mmap,sys_madvise。提供了如下的功能:VMA管理, 匿名页面,page cache, 页面回收,反向映射,slab分配器,页表管理等。 -
硬件层
包括MMU,TLB和cache部件,以及板载内存
4. 内存管理启动部分
在分析启动代码的时候会有一些关于内存管理初始化相关的内容,在此专门将其提取出来,做一个简单的总结。
start_kernel
|--setup_arch(&command_line)
| |- -early_fixmap_init
| |- -early_ioremap_init
| |- -setup_machine_fdt
| |- -parse_early_param
| |- -arm64_memblock_init
| |- -paging_init
| |- -bootmem_init
| \- -kasan_init
|--setup_per_cpu_areas()
|--build_all_zonelists(NULL)
|--page_alloc_init()
|--mm_init()
|--kmem_cache_init_late()
|--setup_per_cpu_pageset()
\--numa_policy_init()
- setup_arch
(1)early_fixmap_init
在物理地址映射前,为了访问dtb,通过early_fixmap_init用来创建映射框架,使用时通过fixmap_remap_fdt来填充pte;
(2)early_ioremap_init
为访问IO内存区域创建映射框架;
(3)setup_machine_fdt
通过fixmap_remap_fdt填充dtb的映射的PTE页表项,同时在memblock.resrved中保留这部分区域,将fdt的memory中描述的区域添加到memblock.memory,初始化memblock.memory下的各个memblock_region,每个memblock_region可理解为一个物理内存bank区域,比如有两个DDR芯片,则每个DDR芯片的物理区间对应一个bank
(4)parse_early_param
主要与以下两个参数相关:
early_param(“mem”, early_mem)
early_param(“numa”, numa_parse_early_param)
(5)arm64_memblock_init
arm64_memblock_init通过memblock_remove将某些memblock_region区域从memblock.memory中移除,这些区域包含了DDR物理地址所不包含的区域,以及内核线性映射区所不能涵盖的区域;同时将某些物理区间添加到memblock.reserved中,这些区间包含dts中预留区域,命令行中通过参数预留的CMA区域,内核的代码段、initrd、页表、数据段等所在区域,crash kernel保留区域以及elf相关区域。这个过程中也会初始化一些全局变量,如物理内存起始地址memstart_addr
(6)paging_init
为kernel image,swapper_pg页表本身,memblock.memory区域创建细粒度映射。经过paging_init将内核空间页表真正从init_pg_dir(粗粒度映射)转换到swapper_pg_dir.
(7)bootmem_init
遍历memblock.memory的每个memblock_region,并划分为section(本例大小为1G),保存在全局mem_section数组,标记section为present; 遍历每个标记为present的mem_section, 为每个mem_section创建page结构体数组;初始化每个node, node下的zone, 以及node下的每一个pfn对应的page - setup_per_cpu_areas
为每个cpu的per-cpu变量副本分配空间 - build_all_zonelists
build_all_zonelists主要的工作就是在当前处理的numa节点和系统中其它numa节点的内存域zone之间建立一种等级次序,之后将根据这种次序来分配内存 - page_alloc_init
内存页初始化,主要是在cpu发生热插拔时将CPU管理的页面移动到空闲列表 - mm_init
主要功能就是将memblock管理的空闲内存释放到伙伴系统 - setup_per_cpu_pageset
完成对每个cpu每个zone的pageset遍历,利用pageset_init()初始化pcp链表,和利用pageset_set_high_and_batch为每个pageset计算每次在高速缓存中将要添加或被删去的页框个数
5. 基本对象
5.1 struct memblock
memblock是Linux内核启动早期使用的内存管理方案,通过struct memblock结构体来记录物理内存使用情况
memory:记录完整的内存资源
reserved:记录已经分配或者预留的内存资源
5.2 struct mem_section
将物理内存划分成一个个的section,然后再划分成page,这样可以减少为物理内存的空洞创建page实例
//arm64物理地址空间bit数
#define CONFIG_ARM64_PA_BITS 48
//最大的物理地址空间bit数
#define MAX_PHYSMEM_BITS CONFIG_ARM64_PA_BITS
//一个mem_section占用的bit数
#define SECTION_SIZE_BITS 30
#define SECTIONS_SHIFT (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)
//物理地址空间可划分的mem_section数目
#define NR_MEM_SECTIONS (1UL << SECTIONS_SHIFT)
#define PFN_SECTION_SHIFT (SECTION_SIZE_BITS - PAGE_SHIFT)
//一个mem_block可划分的page数目
#define PAGES_PER_SECTION (1UL << PFN_SECTION_SHIFT)
//一个page所能分配的mem_section数目
#define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section))
//分配整个物理空间的所有mem_section,需要多少page数目
#define NR_SECTION_ROOTS DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)
由如上宏定义可知:如果对整个物理空间进行划分,则需要SECTIONS_PER_ROOT * NR_SECTION_ROOTS个mem_section结构体,因此需要分配SECTIONS_PER_ROOT * NR_SECTION_ROOTS个mem_section结构体
struct mem_section **mem_section二维数组是内核全局变量,它包含系统中所有的物理内存,它定义了系统中有多少个mem_section,每个mem_section内部又包含指向page数组的指针,page数组的索引对应于该物理页在整个物理内存mem_section中的位置
5.3 struct pg_data_t
-
struct pg_data_t
每个NUMA node节点都有此数据结构来描述物理内存布局, 基本成员:
(1)node_zones:用于记录本节点有哪些zone;
(2)node_zonelists:用于记录zone的分配顺序,包含ZONELIST_FALLBACK和 ZONELIST_NOFALLBACK,分别记录本地node和远端node的zone分配顺序;
(3)wait_queue_head_t kswapd_wait:每个pg_data_t都有一个此结构,由free_area_init_core()初始化;
(4)__lruvec:每个节点都有一整套lru链表用于记录页面使用情况,__lruvec指向这些链表,根据匿名/文件,活跃/不活跃,是否可回收,分为5类LRU链表 -
struct lruvec
用于管理页面回收的LRU链表,包括5个链表 -
struct pagevec
借用一个数组来保存特定数目的页,可以对这些页执行相同的操作,页向量以批处理执行,比单独处理一个页的方式效率要高 -
struct zoneref
是zonelist中某个zone的引用 -
struct zonelist
代表一组zone。伙伴分配器会从zonelist开启分配内存。struct zoneref数组的第一个成员指向的zone是页面分配器的第一个候选者,在第一个候选zone分配失败会从struct zoneref数组的第二个成员指向的zone进行分配,依次类推 -
struct zone
zone代表对物理内存进行的分区,通常包括ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEMORY(32位系统中)。low memory包含ZONE_DMA和ZONE_NORMAL,为保证性能会cache line对齐,且对访问比较频繁的锁之间填充,使得分布在不同的cache line,提高性能,基本成员:
(1)pageblock_flags:指向每个pageblock的MIGRATE_TYPES类型的内存空间
(2)watermark:记录了zone的水位:WMARK_MIN, WMARK_LOW,WMARK_HIGH
(3)lowmem_reserve:zone中预留的内存,在紧急情况下使用,如内存回收本身需要分配的少量内存
(4)free_area[MAX_ORDER]: 每个free_area元素管理一个2^order的页面,有MIGRATE_TYPES个类型链表
物理内存在linux内核中分出几个zone来管理,zone根据内核的配置来划分,zone数组大小是在struct pglist_data中定义的(pglist_data代表一个内存节点,对于UMA,内存节点个数为1)数组成员个数为MAX_NR_ZONES个,也就是表示由MAX_NR_ZONES个zone。
zone数据结构中有一个free_area数组,数组的大小是MAX_ORDER。每个free_area数组成员维护着MIGRAGE_TYPES个链表,类型包括:
enum {
MIGRATE_UNMOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_MOVABLE,
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_RESERVE = MIGRATE_PCPTYPES,
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* can't allocate from here */
#endif
MIGRATE_TYPES
};
通过cat /proc/pagetypeinfo可以查看页面的分配状态
可以看到大部分物理内存页面都存放在MIGRATE_MOVABLE链表中;大部分物理内存页面初始化时存放到2的10次幂的链表中。
-
struct per_cpu_pageset
每个cpu都会初始化一个per_cpu_pageset变量,位于zone结构体, 当释放order为0的页面首先放到pageset->pcp.list对应的链表中,页面类型定义在enum MIGRATE_TYPES中,包含MIGRATE_UNMOVABLE, MIGRATE_RECLAIMABLE,MIGRATE_MOVABLE、MIGRATE_PCPTYPES等 -
struct per_cpu_pages
per_cpu_pageset的成员变量,用于描述当前zone per cpu page的状况
(1)count表示当前zone 每个cpu的页面数量
(2)high表示当空闲页面达到此值则将回收到BUDDY
(3)batch表示每次回收多少个页面,通过zone_batchsize计算得出 -
struct page
用于描述一个物理页面。所有的物理页面首先划分为mem_section,每个mem_section用来描述多个page页面。
(1)__count:跟踪page页面的引用情况
(2)PG_locked:
(3)mapping:指定页面所在的地址空间:文件映射 or 匿名映射
5.4 伙伴系统
伙伴系统(Buddy system)是操作系统常用的动态存储管理方法,用户提出申请时,分配一块大小合适的内存块给用户,反之在用户释放内存块时回收。在伙伴系统中,内存块是2的order次幂,Linux内核中最大的order是MAX_ORDER,通常是11,也就是把所有的空闲页面分组成11个内存块链表,order分为为:0,1,2,3,4,5,6,7,8,9,10。每个内存块链表分别包含1, 2,4, 8, 16, 32… 1024个连续页面,最大的1024个连续页面就是4M.
- pageblock
zone会将空闲内存划分为很多个pageblock, 一个pageblock最大大小为2^(MAX_ORDER-1)个page, pageblock是物理连续的页面;如果定义了HUGETLB_PAGE特性则每个pageblock大小为2的(HUGETLB_PAGE_ORDER-1)次方个page; 每个pageblock有一个MIGRATE_TYPES类型:MIGRATE_MOVABLE(可移动)、MIGRATE_RECLAIMABLE(可回收)、MIGRAE_UNMOVABLE(不可移动)。zone->pageblock_flags指针指向存放每个pageblock的MIGRATE_TYPES类型的内存空间?而free_area数组记录的是页面的MIGRATE_TYPES类型
注:MAX_ORDER一般为11,则pageblock大小为4M
-
struct free_area
在struct zone中是一个数组,大小为MAX_ORDER。每个数组成员管理三个链表,每个链表管理一种类型,每个链表将相同大小相同类型的页面链接起来;通过cat /proc/pagetypeinfo查看各个内存块的类型及分配情况 -
struct alloc_context
伙伴系统分配函数用于保存参数的数据结构
(1)zonelist:指向每个内存及诶单中对应的zonelist;
(2)nodemask: 内存节点的掩码;
(3)preferred_zoneref:表示首选zone的zoneref
(40migratetype: 表示迁移类型
(5)high_zoneidx:分配掩码计算zone的zoneidx,表示这个分配掩码允许内存分配的最高zone
(6)spread_dirty_pages:用于指定是否传播脏页
5.5 SLAB
slab用来解决小内存块分配问题,不同与buddy以页为单位分配。slab实际也是通过buddy来分配,只不过可以在buddy上层实现自己的分配算法,来对小内存块进行管理;slab已经没有专门的数据结构表征它,一个slab是通过此slab的首个page来代表,page中有专门表征slab的成员,如slab_list用于链接到slab节点的三个链表之一。
-
struct kmem_cache
它给每个CPU提供一个对象缓冲池array_cache
(1).batchcount:当array_cache本地缓冲池为空,从共享缓冲池或slabs_partial/slabs_free链表中获取对象的数目
(2).limit:当本地缓冲池空闲对象数目大于limit,将释放batchcount个对象以便于内核回收或销毁slab
(3).shared:用于多核系统
(4).size:对象的长度,要加上对齐长度
(5).flags:对象的分配掩码,影响通过伙伴系统寻找空闲页的行为
(6).num:一个slab中最多可以有多少个对象
(7).gfporder:一个slab占用2^gfporder个页面
(8).color:一个slab有多少个不同的cache line
(9).color_off:一个cache line的长度,与L1 cache line同
(10).freelist_size:每个slab对象在freelist管理区中占用1个字节,这里指freelist管理区的大小
(11):name:slab描述符的名称
(12).object_size:对象的实际大小
(13).align:对齐长度
(14).node: slab节点,struct kmem_cache_node类型,在NUMA系统中每个节点都有一个struct kmem_cache_node结构,vexpress只有一个 -
struct array_cache
对象缓冲池(本地对象缓冲池和共享对象缓冲池),记录对象缓冲池可用对象数目、记录增减变化,释放的阀值等,slab描述符给每个cpu提供一个本地对象缓冲池。
(1)avail:对象缓冲池中可用对象数目
(2)limit: 对象管缓冲池可用数目的最大阀值
(3)batchcount:迁移对象的数目,如从共享对象缓冲池或partial/free链表迁移到本地对象缓冲池对象的数目
(4)touched:从缓冲池移除一个对象置1,收缩对象置0?
(5)entry:指向存储对象的变长数组,每个成员存放一个对象的指针。这个数组最初最多有limit个成员 -
struct kmem_cache_node
简称为slab节点,包含3个slab链表:部分空闲链表、空闲链表、完全用尽链表,链表的每个成员是一个slab
(1)free_objects表示三个链表中空闲对象的总和
(2)fee_limit表示slab上空闲对象的允许的最大数目
5.6 VMALLOC
-
struct vmap_area
代表一个vmalloc区块 -
struct vm_struct
虚拟地址空间
5.7 进程地址空间
-
struct vm_area_struct
也称为VMA, 是内核中用于表示进程虚拟地址空间的数据结构。由于这些地址空间归属各个进程,因此在用户进程的struct mm_struct中也有相关成员 。它有一个红黑树节点,用于加入到mm_struct的红黑树中,通过红黑树查找可以加快速度,它也会按照起始地址递增的方式链入到mm_struct的mmap单链表中 -
struct mm_struct
描述进程内存管理的核心结构,也提供了VMA管理的相关信息
(1)mmap:进程中VMA链表的头
(2)mm_rb:进程中VMA红黑树的根
(3) mm_users:用户空间的用户个数
(4) mm_count:内核中引用了该数据结构的个数
(5) mmap_sem:保护地址空间读写的信号量
(6) page_table_lock:保护进程页表的spin lock
5.8 页面回收
-
struct lruvec
用于指向一套lru链表,这些链表用于记录页面的使用情况,它位于内存节点struct pglist_data中 -
struct scan_control
表示内存回收扫描的参数
(1)nr_to_reclain: 要回收页面的数量
(2) order: 分配页面的数量,从分配器传递过来的参数
(3) reclaim_idx: 表示最高允许页面回收的zone
(4)nr_reclaimed: 已回收的页面数量
(5)nr_scanned:扫描不活跃页的数量
(6)priority: 扫描LRU链表的所有页面的LRU页面数量>>priority个页面,值越小,扫描数越大
5.9 页表映射
- struct mm_struct init_mm
其中记录了PGD页表的基地址
6. ARM64页表映射
以4K页为例,基于ARMV8架构的处理器虚拟地址页表结构如下:
关于虚拟地址各段的说明:
参考文档
- 《奔跑吧,Linux内核》
- 《深入Linux内核架构》