Linux内核引导内存分配器
Linux内核引导内存分配器(Boot Memory Allocator,通常简称为bootmem)是内核在启动初期用于管理物理内存的一种简单机制。
一、背景与需求
在Linux内核启动的早期阶段,系统的物理内存布局尚未完全确定,且可能包含各种复杂的硬件特定的内存配置(如内存空洞、保留区域等)。此时,需要一个简单而有效的内存分配机制来满足内核启动过程中对内存的基本需求。引导内存分配器正是为了满足这一需求而设计的。
1.bootmem 分配器的核心数据结构,用于描述和管理一个节点(Node)的可用内存区域。
typedef struct bootmem_data {
unsigned long node_min_pfn; // 该节点中最小页帧号
unsigned long node_low_pfn; // 该节点中可用的最低页帧号(可能受内存空洞等限制)
void *node_boot_start; // 该节点启动内存区域的起始虚拟地址
struct pglist_data *node_pg_data; // 指向包含该节点的 `pglist_data` 结构(用于 NUMA 系统)
// 以下为分配器内部使用的数据
unsigned long last_end_off; // 上次分配结束的页内偏移量(用于提高分配效率)
unsigned long hint_idx; // 用于记录上次分配的位置,以优化后续分配
struct list_head list; // 将所有 `bootmem_data` 结构链接成一个链表
// 内存位图相关
unsigned long *node_bitmap; // 指向该节点内存位图的指针
unsigned long bitmap_size; // 位图的大小(以字节为单位)
} bootmem_data_t;
2.关键字段解释:
node_min_pfn 和 node_low_pfn:定义了该节点中物理内存页帧号的范围。
node_bitmap:一个位图,每一位代表一个内存页的使用状态(0 表示空闲,1 表示已分配)。
hint_idx:用于记录上次分配的位置,以优化后续分配的效率(首次适应算法的变种)。
内存位图(Bitmap),作用:用于跟踪物理内存页的使用情况。实现:一个连续的位数组,每一位对应一个物理内存页。如果某位为 0,表示对应的内存页空闲,可以分配。如果某位为 1,表示对应的内存页已被分配。大小计算:位图的大小取决于系统中可用内存页的数量。例如,对于 1GB 内存(假设每页 4KB),共有 262,144 个页,因此位图需要 262,144/8/1024 = 32KB。
3. max_low_pfn 和 min_low_pfn
max_low_pfn:系统可用的最高物理页帧号(通常受限于硬件或内核配置)。
min_low_pfn:系统可用的最低物理页帧号(通常为 0,但可能受内存空洞等影响)。
这两个值用于确定 bootmem 分配器管理的物理内存范围。
4. contig_page_data
在非 NUMA 系统中,contig_page_data 是一个全局的 pglist_data 结构,用于描述整个系统的物理内存布局。bootmem 分配器通过 contig_page_data->bdata 指向 bootmem_data_t 结构,从而管理物理内存。
5. 分配和释放操作
分配内存:bootmem 分配器通过扫描位图,找到第一个足够大的连续空闲内存块(首次适应算法)。
分配成功后,将对应的位图位设置为 1。
释放内存(较少使用):在引导阶段,通常不会释放内存,因为内存需求是单向增长的。
如果需要释放,会将对应的位图位清零。
6. 与伙伴系统的交接
一旦内核完成初始化,bootmem 分配器会被更高效的 伙伴系统(Buddy System) 取代。
此时,bootmem 管理的内存会被伙伴系统接管,后续的内存分配和释放由伙伴系统负责。
总结:bootmem 分配器通过 bootmem_data_t 结构体和内存位图(bitmap)来管理物理内存,采用简单的首次适应算法进行分配。它的设计目标是简单高效,适用于内核启动早期的内存分配需求。然而,由于其缺乏复杂的内存管理功能(如内存释放、碎片整理等),在内核完成初始化后会被更强大的伙伴系统取代。ARM64已经不使用bootmem分歧器而是使用memblock.
/**
* enum memblock_flags - definition of memory region attributes
* @MEMBLOCK_NONE: no special request
* @MEMBLOCK_HOTPLUG: memory region indicated in the firmware-provided memory
* map during early boot as hot(un)pluggable system RAM (e.g., memory range
* that might get hotunplugged later). With "movable_node" set on the kernel
* commandline, try keeping this memory region hotunpluggable. Does not apply
* to memblocks added ("hotplugged") after early boot.
* @MEMBLOCK_MIRROR: mirrored region
* @MEMBLOCK_NOMAP: don't add to kernel direct mapping and treat as
* reserved in the memory map; refer to memblock_mark_nomap() description
* for further details
* @MEMBLOCK_DRIVER_MANAGED: memory region that is always detected and added
* via a driver, and never indicated in the firmware-provided memory map as
* system RAM. This corresponds to IORESOURCE_SYSRAM_DRIVER_MANAGED in the
* kernel resource tree.
* @MEMBLOCK_RSRV_NOINIT: memory region for which struct pages are
* not initialized (only for reserved regions).
*/
enum memblock_flags {
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_DRIVER_MANAGED = 0x8, /* always detected via a driver */
MEMBLOCK_RSRV_NOINIT = 0x10, /* don't initialize struct pages */
};
/**
* struct memblock_region - represents a memory region
* @base: base address of the region
* @size: size of the region
* @flags: memory region attributes
* @nid: NUMA node id
*/
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
enum memblock_flags flags;
#ifdef CONFIG_NUMA
int nid;
#endif
};
/**
* struct memblock_type - collection of memory regions of certain type
* @cnt: number of regions
* @max: size of the allocated array
* @total_size: size of all regions
* @regions: array of regions
* @name: the memory type symbolic name
*/
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
/**
* struct memblock - memblock allocator metadata
* @bottom_up: is bottom up direction?
* @current_limit: physical address of the current allocation limit
* @memory: usable memory regions
* @reserved: reserved memory regions
*/
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
二、工作原理
初始化:
在内核启动过程中,引导内存分配器会首先被初始化。它会扫描系统的物理内存布局,并记录下所有可用的内存区域。
分配器会创建一个位图(bitmap)或其他数据结构来跟踪哪些内存页已经被分配,哪些仍然可用。
分配内存:
当内核的其他部分在启动过程中需要内存时,它们会向引导内存分配器发出请求。
分配器会查找其内部数据结构,找到一个足够大的连续内存块来满足请求。
一旦找到合适的内存块,分配器会将其标记为已分配,并返回给请求者一个指向该内存块的指针。
释放内存(在引导阶段通常不执行):
在引导内存分配器的设计中,通常不考虑内存的释放。这是因为内核启动过程中的内存需求是单向增长的,一旦分配出去的内存很少会被再次释放。
然而,在某些特殊情况下,如启动过程中断并需要回滚时,可能会考虑实现内存的释放机制。但这并不是引导内存分配器的常规功能。
替换与交接:
一旦内核完成了启动过程并进入了正常运行状态,引导内存分配器就会被更复杂的内存管理机制(如伙伴系统)所替换。
此时,引导内存分配器所管理的内存会被伙伴系统接管,并由伙伴系统负责后续的内存分配和释放操作。
三、特点与局限性
特点:
简单性:引导内存分配器的设计相对简单,易于实现和理解。
高效性:在内核启动过程中,引导内存分配器能够快速地响应内存分配请求。
灵活性:能够适应各种复杂的物理内存布局。
局限性:
缺乏释放机制:如前所述,引导内存分配器通常不考虑内存的释放,这可能导致内存的浪费。
仅适用于启动阶段:引导内存分配器仅设计用于内核的启动阶段,无法满足内核在正常运行过程中对内存管理的复杂需求。
总体上,Linux内核引导内存分配器是内核启动过程中不可或缺的一部分,它为内核的其他部分提供了在物理内存布局尚未完全确定时的基本内存分配服务。然而,随着内核的启动完成和进入正常运行状态,引导内存分配器会被更强大的内存管理机制所替换。