在Linux内核启动的过程中需要分配内存,但完整的内存管理子系统还没有初始化,此时需要一个简单的内存管理办法,就是我们要介绍的启动内存分配器。
启动内存分配器主要用位图来管理物理页的分配,一个页的分配状态可以用一个位来表示。所以初始化启动内存分配器的时候,肯定要知道总共有多少物理页,起始的物理页在哪里等这样的信息。所以我们可以猜想有以下特征的一个数据结构:
(1) 包含一个记录物理页分配状态的位图
(2) 包含一个指向起始物理页的指针
(3) 物理页的个数或最后一个物理页的地址
具体的设计可能会有差异,不过大体的思路应该是这样。我们可以通过具体的代码来分析。
启动内存分配器代码的分析入口是start_kernel()->setup_arch()->paging_init(),我们就从paging_init开始逐步分析吧。
paging_init需要两个参数,这里只用到了第一个参数,是一个指向struct meminfo结构变量的指针。这个meminfo变量定义在arch/arm/mm/init.c中。
struct meminfo的定义在include/asm-arm/setup.h头文件中。
/*
*Memory map description
*/
#ifdef CONFIG_ARCH_LH7A40X
# define NR_BANKS 16
#else
# define NR_BANKS 8
#endif
struct membank {
unsignedlong start;
unsignedlong size;
int node;
};
struct meminfo {
int nr_banks;
struct membank bank[NR_BANKS];
};
来到paging_init首先映入眼帘的是build_mem_type_table函数,顾名思义这是要构建一个内存类型表,这里的内存指的是cpu可以访问的空间,不是特指物理内存,构建的参数表是用来设置mmu的。
之后是prepare_page_table函数,这个一看是在准备页表。不妨把主要代码贴出来分析一下。
static inline void prepare_page_table(struct meminfo *mi) { unsigned long addr;
for (addr = 0; addr < MODULE_START; addr += PGDIR_SIZE) pmd_clear(pmd_off_k(addr)); … for ( ; addr < PAGE_OFFSET; addr += PGDIR_SIZE) pmd_clear(pmd_off_k(addr)); … for (addr = __phys_to_virt(mi->bank[0].start + mi->bank[0].size); addr < VMALLOC_END; addr += PGDIR_SIZE) pmd_clear(pmd_off_k(addr)); } |
看来就主要是对不同的地址执行pmd_clear,而传给pmd_clear的地址是通过pmd_off_k将虚拟地址转换而来的。不如先看下pmd_off_k吧?可以从arch/arm/mm/mm.h中找到下面两个函数。
static inline pmd_t *pmd_off(pgd_t *pgd,unsigned long virt)
{
returnpmd_offset(pgd, virt);
}
static inline pmd_t *pmd_off_k(unsignedlong virt)
{
returnpmd_off(pgd_offset_k(virt), virt);
}
pgd_offset_k是个宏,定义在include/asm-arm/pgtable.h中。
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
这里发现有两个相似的东西,pgd_offset和pmd_offset,都是在寻找目录项的offset,只不过一个是在pgd中找,一个是在pmd中找。我们当前分析的Linux内核使用的是两级页表,所以pmd_offset的实现就是把pgd中的目录项转换为pmd_t数据类型,接口上使人感觉还有一个二级目录。
在以前的代码分析中,我们知道一级页目录有16KB,一个页目录项占4个字节,所以总共有4K个页目录项,平均一个页目录项要间接找到1M个页才能满足4GB的空间。所以我们划分虚拟地址的时候可以按1M的虚拟空间对应一个页目录项。但事实上,我们知道虚拟地址的划分是由arm的分页方式决定的,是已经规定好了的,我们分析的Linux内核采用的分页方式是arm的二级粗粒度小页映射。
31~20(12位) | 19~12(8位) | 11~0(12位) |
一级页表内偏移序号 | 二级页表内偏移序号 | 页内偏移序号 |
这些看上去都没啥问题,但比较搞事情的是Linux定义的PGDIR_SHIFT是21,0~19不应该是20位吗?这样1左移PGDIR_SHIFT位就是2M的空间,虚拟地址右移PGDIR_SHIFT位就是包含两个页目录项的起始索引。当初这么定义的具体指导思想是啥我也很想搞清楚?但最终用法还是和硬件上吻合的。比如pmd_clear一次清了两个页目录项。好了,从prepare_page_table函数返回之后正式进入bootmem_init,就是启动内存分配器的初始化函数。下面继续粘点代码凑下字数:
void__init bootmem_init(struct meminfo *mi)
{
unsigned long memend_pfn = 0;
int node, initrd_node, i;
for (i = 0; i < mi->nr_banks; i++)
if (mi->bank[i].size == 0 ||mi->bank[i].node >= MAX_NUMNODES)
mi->bank[i].node = -1;
memcpy(&meminfo, mi,sizeof(meminfo));
initrd_node = check_initrd(mi);
for_each_node(node) {
unsigned long end_pfn;
end_pfn = bootmem_init_node(node, initrd_node,mi);
if (end_pfn > memend_pfn)
memend_pfn = end_pfn;
}
high_memory = __va(memend_pfn <<PAGE_SHIFT);
max_pfn = max_low_pfn = memend_pfn -PHYS_PFN_OFFSET;
}
先检查无效的bank,接着把传来的mi信息保存到全局的meminfo变量,找出initrd所在的内存node。然后遍历所有的node,执行bootmem_init_node。
staticunsigned long __init
bootmem_init_node(intnode, int initrd_node, struct meminfo *mi)
{
unsigned long zone_size[MAX_NR_ZONES],zhole_size[MAX_NR_ZONES];
unsigned long start_pfn, end_pfn,boot_pfn;
unsigned int boot_pages;
pg_data_t *pgdat;
int i;
start_pfn = -1UL;
end_pfn = 0;
for_each_nodebank(i, mi, node) {
struct membank *bank =&mi->bank[i];
unsigned long start, end;
start = bank->start >>PAGE_SHIFT;
end = (bank->start +bank->size) >> PAGE_SHIFT;
if (start_pfn > start)
start_pfn = start;
if (end_pfn < end)
end_pfn = end;
map_memory_bank(bank);
}
if (end_pfn == 0)
return end_pfn;
boot_pages =bootmem_bootmap_pages(end_pfn - start_pfn);
boot_pfn = find_bootmap_pfn(node, mi,boot_pages);
node_set_online(node);
pgdat = NODE_DATA(node);
init_bootmem_node(pgdat, boot_pfn,start_pfn, end_pfn);
for_each_nodebank(i, mi, node)
free_bootmem_node(pgdat,mi->bank[i].start, mi->bank[i].size);
reserve_bootmem_node(pgdat, boot_pfn<< PAGE_SHIFT,
boot_pages << PAGE_SHIFT);
#ifdefCONFIG_BLK_DEV_INITRD
if (node == initrd_node) {
reserve_bootmem_node(pgdat,phys_initrd_start,
phys_initrd_size);
initrd_start =__phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
}
#endif
if (node == 0)
reserve_node_zero(pgdat);
memset(zone_size, 0, sizeof(zone_size));
memset(zhole_size, 0,sizeof(zhole_size));
zone_size[0] = end_pfn - start_pfn;
zhole_size[0] = zone_size[0];
for_each_nodebank(i, mi, node)
zhole_size[0] -=mi->bank[i].size >> PAGE_SHIFT;
arch_adjust_zones(node, zone_size,zhole_size);
free_area_init_node(node, pgdat,zone_size, start_pfn, zhole_size);
return end_pfn;
}
为当前的node中的每个bank执行map_memory_bank进行虚拟地址的映射,映射提供的参数是虚拟地址、起始物理页框、映射长度,以及映射的空间类型。
map.pfn= __phys_to_pfn(bank->start);
map.virtual= __phys_to_virt(bank->start);
map.length= bank->size;
map.type= MT_MEMORY;
不妨看一下__phys_to_virt的定义。
#define__phys_to_virt(x) ((x) - PHYS_OFFSET +PAGE_OFFSET)
PHYS_OFFSET是物理地址的起始地址,PAGE_OFFSET是内核虚拟地址的起始地址(第一个虚拟页的起始地址)。
这些参数最终会传给create_mapping函数进行映射操作。
换作是你,你要做什么?
1) 根据虚拟地址算出页目录项的位置
2) 填充页目录项或页表项
create_mapping函数最终会用alloc_init_section决定是按段映射填充还是按页的访问方式填充。
if(((addr | end | phys) & ~SECTION_MASK) == 0) {
pmd_t *p = pmd;
if (addr & SECTION_SIZE)
pmd++;
do {
*pmd = __pmd(phys |type->prot_sect);
phys += SECTION_SIZE;
} while (pmd++, addr += SECTION_SIZE,addr != end);
flush_pmd_entry(p);
} else {
alloc_init_pte(pmd, addr, end,__phys_to_pfn(phys), type);
}
看到这个判断多少人会心里一凉,我想绝大多数的地址都是符合段映射的条件吧?避开残酷的现实,我们还是看下alloc_init_pte。
staticvoid __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
unsigned long end, unsigned long pfn,
const struct mem_type *type)
{
pte_t *pte;
if (pmd_none(*pmd)) {
pte = alloc_bootmem_low_pages(2 *PTRS_PER_PTE * sizeof(pte_t));
__pmd_populate(pmd, __pa(pte) |type->prot_l1);
}
pte = pte_offset_kernel(pmd, addr);
do {
set_pte_ext(pte, pfn_pte(pfn,__pgprot(type->prot_pte)),
type->prot_pte_ext);
pfn++;
} while (pte++, addr += PAGE_SIZE, addr!= end);
}
什么情况?我们还在讲启动内存分配器,这里已经开始用上了,太不值钱了吧?
如果页目录项没被填充过,就会执行alloc_bootmem_low_pages分配两个二级的页表,最终填充页目录项和二级页表项。
alloc_bootmem_low_pages是个宏,真实的分配函数是__alloc_bootmem_low函数。
void *__init __alloc_bootmem_low(unsigned long size, unsigned long align,
unsigned long goal)
{
bootmem_data_t *bdata;
void *ptr;
list_for_each_entry(bdata,&bdata_list, list) {
ptr = __alloc_bootmem_core(bdata,size, align, goal,
ARCH_LOW_ADDRESS_LIMIT);
if (ptr)
return ptr;
}
printk(KERN_ALERT "low bootmem allocof %lu bytes failed!\n", size);
panic("Out of low memory");
return NULL;
}
分配函数会遍历bdata_list链表,用__alloc_bootmem_core函数进行分配。年轻的人又开始迷茫了,一路走来都没看到这个bdata_list的初始化啊,这东西从哪来的?感觉要陷入鸡和蛋的尴尬困境中了。可能是因为算准了要走段映射的分支才这么有恃无恐吧?但总让人感觉不爽,万一有搞事情的程序员非传个不是段对齐的地址呢?你把接口暴露出来总要负责任的吧?一点科学精神都没有。
一点心情都没有了,后面的分配过程还是等分析完初始化再看吧。
直接从map_memory_bank函数返回。映射的循环结束后start_pfn和end_pfn也都知道了。接下来寻找能否够存放bootmem位图结构的页。
bootmem_bootmap_pages计算存放位图需要多少页,find_bootmap_pfn寻找适合存放位图的起始物理页框。
我们的代码在mm/page_alloc.c中有这样的定义。
staticbootmem_data_t contig_bootmem_data;
structpglist_data contig_page_data = { .bdata = &contig_bootmem_data };
计算得到bootmem位图的物理页框之后,就正式进入正篇了,init_bootmem_node函数会完成bootmem分配器的初始化,传入的参数是pgdat, boot_pfn, start_pfn, end_pfn。
进而调用init_bootmem_core完成初始化。
staticunsigned long __init init_bootmem_core(pg_data_t *pgdat,
unsigned long mapstart, unsigned longstart, unsigned long end)
{
bootmem_data_t *bdata = pgdat->bdata;
unsigned long mapsize;
bdata->node_bootmem_map =phys_to_virt(PFN_PHYS(mapstart));
bdata->node_boot_start =PFN_PHYS(start);
bdata->node_low_pfn = end;
link_bootmem(bdata);
mapsize = get_mapsize(bdata);
memset(bdata->node_bootmem_map, 0xff,mapsize);
return mapsize;
}
前面提到的contig_page_data上有个bdata,这个bdata长的下面这个样子。
typedef structbootmem_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是结束物理页框编号。
node_bootmem_map指向位图所在的虚拟地址。
然后执行link_bootmem将bdata挂到bdata_list链表上。
从init_bootmem_node函数返回后,代码接着遍历当前node上的每个bank,并执行free_bootmem_node函数设置位图状态,实际的事情还是free_bootmem_core函数做的。
staticvoid __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr,
unsignedlong size)
{
unsigned long sidx, eidx;
unsigned long i;
BUG_ON(!size);
BUG_ON(PFN_DOWN(addr + size) >bdata->node_low_pfn);
if (addr < bdata->last_success)
bdata->last_success = addr;
sidx = PFN_UP(addr) -PFN_DOWN(bdata->node_boot_start);
eidx = PFN_DOWN(addr + size -bdata->node_boot_start);
for (i = sidx; i < eidx; i++) {
if (unlikely(!test_and_clear_bit(i,bdata->node_bootmem_map)))
BUG();
}
}
找到起始和结束的物理页的index,在位图中清空正中间所有的位。bdata上的last_success记录的是到目前为止的最小地址,用来加快搜索速度。
清空完位图之后,会用一系列的reserve函数来设置位图,表示一些重要空间被占用了。比如位图的地址、initrd的地址等等。那内核代码段及数据段的空间在哪标识被占用的呢?
在reserve_node_zero函数里,你可以找到答案。
至此bootmem算是准备好了,最后free_area_init_node函数为伙伴系统将来接管工作做了一点准备。
void__meminit free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size,unsigned long node_start_pfn,
unsigned long *zholes_size)
{
pgdat->node_id = nid;
pgdat->node_start_pfn =node_start_pfn;
calculate_node_totalpages(pgdat,zones_size, zholes_size);
alloc_node_mem_map(pgdat);
free_area_init_core(pgdat, zones_size,zholes_size);
}