内存管理基础学习笔记 - 1. 概述

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()
  1. 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
  2. setup_per_cpu_areas
    为每个cpu的per-cpu变量副本分配空间
  3. build_all_zonelists
    build_all_zonelists主要的工作就是在当前处理的numa节点和系统中其它numa节点的内存域zone之间建立一种等级次序,之后将根据这种次序来分配内存
  4. page_alloc_init
    内存页初始化,主要是在cpu发生热插拔时将CPU管理的页面移动到空闲列表
  5. mm_init
    主要功能就是将memblock管理的空闲内存释放到伙伴系统
  6. 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架构的处理器虚拟地址页表结构如下:
在这里插入图片描述

关于虚拟地址各段的说明:
在这里插入图片描述

参考文档

  1. 《奔跑吧,Linux内核》
  2. 《深入Linux内核架构》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值