ARM linux 分析之 内存初始化 一

分析平台:mini2440

1.2.1 setup_arch()

需要重点分析的函数。该函数主要完成的是解析引导程序传下来的内核参数以及初始化页全局目录及部分页表。流程如下:

 

图1-5:setup_arch流程图

1.2.1.1 获取并解析内核参数

前面提到内核参数按照TLV结构顺序存放于0x13000100地址开始的地方。有一个疑问是这些参数是如何被解析并使用的呢?

方法很简单,Linux编译的时候将具有不同标签的内核参数的处理函数指针按标签编译存放在.init段的taglist表中,实现如下:

static int __init parse_tag_cmdline(const struct tag *tag)

{

strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);

return 0;

}

__tagtable(ATAG_CMDLINE, parse_tag_cmdline);

其中__tagtable被定义为:

#define __tag __used __attribute__((__section__(".taglist.init")))

#define __tagtable(tag, fn) /

static struct tagtable __tagtable_##fn __tag = { tag, fn }

下图为从某次编译生成的System.map中截取的片段:

 

图1-6:编译后tag表存放的位置

setup_arch() 调用parse_tags()解析参数,方法是轮询内核参数中的标签和上表中的标签,如果相同则调用上表中的fn处理对应的内核参数。

在ICE中用到的处理有__tagtable_parse_tag_cmdline、__tagtable_parse_tag_initrd2。先来看__tagtable_parse_tag_cmdline,它的处理函数指针指向下面这个函数:

static int __init parse_tag_cmdline(const struct tag *tag)

{

       strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);

       return 0;

}

可以看出是用内核参数中的cmdline元素将默认命令行参数直接替换掉。也就是说从这一步开始default_command_line = "mem=464M console=ttyMSM1,115200n8 androidboot.hardware=qcom"(该内容来自于boardConfig.mk),而如果没有这一步,default_command_line应该为msm7627_ice_defconfig文件中的CONFIG_CMDLINE值,即"init=/sbin/init root=/dev/ram rw initrd=0x11000000,16M console=ttyDCC0 mem=88M"。

再来看__tagtable_parse_tag_initrd2,它的处理函数指针指向parse_tag_initrd2函数,如下:

static int __init parse_tag_initrd2(const struct tag *tag)

{

       phys_initrd_start = tag->u.initrd.start;

       phys_initrd_size = tag->u.initrd.size;

       return 0;

}

此函数只是将initrd(ICE中为ramdisk)的起始地址及长度赋给全局变量,以便随后使用。

1.2.1.2 解析命令行参数

完成这一使命的函数是parse_cmdline()。它的入参就是上面刚提到的default_command_line,实际上已经是tag->u.cmdline.cmdline内容了。该函数的实现原理类似于上面的parse_tags(),同样是针对"mem=464M console=ttyMSM1,115200n8 androidboot.hardware=qcom"中的每个参数分别注册了一个处理函数,结构如下:

struct early_params {

       const char *arg;

       void (*fn)(char **p);

};

arg元素内容就是"mem="或" console ="等等,而fn要处理的是等号后面空格前面的字符串,并将相应值送给系统全局变量。如"mem="对应的处理函数early_mem(),它要做的就是将464M转换成整型值,并将这块内存添加进meminfo中。下图是某次编译后从System.map中取出的命令行参数处理函数表:

 

图1-7:命令行参数处理函数表

至此,我们已获取到传给Linux的内存大小,起始物理地址,存放于meminfo结构中。获取这些信息后,内核就可以对内存进行分页及其他管理了。先来看看内存的现状。

引导程序首先会将initrd/ramdisk拷贝到initrd/ramdisk在内存中的起始地址处0x14008000。接着内核解压程序又会把内核的text/data/bss各段解压到内存中对应指定的地址处。因此内存现状如下:

 

 

图1-8:内存分页前的状态

内核解压完毕后,控制权转到内核代码,为了能让处理器使用线性地址,这儿的代码将内存前4M的空间(内核代码所在的空间)以段方式映射到页全局目录(PGD),然后打开MMU。对应代码在arch/arm/kernel/head.S中的ENTRY(stext)处,这也是内核的入口,在内存中放在stext段起始处。

1.2.1.3 分页初始化

完成这一操作的函数是paging_init()。流程如下:

 

