Linux内核源码分析(八)--启动内存分配器

在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);

}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值