引导内存分配器
在内核初始化的过程中需要分配内存,内核提供了临时的引导内存分配器,在页分配器和块分配器初始化完毕后,把空闲的物理页交给页分配器管理,丢弃引导内存分配器;
早期使用的引导内存分配器是bootmem,目前正在使用memblock取代bootmem;
bootmem分配器
bootmem分配器使用的数据结构如下:
/*
* node_bootmem_map is a map pointer - the bits represent all physical
* memory pages (including holes) on the node.
*/
typedef struct bootmem_data {
unsigned long node_min_pfn;
unsigned long node_low_pfn;
void *node_bootmem_map;
unsigned long last_end_off;
unsigned long hint_idx;
struct list_head list;
} bootmem_data_t;
node_min_pfn
,起始物理页号;node_low_pfn
,结束物理页号;node_bootmem_map
,指向一个bitmap,每个物理页对应其中的一个bit,如果物理页被分配,把对应的bit设置为1;last_end_off
,上次分配的内存块的结束位置后面一个字节的偏移;hint_idx
,字面意思是“暗示的索引”,是上次分配的内存块的结束位置后面的物理页在bitmap中的位置,下次优先考虑从这个物理页开始分配;
每个内存节点(pglist_data)有一个bootmem_data实例;
struct bootmem_data;
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
struct bootmem_data *bdata;
#endif
...
}
bootmem算法:
- 只把低端内存添加到bootmem分配器,低端内存是可以直接映射到内核虚拟地址空间的物理内存;
- 使用一个bitmap记录哪些物理面被分配,如果物理页被分配,把这个物理页对应的bit设置为1;
- 采用最先适配算法,扫描位图,找到第一个足够大的空闲内存块;
- 为了支持分配小于一页的内存块,记录上次分配的内存块的结束为止后面一个字节的偏移和后面一页的索引,下次分配时,从上次分配的位置后面开始遍历。如果上次分配的最后一个物理页剩余空间足够,可以直接在这个物理页上分配内存。
bootmem分配器对外提供分配内存函数alloc_bootmem,释放内存的函数是free_bootmem。
Linux内核中的bootmem内存引导分配器使用的是基本的First Fit算法,它使用bitmap来表示内存而不是空闲块的链表。该算法在系统启动时用于管理物理内存,它非常基础但高效,适合用于系统启动时的内存分配管理。 bootmem内存引导分配器主要用于在系统引导过程中保留和分配内存页面给内核使用。内核需要分配内存时,bootmem会遍历bitmap来找到合适大小的空闲内存块,并标记为已分配。
最新的内核已经不支持bootmem分配器;
memblock
memblock是Linux内核中在系统启动早期用于管理物理内存的一种内存分配器。它的工作原理是在系统启动过程中,根据启动程序(如bootloader)提供的物理内存布局信息,建立内存块描述符(memblock descriptor)数据结构。这些描述符包含了系统中各个内存区域的起始地址、大小、状态等信息。memblock分配器允许内核动态地对这些物理内存区域进行分配和释放。在伙伴系统的接管内存管理时将 memblock中可用的空闲内存全部释放给伙伴系统,并丢弃 memblock 内存分配器。
memblock数据结构
图片来源于网上
struct memblock
最顶层是一个struct memblock结构体,用于管理memblock;
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;
#endif
};
- bottom_up
表示内存分配时是否使用自底向上的方式进行分配;
- current_limit
表示当前可分配的最大物理地址;
- memory
表示可供系统使用的内存范围,即可用于动态分配给进程、内核和设备的内存;
- reserved
表示被内核保留的内存范围,通常用于内核自身的数据结构、堆栈、页表等,并且不会被分配给用户进程;后续从 memory 域释放给伙伴系统的可用空闲内存需要全部避开 reserved 域的内存;
- physmem
表示整个物理内存的范围,即系统中所有可寻址的物理内存范围;
三者之间的关系:reserved ⊆ memory ⊆ physmem;
struct memblock_type
struct memblock_type管理内存域集合(regions),当前定义了3个内存域集合,即memory,reserved,physmem;
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;
char *name;
};
- cnt
当前内存域集合中的region数量;
- max
当前内存域集合中能保存的最大region数量;
- total_size
当前内存域集合中所有region的总内存大小
- regions
内存域集合的实例数组;
- name
该内存域集合的名称,主要用于打印信息(“memory”, “reserved”, “physmem”);
struct memblock_region
该结构体表示每个region的实际数据结构;
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid;
#endif
};
- base
该region的起始物理地址;
- size
该region的内存大小;
- flags
用于标识当前region的属性;
flag具体含义如下:
/* 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_NONE
每个区域的默认值,表示该内存区域没有特殊要求;
- MEMBLOCK_HOTPLUG
表示可以热插拔的内存区域,即在系统运行过程中可以拔出或插入物理内存;
- MEMBLOCK_MIRROR
镜像内存区域;内存镜像是一种内存冗余技术,将内存数据做复制,分别放在主内存和镜像内存中;
- MEMBLOCK_NOMAP
不添加到内核的直接映射中,即线性映射区;
- nid
如果系统支持NUMA,nid表示NUMA的节点ID;
memblock分配器初始化流程
Linux内核中,ARM64架构的memblock内存分配器初始化流程通常包括以下步骤:
- 获取物理内存信息:在系统引导早期阶段,引导加载程序(如U-Boot)会收集并传递物理内存布局信息给内核。
- memblock描述符初始化:内核启动后,会根据传递进来的物理内存布局信息调用相关初始化函数,将这些信息转换成memblock描述符数据结构。
- 建立内存描述符:memblock描述符包含了系统中各个内存区域的起始地址、大小和状态等信息,会被用于后续的内存分配和管理操作。
- 提供内存分配和释放功能:一旦memblock描述符初始化完成,memblock内存分配器便可以为内核提供内存分配和释放的功能。通过相应的API函数,如memblock_alloc()和memblock_free(),内核可以在系统启动后动态地对物理内存进行分配和释放操作。
start_kernel
->setup_arch
->arm64_memblock_init
arm64_memblock_init函数在ARM64架构的Linux内核中的作用是初始化内存块管理器和处理可用内存范围,其具体逻辑包括以下几个步骤:
- 通过解析设备树二进制文件节点,获取可用内存的范围,然后从memblock中删除超出该范围的物理内存。
- 确保线性地址范围占用了内核虚拟地址空间的一半,并用一个单独的位来区分线性地址和内核/模块/vmalloc地址。
- 选择适当的物理内存基址,并从memblock中删除无法使用线性映射覆盖的内存范围,同时避免剪切可能位于较高内存中的内核。
- 根据命令行中设备树二进制文件节点指定的内存大小命令,删除超出可用长度的物理内存范围,同时添加必须通过线性映射访问的内核区域。
- 如果启用了initrd,添加刚刚删除的内存,然后将initrd标记为保留。
- 如果启用了CONFIG_RANDOMIZE_BASE,将线性地址进行随机化处理。
- 最后,将内核文本、内核数据、initrd以及初始页表注册到memblock中,并读取设备树二进制文件的内存保留区域,然后添加到memblock中。
该函数还涉及其他操作,如设置32位设备的最大内存限制、保留用于内核崩溃时使用的内存空间、以及设置内存页的连续性等。最后,调用了memblock_allow_resize函数,允许内存调整操作。
memblock分配器api接口
memblock_add,将一块内存添加到内存块分配器管理的内存池中,这个函数通常在内存管理初始化阶段调用,用于在memblock内存池中增加额外的可用内存;
memblock_remove,用于从内存块分配器中移除特定的内存块。这个函数可以用于释放不再需要的内存块,从而有效地管理内存池的大小和可用性;
memblock_alloc,分配内存块;
memblock_free,释放内存块;
memblock核心算法逻辑
后续调查补充
参考文献
- 《linux内核深度解析》,余华兵著