9.启动过程期间的内存管理(bootmem分配器)

在启动过程期间,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段早期分配内存。
1. 数据结构
即使最先适配分配器也必须管理一些数据。内核(为系统中的每个结点都)提供了一个bootmem_data结构的实例,用于该用途。当然,该结构所需的内存无法动态分配,必须在编译时分配给内核。
bootmem_data结构定义如下:
<bootmem.h>
typedef struct bootmem_data {
  unsigned long   node_boot_start;
  unsigned long   node_low_pfn;
  void *      node_bootmem_map;
  unsigned long   last_offset;
  unsigned long   last_pos;
  unsigned long   last_success;
  struct list_head    list;
} bootmem_data_t;

 

 
在下面提到页时,总是指物理页帧。
 node_boot_start保存了系统中第一个页的编号,大多数体系结构下都是零。
 node_low_pfn是可以直接管理的物理地址空间中最后一页的编号。换句话说,即ZONE_NORMAL的结束页。
 node_bootmem_map是指向存储分配位图的内存区的指针。在IA-32系统上,用于该用途的内存区紧接着内核映像之后。对应的地址保存在_end变量中,该变量在链接期间自动地插入到内核映像中。
 last_pos是上一次分配的页的编号。如果没有请求分配整个页,则last_offset用作该页内部的偏移量。这使得bootmem分配器可以分配小于一整页的内存区(伙伴系统无法做到这一点)。
 last_success指定位图中上一次成功分配内存的位置,新的分配将由此开始。尽管这使得最先适配算法稍快了一点,但仍然无法真正代替更复杂的技术。
 内存不连续的系统可能需要多个bootmem分配器。一个典型的例子是NUMA计算机,其中每个结点注册了一个bootmem分配器,但如果物理地址空间中散布着空洞,也可以为每个连续内存区注册一个bootmem分配器。注册新的自举分配器可使用init_bootmem_core,所有注册的分配器保存在一个链表中,表头是全局变量bdata_list。在UMA系统上,只需一个bootmem_t实例,即contig_bootmem_data。它通过bdata成员与contig_page_data关联起来。
mm/page_alloc.c
static bootmem_data_t contig_bootmem_data;
struct pglist_data contig_page_data = { .bdata = &contig_bootmem_data };
2. 初始化
bootmem分配器的初始化是一个特定于体系结构的过程,此外还取决于所述计算机的内存布局。正如前文的讨论,IA-32使用setup_memory,该函数又调用setup_bootmem_allocator来初始化
bootmem分配器。

 

 setup_memory分析检测到的内存区,以找到低端内存区中最大的页帧号。全局变量max_low_pfn保存了可映射的最高页的编号。

 

基于该信息,setup_bootmem_allocator接下来负责发起所有必要的步骤,以初始化bootmem分配器。它首先调用通用函数init_bootmem,该函数是init_bootmem_core的一个前端。

init_bootmem_core的目的在于执行bootmem分配器的第一个初始化步骤。先前检测到的低端内存页帧的范围输入到相应的bootmem_data_t实例中,这里是contig_bootmem_data。

register_bootmem_low_pages通过将位图中对应的比特位清零,释放所有潜在可用的内存页。
 
最初在位图contig_bootmemdata->node_bootmem_map中,所有的页都标记为已用。由于bootmem分配器需要一些内存页管理分配位图,必须首先调用reserve_bootmem分配这些内存页。
 分配内存
