start_kernel之前的汇编代码建立了内核临时页表,完成了内核区域的静态线性映射,保证内核可以在舒适的虚拟地址空间(运行地址和链接地址一致)运行。进入start_kernel之后就要准备建立完整的页表映射,这部分工作是在paging_init中完成。
不过在建立完整页表映射之前还需要进行一些准备工作,本文来分析下。
为了简化整个代码流程,便于分析,我的设备内核配置为不使用高端内存,不配置CONFIG_HIGHMEM。bootargs中传给内核的mem=256.
内核版本号:3.4.55
paging_init是在start_kernel的setup_arch中调用,这里按照先后顺序对setup_arch跟页表相关的各个函数功能做个介绍,重点函数paging_init进行详细分析。
setup_processor:调用lookup_processor_type,跟head.S中__lookup_processor_type一样,获取存储在.proc.info.init段中与cpu id一致的proc_info_list结构体,该结构体中存储着处理器的一些特性。打印出cpu的一些相关信息(如版本号 cache属性等)。
setup_machine_tags:对uboot传递来的tags进行解析,获取mem cmdline等信息,具体过程可以参考我分析kernel传参的博文,链接:http://blog.csdn.net/skyflying2012/article/details/35787971
parse_early_param:对boot_command_line进行早期的解析,具体解析原理可以参考我分析kernel参数解析的博文,链接:http://blog.csdn.net/skyflying2012/article/details/41142801
与页表相关的是对mem的解析,相应的解析函数如下。
{
static int usermem __initdata = 0;
unsigned long size;
phys_addr_t start;
char *endp;
/*
* If the user specifies memory size, we
* blow away any automatically generated
* size.
*/
if (usermem == 0) {
usermem = 1;
meminfo.nr_banks = 0;
}
start = PHYS_OFFSET;
size = memparse(p, &endp);
if (*endp == '@')
start = memparse(endp + 1, NULL);
arm_add_memory(start, size);
return 0;
}
early_param("mem", early_mem);
我的设备内存起始物理地址是0x80000000,即PHYS_OFFSET = 0x80000000,cmdline中mem=256m,
early_mem最终调用arm_add_memory将0x80000000起始的256MB内存添加到meminfo的membank数组中。meminfo中记录着系统有多少块连续内存,用membank数组记录,这里我们仅使用1个membank表示0x80000000起始的256MB内存空间。
sanity_check_meminfo:对meminfo中所有的membank进行范围检查,不能覆盖最小的vmalloc区域,将lowmem_limit设置为最高membank的顶端,我的设备只有一个membank,因此lowmem_limit为0x90000000,这是内存物理地址。最后将high_memory设置为lowmem_limit的虚拟地址,lowmem是线性映射到0xc0000000,因此high_memory=0xd0000000。
static void * __initdata vmalloc_min = (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);
我的设备VMALLOC_END=0xfc000000,这是vmalloc区域的上限,VMALLOC_OFFSET=0x800000,是vmalloc区域与lowmem之间8MB的隔离带。这样计算vmalloc_min是0xec800000,这是一个最小(240MB)的vmalloc区域.
arm_memblock_init:全局结构体memblock用来记录内存中可用和保留的区域,memblock.memory代表可用区域,memblock.reserved代表保留区域。arm_memblock_init中首先将meminfo中记录的membank添加到可用区域中。将kernel的代码段 数据段以及ramdisk区域都添加到保留区域,再将一级页目录的16KB区域添加到保留区域。
最后如果在板级结构体machine_desc定义了reserve函数,则会调用该函数完成板级相关的一些内存区域的保留。
为了方便调试,可以在cmdline中加入memblock=debug,会将memblock中可用和保留的区域全部打印出来。
我的设备是将0x80000000起始的256MB区域添加到可用区域memblock.memory中。
以上完成了对meminfo memblock的初始化,需要注意,其中存储的都是物理地址。
接下来调用paging_init,其中与页表建立相关的函数如下。在arch/arm/mm/mmu.c中
build_mem_type_table();
prepare_page_table();
map_lowmem();
devicemaps_init(mdesc);
kmap_init();
top_pmd = pmd_off_k(0xffff0000);
其中map_lowmem devicemaps_init kmap_init完成了完整页表的建立,下篇文章再详细分析,这里先不说了。
build_mem_type_table:完成对kernel中mem_types数组的修复和补充。mem_types数组是kernel记录当前系统映射不同地址空间类型(普通内存 设备内存 IO空间等)的页表属性,其中页表属性还包括section-mapping的属性prot_sect,以及page-mapping的一级页目录属性prot_l1,二级页表属性prot_pte.
不过要注意的是,mem_types中prot_sect prot_l1都与硬件属性一一对应。但二级页表属性值prot_pte是linux定义的软件页表属性,而不是硬件页表属性,本文最后说明linux与arm适配问题会再详说。
在kernel下页表建立函数create_mapping中会根据选定的mem_types成员来设置当前映射所需的二级软件页表属性,最后会调用处理器相关的set_pte_ext,首先设置软件页表项,然后在根据软件页表项值配置硬件页表项。(linux在建立完整第二级页表时使2个硬件页表相连,并且在其后添加2个软件页表,保证这一页空间被充分利用,文末再细说)
build_mem_type_table中跟处理器特性对mem_types数组成员(各个类型的地址空间)的各个页表属性进行修改补充。软件开发人员一般不需要修改,除非处理器核做过修改。
接下来我们来分析下本文的关键函数prepare_page_table。在arch/arm/mm/mmu.c中
static inline void prepare_page_table(void)
{
unsigned long addr;
phys_addr_t end;
/*
* Clear out all the mappings below the kernel image.
*/
for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
#ifdef CONFIG_XIP_KERNEL
/* The XIP kernel is mapped in the module area -- skip over it */
addr = ((unsigned long)_etext + PMD_SIZE - 1) & PMD_MASK;
#endif
for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
/*
* Find the end of the first block of lowmem.
*/
end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
if (end >= lowmem_limit)
end = lowmem_limit;
/*
* Clear out all the kernel space mappings, except for the first
* memory bank, up to the vmalloc region.
*/
for (addr = __phys_to_virt(end);
addr < VMALLOC_START; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
}
该函数是在建立完整页表前对一级页目录(swapper_pg_dir)进行清空,便于建立页表时对空页目录项进行判断然后分配。不过并不是所有页目录项都清空,我们知道虚拟地址的高12bit是16K一级页目录的索引ndex,prepare_page_table中对于第一块membank(lowmem部分)虚拟地址空间映射的页目录项跳过,不进行清空操作。
以我的设备为例,第一块也是唯一一块membank是0x80000000起始的256MB。lowmem_limit也是0x90000000。这块空间是线性映射,由0x80000000映射到0xc0000000。因此在4KB个页目录项中,第0xc00到第0xd00这256个页目录项,prepare_page_table不进行清空。
根据上一篇内核临时页表建立的分析,head.S中建立的临时页表,主要完成了3个地址空间的映射。
(1)turn_mmu_on所在1M空间的平映射
(2)kernel image的线性映射
(3)atags所在1M空间的线性映射
我计算过我的设备中,临时页表中kernel image线性映射了大约12MB空间,也就是第0xc00之后的12个页目录项。prepare_page_table中跳过0xc00开始的256个,临时页表建立的kernel image线性映射没有破坏,kernel的运行环境得到了保证。
不过为了保证mmu使能后的平滑跳转而建立的1MB平映射(vaddr从0x80000000到0x8100000),在prepare_page_table中就被清空了。
prepare_page_table整个流程如下。
1 对虚拟地址0到MODULES_VADDR(0xc0000000以下8MB或16MB的地址)的一级页目录项进行清空
2 对MODULES_VADDR到PAGE_OFFSET(0xc0000000)的一级页目录项进行清空
3 对lowmem顶端到VMALLOC_START的一级页目录项进行清空
我们来分析下具体的清空过程,以流程1为例。
从0到MODULES_VADDR,对每2MB空间,调用pmd_clear(pmd_off_k(addr))。
pmd_off_k定义如下。在arch/arm/mm/mm.h中
static inline pmd_t *pmd_off_k(unsigned long virt)
{
return pmd_offset(pud_offset(pgd_offset_k(virt), virt), virt);
}
arm-linux仅使用2级页表,pud_offset
pmd_offset不做任何操作,直接返回参数,直接来看pgd_offset_k,如下。
extern pgd_t swapper_pg_dir[PTRS_PER_PGD];
/* to find an entry in a page-table-directory */
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
PGDIR_SHIFT定义为21,pgd_offset_k获取到了一级页目录项的虚拟地址,但是需要注意的是,该地址是8字节对齐。再来看pmd_clear,如下。在./arch/arm/include/asm/pgtable-2level.h
#define pmd_clear(pmdp) \
do { \
pmdp[0] = __pmd(0); \
pmdp[1] = __pmd(0); \
clean_pmd_entry(pmdp); \
} while (0)
将连续的2个页目录项清空,并且清空相应的TLB缓存。
综上,prepare_page_table清空具体流程是,对指定地址空间以2MB为一次操作单元,每次操作连续清空对应的2个一级页目录。
这就奇怪了,按照之前我们介绍的arm硬件页表机制,一级页目录共4096个,每个映射1MB空间,不应该一次操作1MB吗,为啥要以2MB为单位?
理解内核的这种做法,文章最后,就要来说明下linux页表机制与arm页表机制的适配问题了,主要有2点。
1 页表深度的适配
2 软件页表和硬件页表配合
1 页表深度的适配
arm硬件页表机制之前详细分析过,这里不再细说了,可以看我这系列的第一篇文章。
linux内核为了适配各种各样的处理器MMU,特别是64位处理器。2.6.11之前使用三级页表,之后开始使用四级页表。
linux内核定义的标准是这样,最高级pgd为页目录表,它找到每个进程mm_struct结构的pgd成员,用它定位到下一级pud,第二级pud定位到下一级pud,第三级pud定位到pte,pte是页表,它就能定位到哪个页了。最后虚拟地址的最后12位定位的是该页的偏移量。大体流程如下:
virt addr —> pgd —> pud —> pmd —> pte —> phy addr
首先需要理解,内存多级页表,是给mmu来解析的,因此针对不同的处理器mmu页表机制,linux需要在配置多级页表时进行调整。
为了能够符合arm mmu的页表解析机制(2级页表),arm-linux实现时砍掉了中间的pud pmd。如上面分析pmd_off_k时pud_offset pmd_offset中不做任何操作,直接返回参数。大体流程如下。
virt addr —> pgd —> pte —> phy addr
这样就保证了linux配置页表时虽然表面上看是4级页表,但是内存中建立却是一个2级页表。arm-mmu可以正确的解析页表,进行地址翻译。
2 软件页表和硬件页表配合
arm硬件页表机制中,每个一级页目录项对应的二级页表空间都是独立分配的,虽然连续2个一级页目录项所映射2MB地址空间是连续的虚拟地址空间,但是用来存储二级页表的空间之间是没有什么关系的。
不过arm-linux内核为了实现高效的内存管理,做了一个很巧妙的安排。
我们知道1个一级页目录项对应的二级页表是256x4 = 1024字节。arm-linux将2个连续的一级页目录项对应的2个二级页表分配在一起。而且还在这2个二级硬件页表之下在建立2个对应的二级软件页表,一共是4KB,正好占用1页空间。如下。
* pgd pte
* | |
* +--------+
* | | +------------+ +0
* +- - - - + | Linux pt 0 |
* | | +------------+ +1024
* +--------+ +0 | Linux pt 1 |
* | |-----> +------------+ +2048
* +- - - - + +4 | h/w pt 0 |
* | |-----> +------------+ +3072
* +--------+ +8 | h/w pt 1 |
* | | +------------+ +4096
对于这样的安排在./arch/arm/include/asm/pgtable-2level.h有详细的英文解释。
我们告诉linux内核arm一级页目录项是8 bytes,一共是2048个。配置页目录项时,一次分配4KB空间,高2KB空间每1KB的物理基地址依次写入8 bytes一级页目录项的2个word中。
我的理解,arm-linux如此安排的原因有二。
(1)减少空间浪费,一个页目录项仅对应1024字节的页表,这样页表初始化时,按页分配的空间仅能使用1KB,其余3KB空间浪费,如上安排,可以完全利用这4KB页。
(2)linux软件二级页表属性位定义与arm硬件二级页表不一致(软件二级页表位定义在arch/arm/include/asm/pgtable-2level.h,硬件二级页表位定义在arch/arm/include/asm/pgtable-2level-hwdef.h,其中有详细说明),有些属性arm硬件页表没有,如dirty,accessed,young等,因此使用软件页表进行模拟兼容。arm-mmu读取硬件2级页表进行地址翻译,而第二级的软件页表仅仅留给linux来配置和读取。最终配置二级页表的set_pte_ext也会设置完软件页表后,在根据软件页表属性值来配置硬件页表。
所以我们在arm-linux内核中看到各种相关宏定义都表示,linux看到的arm一级页目录项有2048个,每个页目录项8
bytes,二级页表项是512个。不过arm-mmu的硬件机制还是4096个一级页目录项,每个页表有256个页表项。
两种机制是靠2个相邻页目录项的页表存储空间连续来平滑过渡的。
到这里,完整页表建立的准备工作做完了,主要是建立meminfo
memblock,并清空页目录项,但是保留了lowmem所在的线性映射页目录项,保障kernel正常运行。下篇文章就来分析下具体页表建立的过程!