图1-9:paging_init流程图

 

内存初始化及映射

完成此功能的主要函数是bootmem_init()。要想弄清这块代码,得先了解Linux在ARM上的分页方案。下面这个图是从代码arch/arm/include/asm/pgtable.h中拷贝的。

 

图1-10:Linux在ARM上的二级页表

ARM硬件支持二级页表,第一级为页全局目录,可容纳4096个32位目录项,第二级为页表项,只可容纳256个32位页表项。页表项中不支持"accessed"和"dirty"位标志,这两位是Linux常用位标志。

Linux采用三级页表结构,期盼一个页表占用一页,且至少有一个"dirty"位标志。

为了满足Linux的需求,ARM体系做了如上的映射。页全局目录包含2048项,每项包含两个32位的页中间目录项(pmd),每个pmd指向一个含有256个ARM页表项的页表。每个ARM页表项在同一页表中偏移2048的地方备份一个Linux页表项,用以模拟"accessed"和"dirty"位标志,详情请参考pgtable.h中的注释。

内存初始化及映射要做的就是将内存物理空间映射到内核空间,即设置pgd中对应的目录项。

接下来,在内核BSS之后页对齐的地址处放置内存“页-位”映射图,一位表示对应的那一页是否被使用。然后,用此原则将内存中已经使用的页对应的位全部置位,以免之后再被别人申请使用。自此之后,内核便可使用引导级内存申请函数来申请内存了,如:alloc_bootmem(),alloc_bootmem_low()。这些函数只能在引导过程中使用,内核初始化完成后不允许在使用这些函数。

bootmem_init()函数执行后受影响的内存如下图中粉红色的地方:

 

图1-11:各步操作中受影响的内存

 

设备映射

由devicemaps_init()函数来完成这一任务。包括:一、申请一页内存页放置中断向量表,并通过修改页表将此页映射到高端地址0xffff0000处;二、分配PMEM。代码比较简单,这儿就不多描述了。上图桔黄色的地方为受影响的内存。

 

设定顶级页中间目录项(PMD)

ICE中将高端向量线性地址对应的PMD作为顶级PMD,源码如下:

top_pmd = pmd_off_k(0xffff0000);

 

获取“0页”

暂时还没弄清zero page是作何用处,该页位于图1-11的蓝色区域。

1.2.1.4 申请标准资源

该功能由request_standard_resources()函数实现。先不讲申请标准资源的意义(暂未阅读使用该资源的相关代码)。主要介绍该函数的实现过程。内核中管理了两个资源链表:内存(iomem_resource)和端口ioport_resource。这儿要做的就是将相应资源插入到这两个表中。具体过程是,先把物理内存挂到iomem_resource 子节点下,记为"System RAM"。然后再把内核文本段及数据段挂到"System RAM"子节点下,分别记为"Kernel text"和"Kernel data",这两个节点互为兄弟。

节点插入的原则是:

1. 子节点的内存空间在父节点范围内;

2. 兄弟节点内存空间不可重叠,且按地址从小到大的顺序。

如果有显存,要把它挂到iomem_resource子节点下。

ICE没有显存,也没有端口资源要申请。

1.2.2 mem_init()

mem_init()标示出mem_map中的空闲区域,并告诉我们还有多少内存可以使用。本函数执行完毕后,引导内存分配函数(如:alloc_bootmem())的历史使命也就完成了,取而代之的是伙伴系统及slab分配器。

下面分析一下mem_init()函数。先来看一下流程图:

 

图1-12:mem_init流程图

根据此流程来分析代码,如下:

该函数531行以下只是内存初始化完成后的一些打印信息,不需要解释,重点在516到524行。这是一个以节点(请参考2.1节)号为变量的for循环。520行free_unused_memmap_node()函数的目的是将同一个节点内不同bank间的间隙(代码中称为hole)所分配的页描述符(一个页描述符占用32字节,因此,如果内存很大,页描述符占用空间较大,ice中为3.625M)占用的内存空间释放掉,以节省空间。对于ICE,只有一个节点,该节点内只有一个bank,故而没有bank间隙。522行是一个条件检查,即:如果该节点的内存页数(包括hole)不为0,则执行523行。第523行是mem_init()函数的核心,下面重点分析free_all_bootmem_node(),先记下,该函数入参是节点描述符:

