Linux内存管理 -- Boot Memory Allocator

Linux内存管理 – Boot Memory Allocator

在编译阶段就初始化内核核心的memory struct是不现实的,因为有太多的硬件配置的排列组合。但是要初始化这些基本的structures需要用到内存,甚至说physical page allocator也需要分配内存来初始化它自己。但时physical page allocator如何来分配内存来初始化自己呢 ?
为了达到这个目标,一种叫做Boot Memory Allocator的特殊分配被使用。它是基于一种最基本的allocator:First Fit allocator的原理实现的。它使用bitmap来呈现memory的状态而不是使用链表将free blocks连接起来。如果一个bit是1,那么该page已经被分配了,如果是0,则该page还未被分配。为了能够满足比一个page还小内存块的分配,boot memory allocator会记住上次分配使用的PFN(页框号)和上次分配完成之后在该page中的偏移。将细小的内存块的分配尽量集中在相同的page中。
至此,很多人可能会问为什么改分配器没有在系统运行起来之后再继续使用?一种最终要的原因是尽管该分配器不会造成严重的内存碎片,但是每次分配过程中都需要线性扫描搜索内存来满足当前的这次分配。因为是检查bitmaps,所以代价是很昂贵的,尤其是first fit算法倾向将小块分配放在物理内存的开头,但是这些内存区域在分配大块内存时也需要扫描,因此该过程将非常浪费。

unsigned long init_bootmem(unsigned long start, unsigned long page)
初始化0 ~~ PFN page个page的物理内存,可用内存从PFN start开始
void reserve_bootmem(unsigned long addr, unsigned long size)
标记从地址addr开始,到地址addr + size终止的这段内存区域为保留区域。部分保留页面的请求将导致整个页面被保留。
void free_bootmem(unsigned long addr, unsigned long size)
标记从地址addr开始,到地址addr + size终止的这段内存区域为free
void * alloc_bootmem(unsigned long size)
从ZONE_NORMAL中分配size个字节的内存。通过此函数分配的地址将会与L1 硬件缓存对齐从而最大效率利用硬件缓存
void * alloc_bootmem_pages(unsigned long size)
从ZONE_NORMAL中分配满足size的页对齐的pages,如:不足一页返回整页,不足两页返回两个整页。
void * alloc_bootmem_low_pages(unsigned long size)
从ZONE_NORMAL中分配满足size的页对齐的pages,如:不足一页返回整页,不足两页返回两个整页。
unsigned long bootmem_bootmap_pages(unsigned long pages)
计算分配pages个page所需要的用于代表分配状态的bitmap所占用page的个数
unsigned long free_all_bootmem()
在boot allocator生命结束的时候使用。它遍历bitmap中的所有页面。每个page将释放给physical page allocator。

Representing the Boot Map

系统中每个node都包含有一个bootmem_data struct。它包有boot memory allocator从一个node中分配内存所需的信息,比如:代表page状态的bitmap和分配的page的位置。其定义如下:

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;	/* Previous allocation point.  To speed  up searching */
} bootmem_data_t;
  • node_boot_start:当前bootmem_data结构中所描述的memory块的起始物理地址。
  • node_low_pfn:当前bootmem_data结构中所描述memory块的结束pfn。换句话说,就是该node中ZONE_NORMAL的结束物理地址。
  • node_bootmem_map:代表page分配状态的bitmap的地址。
  • last_offset:在某个page中上次分配完成之后的偏移。如果为0,则代表该page全部被分配完了。
  • last_pos:上次分配所使用的page的PFN。将last_pos和last_offset联合起来,可以测试多个分配是否可以合并到一个page中进行而不是使用一个全新的page
  • last_success:上次分配所对应的物理地址

其中node_bootmem_map数组中的每个bit代表一个page,下面来看下函数bootmem_bootmap_pages()的实现:

unsigned long __init bootmem_bootmap_pages (unsigned long pages)
{
	unsigned long mapsize;
	mapsize = (pages+7)/8;//一个page代表为一个bit,将pages个bit转换成字节数,也就是字节对齐
						  //比如pages = 11, (11 + 7)/8 = 2
	mapsize = (mapsize + ~PAGE_MASK) & PAGE_MASK;//将字节数进行页对齐,比如:mapsize = 10,计算之后mapsize = 4096.
	                                             //如果mapsize = 4098,计算之后mapsize = 8192
	mapsize >>= PAGE_SHIFT;
	return mapsize;
}

该函数的作用是有pages个物理page,bitmap需要多大的空间。
下面来看下两次分配过程中上述字段的变化:
boot

Allocating Memory

reserve_bootmem()函数可以为调用者保留页面,但该函数对已常规分配来说非常麻烦。因此有四个函数: alloc_bootmem(), alloc_bootmem_low(), alloc_bootmem_pages() and alloc_bootmem_low_pages()
这四个函数最终都会调用到__alloc_bootmem_core(),其参数含义如下:

