注:本文中提及的ICE为一个使用Android2.1(Linux2.6.29)的项目。
在进入复杂的内存初始化过程前,我们先看看初始化后的内存分配及映射图,以便有一个整体的印象。以此印象为轴,将各个小的过程、细节串起来达到最终对内存认识的融会贯通。图1-1描绘的是ICE平台划给Linux的464M内存初始化后的结果:
图1-1:ICE初始化后的内存
内核初始化时如何知道系统中有多大内存可用,用什么设备作为控制台,initrd在什么地方?大家都知道在平台的defaultConfig中会配置这些参数。但在ICE里,你会发现系统真正用到的并不是defaultConfig配置的,何故?下面将从代码中找到答案。
要想弄清这些问题,需要从boot.img开始,因为这里包含了内核初始化过程的所有信息。从android/bootable/bootloader/lk/app/aboot/bootimg.h中可以得知boot.img的结构如下:
图1-2:boot.img组成结构
上图中kernel、ramdisk、second stage都比较面熟,但boot header是什么?阅读该目录下的代码可知它就是我们要找的内核初始化参数表,对应于boot_img_hdr结构,各成员如下:
unsigned char magic[BOOT_MAGIC_SIZE]; /* 魔术字,值为"ANDROID!" */
unsigned kernel_size; /* size in bytes */
unsigned kernel_addr; /* physical load addr */
unsigned ramdisk_size; /* size in bytes */
unsigned ramdisk_addr; /* physical load addr */
unsigned second_size; /* size in bytes */
unsigned second_addr; /* physical load addr */
unsigned tags_addr; /* physical addr for kernel tags */
unsigned page_size; /* flash page size we assume */
unsigned unused[2]; /* future expansion: should be 0 */
unsigned char name[BOOT_NAME_SIZE]; /* asciiz product name */
unsigned char cmdline[BOOT_ARGS_SIZE];
unsigned id[8]; /* timestamp / checksum / sha1 / etc */
下图为某次编译的ICE的boot.img起始部分:
图1-3:boot.img的boot header
由此可知boot_img_hdr中各成员值为:
magic[BOOT_MAGIC_SIZE] = "ANDROID!"
kernel_size = 0x00251E28
kernel_addr = 0x13010000
ramdisk_size = 0x0002C021
ramdisk_addr = 0x14008000
second_size = 0x00000000
second_addr = 0x13F08000
tags_addr = 0x13008100
page_size = 0x00000800
unused[2] = 0
name[BOOT_NAME_SIZE] = 无名字
cmdline[BOOT_ARGS_SIZE] = "mem=464M console=ttyMSM1,115200n8 androidboot.hardware=qcom"
id[8] = 0
内核参数找到了,但内核又如何利用这些参数进行初始化呢?这要弄清两件事情:一是内核引导时将这些参数放到内存中的什么位置;二是如何到这个位置取参数又如何一一初始化。android/bootable/bootloader/lk/app/aboot/aboot.c中的boot_linux()函数很好的回答了第一个问题。代码如下:
void boot_linux(void *kernel, unsigned *tags,
const char *cmdline, unsigned machtype,
void *ramdisk, unsigned ramdisk_size)
{
unsigned *ptr = tags;
void (*entry)(unsigned,unsigned,unsigned*) = kernel;
struct ptable *ptable;
int cmdline_len = 0;
int have_cmdline = 0;
/* CORE */
*ptr++ = 2;
*ptr++ = 0x54410001;
if (ramdisk_size) {
*ptr++ = 4;
*ptr++ = 0x54420005;
*ptr++ = (unsigned)ramdisk;
*ptr++ = ramdisk_size;
}
ptr = target_atag_mem(ptr);
……
if (cmdline && cmdline[0]) {
cmdline_len = strlen(cmdline);
have_cmdline = 1;
}
……
if (cmdline_len > 0) {
const char *src;
char *dst;
unsigned n;
/* include terminating 0 and round up to a word multiple */
n = (cmdline_len + 4) & (~3);
*ptr++ = (n / 4) + 2;
*ptr++ = 0x54410009;
dst = (char *)ptr;
if (have_cmdline) {
src = cmdline;
while ((*dst++ = *src++));
}
……
ptr += (n / 4);
}
/* END */
*ptr++ = 0;
*ptr++ = 0;
……
entry(0, machtype, tags);
}
入参tags值为TAGS_ADDR,在android/bootable/bootloader/lk/target/msm7627_ffa/rules.mk中被定义为0x13000100。该地址将会作为参数传给内核入口函数entry(0, machtype, tags)。上面这个函数主要功能就是将boot_img_hdr中的内容一项项填到tag表中。下面列出了tag的结构体,是一TLV(Tag-Length-Value)结构。hdr中定义了32位无符号的size(Length)和tag。联合中列举了不同参数的值(Value)的结构。
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
第一个问题我们已经明白,引导函数会把内核参数按照tag结构的要求放到物理地址0x13000100处,并将该地址传给内核入口函数。我们再把目光转到第二个问题。先从使用内核参数的地方入手,在setup_arch函数中有如下几句:
void __init setup_arch(char **cmdline_p)
{
......
mdesc = setup_machine(machine_arch_type);
......
if (__atags_pointer)
tags = phys_to_virt(__atags_pointer);
else if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params);
......
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags);
save_atags(tags);
parse_tags(tags);
}
......
}
从上面的代码中可以知道内核参数tags为__atags_pointer或者mdesc->boot_params。对于ICE来说mdesc->boot_params在arch/arm/mach-msm/board-msm7x27.c中被赋值为PHYS_OFFSET + 0x100(如图1-4,PHYS_OFFSET在ICE中被定义为0x13000000)。而全局变量__atags_pointer在arch/arm/kernel/head-common.S中被赋值为entry(0, machtype, tags)中的tags。
图1-4:ICE机器描述符
从init/main.c中的start_kernel函数开始。与内存初始化有关的函数如下:
asmlinkage void __init start_kernel(void)
{
......
setup_arch(&command_line);
......
mem_init();
......
}
下面从调用顺序依次分析这些函数。
需要重点分析的函数。该函数主要完成的是解析引导程序传下来的内核参数以及初始化页全局目录及部分页表。流程如下:
图1-5:setup_arch流程图
前面提到内核参数按照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)的起始地址及长度赋给全局变量,以便随后使用。
完成这一使命的函数是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段起始处。
完成这一操作的函数是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的蓝色区域。
该功能由request_standard_resources()函数实现。先不讲申请标准资源的意义(暂未阅读使用该资源的相关代码)。主要介绍该函数的实现过程。内核中管理了两个资源链表:内存(iomem_resource)和端口ioport_resource。这儿要做的就是将相应资源插入到这两个表中。具体过程是,先把物理内存挂到iomem_resource 子节点下,记为"System RAM"。然后再把内核文本段及数据段挂到"System RAM"子节点下,分别记为"Kernel text"和"Kernel data",这两个节点互为兄弟。
节点插入的原则是:
1. 子节点的内存空间在父节点范围内;
2. 兄弟节点内存空间不可重叠,且按地址从小到大的顺序。
如果有显存,要把它挂到iomem_resource子节点下。
ICE没有显存,也没有端口资源要申请。
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())。