内存管理子系统是linux内核最核心最重要的一部分,内核的其他部分都需要在内存管理子系统的基础上运行。而对其初始化是了解整个内存管理子系统的基础。对相关数据结构的初始化是从全局启动例程start_kernel开始的。本文详细描述了从bootloader跳转到linux内核内存管理子系统初始化期间所做的操作,从而来加深对内存管理子系统知识的理解和掌握。
内核的入口是stext,这是在arch/arm/kernel/vmlinux.lds.S中指定的。而符号stext是在arch/arm/kernel/head.S中定义的。整个初始化分为两个阶段,首先是在head.S中用汇编代码执行一些平台相关的初始化,完成后跳转到start_kernel函数用C语言代码执行剩余的通用初始化部分。整个初始化流程图如下图所示:
一、 启动条件
通常从系统上电到运行到linux kenel这部分的任务是由boot loader来完成。Boot loader在跳转到kernel之前要完成一些限制条件:
1、CPU必须处于SVC(supervisor)模式,并且IRQ和FIQ中断必须是禁止的;
2、MMU(内存管理单元)必须是关闭的, 此时虚拟地址对应物理地址;
3、数据cache(Data cache)必须是关闭的;
4、指令cache(Instruction cache)没有强制要求;
5、CPU通用寄存器0(r0)必须是0;
6、CPU通用寄存器1(r1)必须是ARM Linux machine type;
7、CPU通用寄存器2(r2)必须是kernel parameter list的物理地址;
二、 汇编代码初始化部分
汇编代码部分主要完成的工作如下所示,下文只是概述head.S中各个函数完成的主要
功能,具体的技术细节,本文不再赘述。
? 确定processor type
? 确定machine type
? 创建页表
? 调用平台特定的__cpu_flush函数
? 开启mmu
? 切换数据
? 最终跳转到start_kernel
三、 C语言代码初始化部分
C语言代码初始化部分主要负责建立结点和内存域的数据结构、初始化页表、初始化用
于内存管理的伙伴系统。在启动过程中,尽管内存管理模块尚未初始化完成,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段分配内存。所有涉及的实现都是在start_kernel函数中实现的,其中涉及到内存初始化的函数如下:
1、 build_all_zonelist用于结点和内存域的初始化。
在linux系统中,内存划分结点,每个结点关联到系统中的一个处理器。各个结点又划分内存域,主要包括ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。大部分系统只有一个内存结点,下文只针对此类系统。此函数用于初始化内存的结点和内存域。内核在mm/page/page_alloc.c中定义了一个pglist_data的内存结点实例(contig_page_data)用于管理所有系统的内存。
build_all_zonelist用于初始化结点和内存域。该函数首先调用__build_all_zonelists,此函数遍历系统的每个内存结点,针对每个内存结点调用build_zonelists(),该函数的任务是在当前处理的结点和系统中其他结点的内存域之间建立一种等级次序。接下来,依据这种次序分配内存,如果在期望的结点内存域中,没有空闲内存,就去查找相邻结点的内存域。内核定义了内存的一个层次结构关系,首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存。
高端内存最廉价,因为内核没有任何部分依赖于从该内存域分配的内存,如果高端内存用尽,对内核没有副作用,所以优先分配高端内存。
普通内存域的情况有所不同,许多内核数据结构必须保存在该内存域,而不能放置到高端内存域,因此如果普通内存域用尽,那么内核会面临内存紧张的情况。
DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。
举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。
该函数接下来计算所有剩余的内存页,存放到全局变量vm_total_pages中。接下来如果空闲内存页太少,则关闭页的可迁移性,即page_group_by_mobility_disabled置位。页的可迁移性是内核为了避免内存碎片而在linux2.6.24中加入的新特性。该特性把内存页分为不可移动内存页、可回收页和可移动页三种类型来避免内存碎片。
2、 mem_init用于停用bootmem分配器并迁移到伙伴系统。
在系统初始化进行到伙伴系统分配器能够承担内存管理的责任后,必须停用bootmem分
配器。该函数遍历所有的内存域结点,对每个结点分别调用free_all_bootmem_node函数,停用bootmem分配器。该函数调用free_all_bootmem_core,首先扫描bootmem分配器位图,释放每个未用的页。到伙伴系统的接口是__free_pages_bootmem函数,该函数对每个空闲内存页调用,该函数内部依赖于标准函数__free_page。它使得这些页并入到伙伴系统的数据结构,在其中作为空闲页,用于分配管理。在页位图已经完全扫描后,它占据的内存空间也必须释放掉,此后,只有伙伴系统可用于内存分配。
3、 初始化slab分配器。
kmem_cache_init初始化内核用于小块内存区的分配器(slab分配器)。他在内核初始化阶段、伙伴系统启用之后调用。
kmem_cache_init创建系统中的第一个slab缓存,以便为kmem_cache的实例提供内存,为此,内核使用一个在编译时创建的静态数据(cache_cache)。该函数接下来初始化一般性的缓存,用作kmalloc内存的来源。为此,针对所需的各个缓存长度,分别调用kmem_cache_create函数。
4、 初始化页表,初始化bootmem分配器。
setup_arch是特定于体系结构的,用于初始化页表,初始化bootmem分配器。该函数的执行流程图如下:
4.1、setup_processor和setup_machine用来确定处理器类型和机器类型。
4.2、parse_targs用来解析从uboot传递过来的tag值。在uboot的do_bootm_linux函数中,会创建传递给内核的各个tag。tag的地址是由内核和uboot约定的,在uboot中是在函数board_init中将该约定好的地址保存在gd->bd->bi_boot_params中,如下所示:
gd->bd->bi_boot_params = CFG_BOOT_PARAMS;
在内核中,MACHINE_START定义了struct machine_desc数据结构,各成员函数在linux启动的不同时期被调用。该结构体的boot_params成员存放的就是uboot传递给内核的tag的起始地址。
MACHINE_START(hi3520v100, "hi3520v100")
.phys_io = IO_SPACE_PHYS_START,
.io_pg_offst = (IO_ADDRESS(IO_SPACE_PHYS_START) >> 18) & 0xfffc,
.boot_params = PHYS_OFFSET + 0x100,
.map_io = hisilicon_map_io,
.init_irq = hisilicon_init_irq,
.timer = &hisilicon_timer,
.init_machine = hisilicon_init_machine,
MACHINE_END
在uboot中通过setup_memory_tags来建立内存的tag,把物理内存的起始地址和大小记录在tag里,tag头用ATAG_MEM标识。内核中有一个tagtable的表,把各种表头的标识和解析函数关联起来,关于内存的如下所示:
__tagtable(ATAG_MEM, parse_tag_mem32);
当parse_targs找到以ATAG_MEM标识的tag后,调用parse_tag_mem32,把在uboot中填充到tag里的内存起始地址和大小填充到全局变量meminfo数组中。
4.3、parse_cmdline解析命令行参数。
此函数解析由uboot传进来的命令行参数。在内核中,各个命令行参数的头都与相应的解析函数一一对应,关于内存,有如下对应:
__early_param("mem=", early_mem);
在内核中解析命令行中以"mem="开头的命令行,找到此命令行后,调用early_mem函数解析命令行。如果内核需要管理几段不同的内存,可以在uboot的bootarg环境变量中分别指定对应的内存段的起始地址和长度,如下所示:
mem=72M@0xe2000000 mem=128M@0xe8000000
表示内核需要管理两段不连续的内存,第一段起始地址为0xe2000000,大小为72M,另外一段起始地址为0xe8000000,大小为128M,内核通过early_mem函数分别把这两段内存写入到meminfo的两个bank中。
4.4、paging_init函数用来初始化页表项,启用bootmem分配器。
4.4.1、build_mem_type_table用来建立各种类型的页表选项。该函数是为了给mem_types数组中的各种类型的页表参数添加上我们的要求,主要是一级页表,二级页表,访问权限控制等标志位。
4.4.2、prepare_page_table函数清除一级页表中无效的页表,只保留物理内存在虚拟地址空间中的映射。
4.4.3、devicemaps_init函数用来清除VMALLOC_END之后的一级页表,分配vector page,调用mdesc->map_io来映射设备,mdesc->map_io即上文中提到的用MACHINE_START定义的结构体中的成员,即hisilicon_map_io
4.4.4、bootmem_init函数在做一些必要的操作后,调用bootmem_init_node来初始化一级页表、启用bootmem分配器。
Linux内核的段页表项将4GB的地址空间分成4096个1MB的段(section),每个段页表项是一个unsigned long型变量,占用4字节,因此段页表项占用4096*4=16K的内存空间。而全局页表项的大小为2M,如下
#define PGDIR_SHIFT 21
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
而全局页表项的数据结构pgd_t的定义如下
typedef struct { unsigned long pgd[2]; } pgd_t;
所以一个全局页表对应两个段页表,正好符合上述定义。
该函数首先根据meminfo数组中的bank数目,分别调用map_memory_bank来映射各个物理内存区的段页表项。该函数调用create_mapping来执行具体的映射工作。
建立完物理内存的页表映射后,内核会把物理内存的起始地址的页帧号放入start_pfn,物理内存的结束地址的页帧号放入end_pfn。这里要注意如果有多个内存bank时,start_pfn和end_pfn之间可能有空洞,拿上文提到的例子来讲,start_pfn和end_pfn之间是有内存空洞的,具体如下:
mem=72M@0xe2000000 mem=128M@0xe8000000
start_pfn = 0xe2000000 >> PAGE_SHIFT;
end_pfn = (0xe8000000 + 8000000) >> PAGE_SHIFT;
在内核启动期间,由于基于物理内存的伙伴系统尚未初始化完成,但内核仍然需要分配内存以创建各种数据结构,bootmem分配器用于在启动阶段早期分配内存。bootmem分配器是一个最先适配分配器,该分配器使用一个位图来管理内存页,位图比特位的数目与系统中物理内存页的数目相同,比特位为1表示已用页,比特位为0表示空闲页。在需要分配内存时,分配器逐位扫描位图,直至找到一个能提供足够连续内存页的位置,即所谓的最先适配位置。bootmem分配器也必须管理一些数据,内核为系统中的每个结点都提供了一个bootmem_data结构的实例,用于该用途。当然,该结构不能动态分配,只能在编译时分配给内核,在UMA系统上,只有一个bootmem_data_t实例,即contig_bootmem_data。
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;
struct list_head list;
} bootmem_data_t;
node_boot_start保存了系统中物理内存的第一个页帧的编号。
node_low_pfn是系统物理内存最后一页的页帧编号。
node_bootmem_map 是指向bootmem分配器位图所在地址的指针。
last_pos是上一次分配的页帧编号。如果没有请求分配整个页帧,last_offset用作该页内部的偏移量,这使得bootmem分配器可以分配小于一整页的内存区。
last_success指定位图中上一次成功分配内存的位置,新的分配由此开始。
list:所有注册的bootmem分配器保存在一个链表中,表头是全局变量bdata_list。
内核接下来调用bootmem_bootmap_pages来计算bootmem分配器位图的大小(需要按页对齐),然后调用find_bootmap_pfn在物理内存中找到一块大小合适的内存,优先考虑内核的数据段的结尾处。接下来调用init_bootmem_node初始化bootmem分配器,该函数调用init_bootmem_core来填充bootmem_data_t结构中各个成员。
接下来调用free_bootmem_node进而调用free_bootmem_core把整个位图清零,把所有内存页标记为空闲页。然后在位图中分别把已用的内存页标记为1,已用的内存页包括位图占用的内存页、initrd占用的内存页、内核的代码段和数据段占用的内存,到此bootmem分配器正式建立起来了。
最后,free_area_init_node用来填充内存结点的数据结构pglist_data中的各个成员变量,其中zones_size表示物理内存的大小,包含内存空洞,zholes_size是内存空洞的页数。pgdat是内存结点的数据结构,node_start_pfn是物理内存的起始页帧编号。该函数首先调用calculate_node_totalpages用来计算总的物理内存页帧数,包括内存空洞,保存在内存结点数据结构的node_spanned_pages成员中,然后计算去掉内存空洞后的实际物理内存页帧数,保存在node_present_pages成员中。
由于每个内存页都需要有一个struct page的数据结构来管理,所以该函数接下来调用
alloc_node_mem_map来创建所有物理内存页的struct page实例,指向该空间的指针不仅保存在pglist_data结构的node_mem_map中,还保存在全局变量mem_map中。
最后,free_area_init_node函数调用free_area_init_core来初始化pglist_data实例的各个内存域。该函数负责初始化各个zone结构中的成员。在ARM Linux中,没有ZONE_HIGHMEM,ZONE_NORMAL初始化成0,所有可用的物理内存被放置在ZONE_DMA。主要涉及两个函数:zone_pcp_init用来初始化冷热缓存页,init_currently_empty_zone用来初始化伙伴系统的free_area列表,并将属于该内存域的所有page实例都设置成默认值。
zone_pcp_init负责初始化冷热缓存页。struct zone的pageset成员用于实现冷热页分配器。内核说页是热的,意味着页已经加载到了CPU高速缓存,与在内存中的页相比,其数据能够更快的访问。相反,冷页则不在高速缓存中。在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的。pageset是一个数组,其容量与系统能够容纳的CPU数目的最大值相同。
struct zone{
………
Struct per_cpu_pageset pageset[NR_CPUS];
………
}
NR_CPUS是一个可以在编译时配置的宏常数。在单处理器上其值总是1,针对SMP系统编译的内核中,其值可能是2到32之间。数组元素的类型为per_cpu_pageset,定义如下
struct per_cpu_pageset {
struct per_cpu_pages pcp[2]; /* 0: hot. 1: cold */
} ____cacheline_aligned_in_smp;
该结构有两个数组项,第一项管理热页,第二项管理冷页。per_cpu_pageset结构如下:
struct per_cpu_pages {
int count;
int high;
int batch;
struct list_head list;
};
count记录了与该列表相关的页的数目。
high是页数上限的水印值,在需要的情况下清空列表。
batch是每次添加页数的值。
zone_pcp_init函数首先用zone_batchsize计算出批量添加页的大小,保存在batch中,然后遍历系统中所有CPU,同时调用setup_pageset填充每个per_cpu_pageset实例的常量。根据计算得到的batch大约相当于内存域中页数的0.25‰。
init_currently_empty_zone用来初始化与伙伴系统相关的free_aera列表。该函数首先调用memmap_init_zone初始化上文分配好的保存在全局变量mem_map中的物理内存的struct page实例。然后调用zone_init_free_lists初始化free_aera空闲列表。空闲页的数目free_area.nr_free当前仍然规定为0,直至停用bootmem分配器、普通的伙伴系统分配器生效时,才会设置正确的数值。
整个内核地址空间的划分请参见下图:
图中PAGE_OFFSET=0xc0000000,TEXT_OFFSET=0x00008000在arch/arm/makefile中指定,swapper_pg_dir=0x00004000在head.S中指定。
地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量0xc0000000开始,即3GiB,所以每个虚拟地址x都对应于物理地址x-0xc0000000,因此这是一个简单的线性偏移。
直接映射区:从PAGE_OFFSET(即0xc0000000)开始到high_memory的地址空间,在内核调用bootmem_init函数中,把high_memory的值设置为实际物理内存的结束地址对应的虚拟地址,最大不超过896M。当实际物理内存大于896M时,超出的部分映射为高端内存区。物理内存的起始地址的16K未用,从swapper_pg_dir到0xc0008000的16K存放段地址的页表项内容,前文已经描述过了。从0xc0008000开始存放内核的代码段、初始化的数据段和未初始化的数据段。紧接着内核的代码段和数据段的是bootmem分配器的位图区域,上文中页提到过。
VMALLOC区:虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理内存。但在已经运行了很长时间的系统上,在内核需要物理内存时,可能出现可用内存不连续的情况。此类情况,主要出现在动态加载模块时。vmalloc区域在何处结束取决于是否启用了高端内存支持。如果没有启用,那么就不需要持久映射区,因为整个物理内存都是可以直接映射的。因此根据配置的不同,该区域结束于持久内核映射或固定映射区域的起始处,中间总会留下两页,作为vmalloc区与这两个区域之间的保护措施。
持久映射区:用于将高端内存域中的非持久页映射到内核。
固定映射区:是与物理地址空间中的固定页关联的虚拟地址空间页,但具体关联的页帧可以自由选择。它通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义。
在直接映射区和vmalloc区域之间有一个8M的缺口,这个缺口可用作针对任何内核故障的保护措施。如果访问越界地址,则访问失败并生成一个异常,报告该错误。如果vmalloc区域紧接着直接映射,那么访问将成功而不会注意到错误。
到此,整个linux的内存管理子系统初始化完成了,具体的细节部分请参考代码阅读体会。
from:http://sunjiangang.blog.chinaunix.net/uid-9543173-id-3568385.html