static void * __init
__alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,
								unsigned long align, unsigned long goal)
  • bdata:为每个node中对应的bootmem_data struct
  • size:当前需要分配的内存大小
  • align:需要进行字节对齐,主要是为了与硬件cache对齐高效利用L1 CACHE
  • goal:用户期望得到该从该地址开始的size大小的内存块。

该函数最聪明的部分是处理最新一次的分配能否和上一次分配合并。如果满足以下条件就能合并,

  • 上次分配所后的last_pos,与此次分配找到的page是连续的
  • 上次分配之后的page中海油空闲的空间last_offset != 0
  • 对齐大小小于PAGE_SIZE

无论当前的分配是否有与上次分配合并,last_pos和last_offse都将更新用来显示当前分配所使用的是哪一个page以及该page使用了多少空间。如果该page全部被使用,则last_offset = 0。

Freeing Memory

与分配函数相比,释放函数只有free_bootmem(),其核心原理相当简单。该free会影响每个整体page,将page在bitmap中对应的bit清零。如果在此次清零时发现已经是0,则是由double free的错误上报。
对于free函数来说有个严格的限制:只有full pages才可能被释放。系统不会记录某个page部分被分,如果部分被释放的,该page整体仍是预留的。这并不像它所表现的问题那么严重,因为此次分配会在系统生命周期内持续。但是在系统启动阶段,它对于开发者来说仍是一个中药的限制。
下面来看下free函数的源代码:(有些疑问??)

static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
	unsigned long i;
	unsigned long start;
	/*
	 * round down end of usable mem, partially free pages are
	 * considered reserved.
	 */
	unsigned long sidx;
	unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE;//获取从addr开始size大小地址块的结束相对页框号
																//因为node_bootmem_map中的bit 对应的是相对页框号,从0,1,2...开始
																//bdata->node_boot_start可能在第100个page中。
	unsigned long end = (addr + size)/PAGE_SIZE;

	BUG_ON(!size);
	BUG_ON(end > bdata->node_low_pfn);

	if (addr < bdata->last_success)
		bdata->last_success = addr;

	/*
	 * Round up the beginning of the address.
	 */
	start = (addr + PAGE_SIZE-1) / PAGE_SIZE;//获取从addr开始size大小地址块的起始绝对页框号
	sidx = start - (bdata->node_boot_start/PAGE_SIZE);//获取addr的相对页框号
    //如果sidx = eidx,则该page不会释放.即此次要释放的内存大小不足一个page,那这种page是时候释放?
    //另外,如果size不足一个page,但addr 和addr + size落在两个相邻的page中,直接释放掉会不会出错 ?
	for (i = sidx; i < eidx; i++) {
		if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map)))
			BUG();
	}
}

Retiring the Boot Memory Allocator

众所周知,在系统启动过程的后半部分,start_kernel()函数被调用用来安全的删除boot allocator和以及和它相关的数据结构。每种硬件架构下都会提供一个mem_init()的函数来销毁boot memory allocator和其相关数据结构。
mem_init()函数的目的非常简单。它负责计算low memory 和high memory的大小并打印出信息给使用者来做最终的硬件初始化。在x86上与virtual memory相关的主要函数是set_highmem_pages_init()。
mem_init()首先通过free_all_bootmem()来讲boot memory allocator退休即释放boot memory自己所占用和分配出去的memory。该函数会调用free_all_bootmem_core(),其核心步骤如下:

  • boot memory allocator中为分配的pages:
    • 清除struct page中的PG_reserve的标记位
    • 将count设置为1
    • 调用__free_pages()将memory移交给buddy allocator来构建它的free lists
  • 释放bitmap中使用过的pages并将这些pages移交给buddy allocator。

至此,buddy allocator将完全控制所有page中的low memory的部分,留下hig memory pages。当free_all_bootmem()返回后,mem_init()将首先出于统计的目的,计算一下保留pages的个数。然后调用set_highmem_pages_init()来处理high memory pages。但是在此时,应该要清除mem_map数组是如何分配和初始化的,以及如何将mem_map数组中的struct pages给到main allocator。
在但个node系统中初始化low memory的基本流程见下图:
retire
一旦free_all_bootmem()返回,ZONE_NORMAL中的所有pages就已经交给buddy allocator了。然后调用set_highmem_pages_init()-> one_highpage_init()来循环初始化highstart_pfn到highend_pfn之间的每一个high memory page。one_highpage_init()简单地清除PG_reserved标记。将count设置为1然后再调用__free_pages()来将该struct page移交给buddy allocator,其过程与free_all_bootmem_core()中的行为一致。
此时,boot memory allocator不再需要了并且buddy allocator是系统中主要的物理page allocator。一个有趣的特点需要注意下就是不仅仅boot allocator的数据被删除了,其代码同时也被删除了。只在系统初始化阶段用到的初始化函数会使用__init标记:

unsigned long __init free_all_bootmem (void)

所有的这些函数都被连接器集中放置在.init段。在x86上,函数free_initmem()从__init_begin到__init_end遍历所有page并且将这些page释放给buddy allocator。使用这种方法,Linux可以释放大量的memory:启动阶段使用的代码,系统启动后就不再需要的代码。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章