linux 内存管理(13) - memblock

  • 了解memblock机制。

1.概述

  在引导内核的过程中,需要使用内存, 而这个时候内核的内存管理并没有被创建, 因此也就需要一种精简的内存管理系统先接受这个工作, 而在初始化完成后, 再将旧的接口废弃, 转而使用强大的buddy系统来进行内存管理。

  早期的Linux内核在引导阶段都是通过bootmem来完成初期的内存管理的,但是后来的版本开始把bootmem弃用了,使用memblock机制。[refer to: Use memblock interface instead of bootmem]

拓展:bootmem
  在启动过程期间,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段早期分配内存。
  显然,对该分配器的需求集中于简单性方面,而不是性能和通用性。因此内核开发者决定实现一个最先适配(first-fit)分配器用于在启动阶段管理内存。这是可能想到的最简单的方式。
  该分配器使用一个位图来管理页,位图比特位的数目与系统中物理内存页的数目相同。比特位位1,表示页以用;比特位位0,表示页空闲。
  在需要分配内存时,分配器逐位扫描位图,知道找到一个能提供足够连续页的位置,即所谓的最先最佳(first-best)或最先适配位置。
  该过程不是很高效,因为每次分配都必须从头扫描比特链。因此在内核完全初始化之后,不能将该适配器用于内存管理。伙伴系统(连同slab、slub、或slob分配器)是一个好得多的备选方案。

1.1.memblock 主要功能

  mmemblock 内存页帧分配器是 Linux 启动早期内存管理器,在伙伴系统(Buddy System)接管内存管理之前为系统提供内存分配、预留等功能。

  memblock 将系统启动时获取的可用内存范围(如从设备树中获取的内存范围)纳入管理,为内核启动阶段内存分配需求提供服务,直到 memblock 分配器将内存管理权移交给伙伴系统。同时 memblock 分配器也维护预留内存(reserved memory),使其不会被分配器直接用于分配,保证其不被非预留者使用。

  默认情况,内核会为所有的内存建立线性地址空间,并将可用内存释放到伙伴系统中。但是像内核镜像、dtb以及initrd这些系统本身使用的内存,并不能再次被分配做它用,因此也不能被释放到伙伴系统中。更有甚者,有些驱动或者异构核可能需要保留一些专用内存,这些内存当然也不能释放给伙伴系统。因此,memblock又抽象出了保留内存类型,它同样通过一组region进行管理,以标识这些内容已经被使用,或者是专用的。

  首先在内核启动后,对于内存分成好几块:

  • 内存中的某些部分使永久分配给内核的,例如代码段和数据段、ramdisk和dtb占用的空间、临时页表和设备数中的保留区域等,是系统内存的一部分,不能被侵占,也不参与内存的分配,称之为静态内存;
  • GPU/camera/多核共享的内存都需要预留大量连续内存,这部分内存平时不使用,但是必须为各个应用场景预留,这样的内存称之为预留内存;
  • 内存其余的部分,是需要内核管理的内存,称之为动态内存;

  那么memblock就是将以上内存按功能划分为若干内存区,使用不同的类型存放在memory和reserved的两个集合中,memory即为动态内存,而resvered包括静态内存等。

  在mm_init中会建立内核的内存分配器,停用memblock,释放内存给伙伴系统和slab分配器。memblock是bootmem的升级版本,在config中配置:CONFIG_NO_BOOTMEM=Y。

2.memblock 数据结构

  内核中定义了一个 memblock 实体,作为 memblock 分配器管理载体,其类型为 struct memblock 。

  memblock 分配器管理结构共有三层,从顶向下分别为 struct memblock , struct memblock_type , struct memblock_region ,三层结构关系如下图所示,可结合代码理解。下面将详细分析。
在这里插入图片描述

2.1.struct memblock

  memblock结构描述了系统中所有物理内存的布局信息,它通过扫描dtb的memory节点,获取所有内存的起始地址和长度信息,并将其依次存放到memblock的memory region中。将需要保留的内存信息存放到memblock的reserved region中。