在NUMA系统上,__alloc_bootmem_node则用于实现该API函数。
首先,工作传递到__alloc_bootmem_core,尝试在该结点的bootmem分配器进行分配。如果失败,则后退到__alloc_bootmem,并将尝试所有的结点。
mm/bootmem.c
void * __init __alloc_bootmem(unsigned long size, unsigned long align,unsigned long goal)
__alloc_bootmem需要3个参数来描述内存分配请求:size是所需内存区的长度,align表示数据的对齐方式,而goal指定了开始搜索适当空闲内存区的起始地址。
(1) 从goal开始,扫描位图,查找满足分配请求的空闲内存区。
(2) 如果目标页紧接着上一次分配的页,即bootmem_data-> last_pos,内核会检查bootmem_data->last_offset,判断所需的内存(包括对齐数据所需的空间)是否能够在上一页分配或从上一页开始分配。
(3) 新分配的页在位图对应的比特位设置为1。最后一页的数目也保存在bootmem_data->last_pos。如果该页未完全分配,则相应的偏移量保存在bootmem_data->last_offset;否则,该值设置为0。
 释放内存
NUMA系统上函数的名称为free_bootmem_node,它需要一个额外的参数来指定结点。
<bootmem.h>
void free_bootmem_node(pg_data_t *pgdat,unsigned long addr,unsigned long size);
 
将其工作委托给__free_bootmem_core。只能释放整页,因为bootmem分配器没有保存有关页划分的任何信息。内核使用__free_bootmem_core首先计算完全包含在该内存区中、将被释放的页。只是部分包含在内存区中的页将忽略。位图中对应的项设置为0,完成页的释放。该过程隐藏了一些风险,如果页包含在两个不同的内存区中,那么连续释放这些内存区,却无法释放该页。包含页的前一半和后一半的内存区在间隔一段时间后分别被释放,分配器无法了解到该页是否不再使用,因而也无法释放。该页的状态就一直保持为“使用中”,尽管事实上不是这样。尽管如此,由于free_bootmem很少使用,这也不是大问题。系统初始化期间分配的大多数内存区都用于基本的数据结构,在内核运行的所有时间都需要使用,因此无需释放。
 
 停用bootmem分配器
在NUMA系统上,停用由free_all_bootmem_node完成。在伙伴系统建立之后,特定于体系结构的初始化代码需要调用这个函数。首先扫描bootmem分配器的页位图,释放每个未用的页。到伙伴系统的接口是__free_pages_bootmem函数,该函数对每个空闲页调用。该函数内部依赖于标准函数__free_page。它使得这些页并入伙伴系统的数据结构,在其中作为空闲页管理,可用于分配。在页位图已经完全扫描之后,它占据的内存空间也必须释放。此后,只有伙伴系统可用于内存分配。
 释放初始化数据
许多内核代码块和数据表只在系统初始化阶段需要。
内核提供了两个属性(__init和__initcall)用于标记初始化函数和数据。这些必须置于函数或数据的声明之前。__init属性插入到函数声明中返回类型和函数名之间。
__init和__initdata不能使用普通的C语言实现,因此内核必须再一次借助于特殊的GNU C编译器语句。
初始化函数实现的背后,其一般性的思想在于,将数据保持在内核映像的一个特定部分,在启动结束时可以完全从内存删除。下列宏的定义即怀着这个目的:
<init.h>
#define __init __attribute__ ((__section__ (".init.text"))) __cold
#define __initdata __attribute__ ((__section__ (".init.data")))
__attribute__ 详细信息参见 __attribute__机制
__attribute__是一个特殊的GNU C关键字,属性即通过该关键字使用。__section__属性用于通知编译器将随后的数据或函数分别写入二进制文件的.init.data和.init.text段(不熟悉ELF文件结构的读者可参考相关资料)
为从内存中释放初始化数据,内核不必知道数据的性质,即哪些数据和函数保存在内存中和它们的用途都是完全不相干的。唯一相关的信息是这些数据和函数在内存中开始和结束的地址。
由于该信息在编译时无法得到,它是内核在链接时插入的。为提供该信息,内核定义了一对变量__init_begin和__init_end,其含义很明显。
free_initmem负责释放用于初始化的内存区,并将相关的页返回给伙伴系统。在启动过程刚好结束时会调用该函数,紧接其后init作为系统中第一个进程启动。启动日志包含了一条信息,指出释放了多少内存。
posted @ 2022-03-19 19:54  while(true);;  阅读(146)  评论(0)    收藏  举报