unsigned long __init free_all_bootmem_node(pg_data_t *pgdat)

{

       register_page_bootmem_info_node(pgdat);

       return free_all_bootmem_core(pgdat->bdata);

}

对于ICE,不支持内存热插拔,故而register_page_bootmem_info_node()函数是空函数。因此继续看free_all_bootmem_core()源码: 

该函数的意义在于将内核引导后的剩余内存及引导时申请但现在已经无用的内存按伙伴系统要求或“每CPU”的缓冲区要求存放起来。

我们直接从173行看起,如果同时满足三个条件(1.起始页框号四字节对齐;2.从页框号start开始的32个页框都未被使用;3.start后真实存在32个页框),则整批(32页)将页放到伙伴系统中,否则将未使用的页一页一页的放到“每CPU”的缓冲区中。由此,我们可以看到,如果起始页框四字节不对齐,伙伴系统算法就废了——里面没有内存。伙伴系统若废了,整个内存管理将不堪重负,因为申请页的时候只能一次一页,这是“每CPU”分配器的特点。

194到199行,运行到这儿,引导内存分配器(alloc_bootmem()等)的历史使命即将结束,收回其“页-位”图占用的空间,ICE上为4页,将其放入到“每CPU”的缓冲区。

下面继续分析__free_pages_bootmem(),它将告诉我们如何将放置页框以及放到哪里。

在上个函数free_all_bootmem_core()分析中,我们知道只有三大条件同时满足时,order为5,否则为0。本函数,如果order为0则调用__free_page()->free_hot_page()->free_hot_cold_page()将单页放入“每CPU”的缓冲区。相反,如果order为5,则调用__free_pages()->__free_pages_ok()->free_one_page()->__free_one_page()将一批页放到伙伴系统中。这儿是个难点,必须先弄懂伙伴系统算法才能明白,请参考2.4节。继续分析__free_one_page()。

第463行获取页框所在的页框块(大小由伙伴系统中的MAX_ORDER决定,即2(MAX_ORDER-1)个页框组成一个页框块,ICE的页框块中有1024个页框,也就是1个页框块覆盖4M内存空间)的第一个页框的迁徙类型(migrate type,暂且这么叫着,我也没理解),从前面的初始化函数bootmem_init()->…->setup_usemap()、module_init()->init_per_zone_pages_min()->…->setup_zone_migrate_reserve()可知第一个可用的页框块迁徙类型为MIGRATE_RESERVE,其它可用的页框块迁徙类型为MIGRATE_MOVABLE。

迁徙类型是由管理区(zone)的pageblock_flags字段计算出来的。pageblock_flags是一个位图(ICE 464M内存,占用44字节),每个页框块的flag占用3个bit,其中存放了迁徙类型的序号,比如MIGRATE_MOVABLE在ICE中为2,则该块的flag的中间一个bit为1,其它为0。

476行读取当前空闲的页框数。

477行将管理区页框统计字段的NR_FREE_PAGES项(即zone->vm_stat[NR_FREE_PAGES])中的counter值增加32,表示有32个空闲页增加进来。

478到494行,这段代码是个难点。我们知道父函数传下来的order为5,定然小于10(MAX_ORDER-1)。这个循环的目的就是从order 5开始合并前后伙伴块,形成order 6,以此类推直至order 10。

495到498行,只是按要求将上面组好的伙伴块插入到对应的链表中。

 

至此,内存初始化完毕,管理方式由简单的引导内存管理转变成“buddy-slab”管理。事实上,Linux真正能够管理的内存也就是执行到mem_init()时剩余的内存再加上之后要释放的initrd及init段占用的内存。来看一下ICE平台Linux可管理的内存总量,cat /proc/meminfo执行结果如下:

 

图1-13:meminfo

从上图可以看出,Linux可管理的内存是390992kB,而图1-1中亮绿色的块为初始化后剩余的内存,累加后为390672kB。相差320kB,就是之后要释放的180kB的initrd空间(装载rootfs的populate_rootfs()函数里,在装载rootfs之后释放)和140kB的init段空间(系统初始化即将结束时释放。事实上释放它就是最后一个操作,调用流程如下:start_kernel ()->rest_init()->kernel_init()->init_post()->free_initmem())。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值