include/linux/memblock.h
struct memblock {
    bool bottom_up;  /* is bottom up direction? 
    如果true, 则允许由下而上地分配内存*/
    phys_addr_t current_limit; /*指出了内存块的大小限制*/  
    /*  接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存*/
    struct memblock_type memory;
    struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    struct memblock_type physmem;
#endif
};

在这里插入图片描述
2.2.struct memblock_type

struct memblock_type
{
    unsigned long cnt;      /* number of regions */
    unsigned long max;      /* size of the allocated array */
    phys_addr_t total_size; /* size of all regions */
    struct memblock_region *regions;
};

在这里插入图片描述

2.3.struct memblock_region

struct memblock_region
{
    phys_addr_t base;
    phys_addr_t size;
    unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
    int nid;
#endif
};

在这里插入图片描述
  内存区块被维护在不同的 struct memblock_type 的 regions 链表上,这是一个由数组构成的链表,链表通过每个区块 的基地址的大小,从小到大的排列。每个内存区块代表的内存区不能与本链表中的其他内存区块相 互重叠,可以相连。内核初始定义了两个内存区块数组,如下:

static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblok;

  以上介绍了 memblock 的三层结构,其中前两层在源码中已经定义。第三层 region 内存区块则是在内核启动过程中,内核调用相关接口函数动态添加。

2.4.memblock_region flags字段

/* Definition of memblock flags. */
enum {
    MEMBLOCK_NONE       = 0x0,  /* No special request */
    MEMBLOCK_HOTPLUG    = 0x1,  /* hotpluggable region */
    MEMBLOCK_MIRROR     = 0x2,  /* mirrored region */
    MEMBLOCK_NOMAP      = 0x4,  /* don't add to kernel direct mapping */
};

MEMBLOCK 内存分配器基础框架如下:
在这里插入图片描述
  MEMBLOCK 分配器使用一个 struct memblock 结构维护着两种内存, 其中成员 memory 维护着可用物理内存区域;成员 reserved 维护着操作系统预留的内存区域。 每个区域使用数据结构 struct memblock_type 进行管理,其成员 regions 负责维护该类型内存的所有内存区,每个内存区使用数据结构 struct memblock_region 进行维护。

2.5.struct memblock 实例化

  内核实例化struct memblock 结构,以供 MEMBLOCK 进行内存的管理,其定义如下:

mm/memblock.c:
struct memblock memblock __initdata_memblock = {
        .memory.regions         = memblock_memory_init_regions,
        .memory.cnt             = 1,    /* empty dummy entry */
        .memory.max             = INIT_MEMBLOCK_REGIONS,
        .memory.name            = "memory",

        .reserved.regions       = memblock_reserved_init_regions,
        .reserved.cnt           = 1,    /* empty dummy entry */
        .reserved.max           = INIT_MEMBLOCK_RESERVED_REGIONS,
        .reserved.name          = "reserved",

#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
        .physmem.regions        = memblock_physmem_init_regions,
        .physmem.cnt            = 1,    /* empty dummy entry */
        .physmem.max            = INIT_PHYSMEM_REGIONS,
        .physmem.name           = "physmem",
#endif

        .bottom_up              = false,
        .current_limit          = MEMBLOCK_ALLOC_ANYWHERE,
};

2.5.1.__initdata_memblock 宏指定存储位置

include/linux/memblock.h
#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK
#define __init_memblock __meminit
#define __initdata_memblock __meminitdata
#else
#define __init_memblock
#define __initdata_memblock
#endif

include/linux/init.h
#define __meminitdata    __section(.meminit.data)

  编译链接后,从内核镜像中的__initdata_memblock段中划分了内存空间,内核启动后的拷贝动作,便从SRAM中得到了对应的物理内存空间,memblock也就初始化完成,可以工作了。

  memblock只用4个数据结构就描述了所有的内存信息。

  使用struct memblock描述系统所有的物理内存信息,物理内存将按照在启动阶段是否作为特殊用途挂靠到struct memblock的memory以及reserved两个成员中。

  memory以及reserved是struct memblock_type类型的数据结构,表征内存是否作为特殊用途的类型,在启动早期,所有的内存都先挂靠到memory中,在启动过程中,将系统自身使用的、编程预留以及其他特殊功能预留的内存,也会将这些内存的地址挂靠到reserved中。这些预留的内存,将同时被memory以及reserved一起管理。

  memblock使用struct memblock_region来描述内存的细节信息,在memblock生命周期中,所有管理的内存块都用一个memblock_region来描述,简单来说,就算我们只是给页表分配了PAGE_SIZE的内存,这PAGE_SIZE大小的内存块也是一个memblock_region,但是系统对memblock_region的成员数量也有限制,避免memblock本身使用过多的内存。所以memblock使用了memblock标志字段来描述每一个memblock_region,从而控制memblock_region的合并算法。

  在memblock的后期,memblock将会memory中没有与reserved共享的内存填充到buddy内存管理器,完成自己的使命。

2.5.2.memblock_type初始化

  对于可用物理内存,其名字设定为 “memory”,初始状态系统下,可用物理物理的所有内存区块都维护在 memblock_memory_init_regions 上,当前情况下,可用物理内存区只包含一个内存区块,然而可用物理内存可管理 INIT_MEMBLOCK_REGIONS 个内存区块;

  对于预留内存,其名字设定为 “reserved”,初始状态下,预留物理内存的所有区块都维护在 memblock_reserved_init_regions 上,当前情况下,预留物理内存区只包含一个内存区块,然而预留内存区可以维护管理 INIT_MEMBLOCK_RESERVED_REGIONS 个内存区块。

3.memblock 主要接口函数

  memblock 系统提供相关接口供内核使用,包括内存区块的添加、预留、内存申请等功能。本文将对以下五个关键接口函数进行分析,其余函数可举一反三:

  • memblock_add 将内存区块添加到可用内存集合。通过此函数可展示 memblock 添加区块的思路。

  • memblock_reserve 将内存区块添加到预留内存集合。

  • for_each_reserved_mem_range 遍历预留内存区块。通过此函数可展示 memblock 遍历区块的逻辑和思路。

  • memblock_phys_alloc 用于申请 memblock 中的物理内存。

  • memblock_alloc 用于申请 memblock 的内存并返回虚拟地址。可供内核申请内存是 memblock 价值实现的关键。

4.memblock做的事情

1)setup_machine_fdt,解析设备树文件,保存系统所有的内存信息。
解释fdt前,保存fdt本身占据的内存到memblock.reserved类型中
解析fdt的memory节点,保存所有物理内存范围到memblokc.memory类型中

2)arm64_memblock_init,初始化内核启动过程中的内存配置信息,对内存做细分。主要涉及到后续buddy内存管理器所需要的一些内存信息,比如物理内存的起始地址、结束地址、将内核镜像本身占据的内存、fdt reserved类型的内存以及PERCPU、CMA的内存预留出来。总的来说,就是将内存再按照功能需求再做细分,将特殊用途的内存都划到reserved中。

  • 解析设备树中的chose节点,读取"linux,usable-memory-range",设置可用内存范围;
  • 保存物理内存的起始地址到全局变量memstart_addr;
  • 将内核本身使用的内存保存到预留区,这样整个内核运作过程中所使用的内存独立出来,也就得到保护。
  • 将DMA zone的内存划分给CMA,保存到reserved区,这里请注意,因为memblock后续将可用内存交给buddy,这个可用内存指的是memory类型且没有保存到reserved中的内存。熟悉buddy都知道,DMA32肯定属于buddy管,所以,后续这个CMA区域的内存将会做特殊处理,也会交给buddy管理。
  • 还有其他一些在内核启动以及后续内核一直占用的内存,都会划到reserved区域。

3)paging_init,创建映射,调用memblock分配器做内存分配,比如页表的创建等

4)mem_init,调用memblock_free_all将可用内存转到buddy,memblock的使命也就结束,并且memblock本身所占据的内存也会释放。

4.1.memblock 可用内存初始化

  内核启动后,执行 start_kernel 函数,该函数中 setup_arch 函数对特定架构进行初始化。arm64 调用 setup_machine_fdt 解析设备树,setup_machine_fdt 函数与 memblock 相关的分支如下:

- setup_machine_fdt
    - early_init_dt_scan
        - early_init_dt_scan_nodes
            - early_init_dt_scan_memory
		        - early_init_dt_add_memory_arch
                	- memblock_add

  可见最终会调用上节所述 memblock_add 函数将可用内存写入 memblock 全局变量中,使可用内存区域受 memblock 分配器管理。

4.2.arm64_memblock_init

void __init arm64_memblock_init(void)
{
	/*
	将memblock.reserve保留区域大小强制限定,因为总内存大小有限
	并将最后超标的内存用memblock_remove_range进行截断
	*/
	memblock_enforce_memory_limit(memory_limit);

	/*
	 * Register the kernel text, kernel data, initrd, and initial
	 * pagetables with memblock.
	 */
	memblock_reserve(__pa(_text), _end - _text);
#ifdef CONFIG_BLK_DEV_INITRD
	if (initrd_start)
		memblock_reserve(__virt_to_phys(initrd_start), initrd_end - initrd_start);
#endif
	/*
	1. 将dtb区域加入memblock.reserve, initial_boot_params=fdt
	2. 通过fdt_header.off_mem_rsvmap指针,寻找/memreserve/ fields,并加入memblock.reserve
	3. 使用回调__fdt_scan_reserved_mem, 找出所有"reserved-memory",分析后加入memblock.reserve,并分配内存空间
	*/
	early_init_fdt_scan_reserved_mem();

	/* 4GB maximum for 32-bit only capable devices */
	if (IS_ENABLED(CONFIG_ZONE_DMA))
		arm64_dma_phys_limit = max_zone_dma_phys();
	else
		arm64_dma_phys_limit = PHYS_MASK + 1;
	/* CMA区域或CMA上下文, 保留连续的内存空间给Global CMA area使用,稍后返回给伙伴系统从而可以被用作正常申请, 关于CMA具体可参考:https://www.cnblogs.com/newjiang/p/9592797.html */
	dma_contiguous_reserve(arm64_dma_phys_limit);
	/* 设置memblock_can_resize标志,目前暂未确定用途?*/
	memblock_allow_resize();
	/* 如果打开了memblock_debug开关,则会打印memblock结构体中目前保存的memory、reserve/* 
	memblock_dump_all();
}

  在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化,主要是通过memblock_remove将某些memblock_region区域从memblock.memory中移除,这些区域包含了DDR物理地址所不包含的区域,以及内核线性映射区所不能涵盖的区域;同时将某些物理区间添加到memblock.reserved中,这额区间包含dts中预留区域,命令行中通过参数预留的CMA区域,内核的代码段、initrd、页表、数据段等所在区域,crash kernel保留区域以及elf相关区域。这个过程中也会初始化一些全局变量,如物理内存起始地址memstart_addr。

4.3 memblock 预留内存初始化

  将需要保留的内存添加进预留内存类型集合(memblock.reserved),使得后续使用 memblock 分配内存时,避开预留内存。例如在分页系统初始化过程中会调用 memblock_reserve 函数将内核程序在内存中的范围保留,保证其不会被覆盖,调用关系如下:

    - paging_init
      - setup_bootmem()
        - memblock_reserve(vmlinux_start, vmlinux_end - vmlinux_start)

  类似的,其他预留内存的地方(如设备树中设置的预留内存)也均是通过调用 memblock_reserve 接口函数实现。 关于预留内存的使用,将会在后续分析驱动相关的 ioremap 文章中举例说明。

5.memblock调试

  在kernel command中加入“memblock=debug”。

  在内核启动后,通过/proc/kmsg查看调试信息。

  查看内存地址范围和reserved区域可以通过:

/sys/kernel/debug/memblock/memory 
/sys/kernel/debug/memblock/reserved

refer to

  • Documentation/core-api/boot-time-mm.rst
  • https://www.lagou.com/lgeduarticle/23340.html
  • https://github.com/BiscuitOS/HardStack/tree/master/Memory-Allocator/Memblock-allocator/API
  • https://tinylab.org/riscv-memblock/
  • https://www.cnblogs.com/LoyenWang/p/11440957.html
  • https://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-1.html
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值