linux内核是如何实现分页机制的

来源:互联网 发布:图像阈值分割算法 编辑:程序博客网 时间:2024/04/28 08:54

注意:请使用谷歌浏览器阅读(IE浏览器排版混乱)


【摘要】

 本文主要介绍linux源代码中,是如何实现分页机制的。内存分页管理是arm架构中MMU的重要组成部分,理解它大有裨益。本文重点不在讲解理论知识,旨在通过源码的剖析,带你走进linux内存管理的世界。
【写作原因】
 主要原因:后续介绍cache和缺页异常时都将以本文为根基进行展开。为以后介绍起来方便,专门写一篇文章。
【正文分析】
本文以linux3.18.20,armv7为例介绍。
一. 何时会创建页表?
首先谈一下用户常用的获取内存方式,以下几种常用的内存申请为例:
1) 内核态:kmalloc , kmalloc的内存从何而来?
kmalloc是从slab缓存区中申请的内存,而slab缓存区的内存实际上是从操作系统的低端内存申请到的。kmalloc过程其实并未创建页表,以后也不用再创建页表,这是因为系统启动过程已经通过map_lowmem->create_mappping为所有的低端内存创建好了页表。后文会对这一过程进一步描述。
2) 用户态:malloc
malloc:malloc过程其实大部分人都很了解,它主要通过brk或mmap拓展进程的虚拟地址空间,malloc过程本身并未申请物理内存,也未创建页表。可以理解为malloc申请了一段用户态的虚拟地址区间,然后当用户真正使用这段地址时,会触发arm的缺页异常,缺页异常处理函数handle_pte_fault会通过set_pte_at->set_pte_ext->cpu_v7_set_pte_ext 一系列调用 来实现页表创建。事实上,malloc申请的内存,来源也和kmalloc一样,也是来自低端内存。不过与kmaollc不同的是,缺页异常中对这段低端内存对应的物理内存,又做了二次映射,即重新创建了页表。发现了么,同一物理地址其实可以对应多个虚拟地址进行映射,即存在多个页表项,这其实也是多进程的实现基础,但同时,使用过程中也可能引入cache问题,可以参考我的另一篇介绍cache的博文。
ps:可以通过/proc/$pid/smaps或/proc/$pid/maps查看一个进程的地址空间。
3)高端内存:vmalloc
vmalloc实际上是为[VMALLOC_START,VMALLOC_END]地址区间创建页表。注意,我们申请到的低端内存对应的物理内存,不能再映射到其他的低端地址,却可以通过vmap映射到高端内存区[VMALLOC_START,VMALLOC_END]。
4)io地址的映射:ioremap
驱动中经常使用ioremap 将io地址映射到指定区间,实际上,这一过程的本质也是创建页表。
以上几种申请内存的方式,最终都是通过cpu_v7_set_pte_ext配置页表项。
二  如何创建页表:
通过上面分析,我们了解了几种创建页表的时机,其实无论是kmalloc还是malloc,最后申请到的内存,都是通过cpu_v7_set_pte_ext来创建页表的。万法同源,我们以低端内存的映射为例,详细介绍下页表的创建。
1 创建页表函数
1)为低端内存创建页表:
setup_arch->paging_init->map_lowmem()->create_mapping
2)为io内存映射创建页表,ahb、apb等地址的映射。iotable_init->create_mapping
可见无论低端内存还是io地址,系统为他们创建页表项都是通过create_mapping
2 create_mapping介绍:
static void __init create_mapping(struct map_desc *md){unsigned long addr, length, end;phys_addr_t phys;const struct mem_type *type;pgd_t *pgd;if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {printk(KERN_WARNING "BUG: not creating mapping for 0x%08llx"       " at 0x%08lx in user region\n",       (long long)__pfn_to_phys((u64)md->pfn), md->virtual);return;}if ((md->type == MT_DEVICE || md->type == MT_ROM) &&    md->virtual >= PAGE_OFFSET &&    (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {printk(KERN_WARNING "BUG: mapping for 0x%08llx"       " at 0x%08lx out of vmalloc space\n",       (long long)__pfn_to_phys((u64)md->pfn), md->virtual);}/* mem_type中保存了页表属性和页中间目录的属性 */type = &mem_types[md->type];addr = md->virtual & PAGE_MASK;        /*phys对应物理地址,本函数实际上就是把phys映射到addr*/        phys = __pfn_to_phys(md->pfn);length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {printk(KERN_WARNING "BUG: map for 0x%08llx at 0x%08lx can not "       "be mapped using pages, ignoring.\n",       (long long)__pfn_to_phys(md->pfn), addr);return;}/* 页表创建过程如下:*/pgd = pgd_offset_k(addr);end = addr + length;do {unsigned long next = pgd_addr_end(addr, end);alloc_init_pud(pgd, addr, next, phys, type);phys += next - addr;addr = next;} while (pgd++, addr != end);}
注意:低端内存的页表属性已经在mem_types][MT_MEMORY_RW]中定义好。如果想要通过kamlloc获取到的内存是只读的,可以在此修改。
3 页表创建过程
static void __init create_mapping(struct map_desc *md){/*1 init_task进程的页全局目录地址:swapper_pg_dir以后每个进程都从这里拷贝页全局目录到各自的pgd里dup_mm->pgd_alloc中实现,cpu_switch_mm中切换。*/pgd = pgd_offset_k(addr);end = addr + length;do {unsigned long next = pgd_addr_end(addr, end);/* 每个页全局目录 都要初始化页二级目录 */alloc_init_pud(pgd, addr, next, phys, type);phys += next - addr;addr = next;} while (pgd++, addr != end);}->2 页二级目录初始化:static void __init alloc_init_pud(pgd_t *pgd, unsigned long addr,  unsigned long end, phys_addr_t phys,  const struct mem_type *type){/*页二级目录地址,因为不使用页二级目录,所以页二级目录地址等于页全局目录地址。为实现软件兼容性所以代码中还保留了页二级目录的处理流程,只不过它的地址即是页全局目录*/pud_t *pud = pud_offset(pgd, addr);unsigned long next;do {next = pud_addr_end(addr, end);/*页三级目录初始化*/alloc_init_pmd(pud, addr, next, phys, type);phys += next - addr;} while (pud++, addr = next, addr != end);}->3页三级目录初始化:static void __init alloc_init_pmd(pud_t *pud, unsigned long addr,      unsigned long end, phys_addr_t phys,      const struct mem_type *type){/*页三级目录地址,因为不使用页三级目录,所以页三级目录地址等于页二级目录,当然也等于页全局目录地址。为实现软件兼容性所以代码中还保留了页三级目录的处理流程,只不过它的地址即是页全局目录*/pmd_t *pmd = pmd_offset(pud, addr);unsigned long next;do {next = pmd_addr_end(addr, end);/*if;else都有可能执行到*//* 当我们创建页表项的虚拟地址区间是1M对齐时 */if (type->prot_sect && ((addr | next | phys) & ~SECTION_MASK) == 0) {__map_init_section(pmd, addr, next, phys, type);} /* 当我们创建页表项的虚拟地址区间非1M对齐时 */else {alloc_init_pte(pmd, addr, next,__phys_to_pfn(phys), type);}phys += next - addr;} while (pmd++, addr = next, addr != end);}->4 页表初始化:虚拟地址区间1M时,如下方式MMU映射:static void __init __map_init_section(pmd_t *pmd, unsigned long addr,unsigned long end, phys_addr_t phys,const struct mem_type *type){pmd_t *p = pmd;do {*pmd = __pmd(phys | type->prot_sect);phys += SECTION_SIZE;} while (pmd++, addr += SECTION_SIZE, addr != end);flush_pmd_entry(p);}虚拟地址区间非1M对齐时,如下方式MMU映射,即创建页表:static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,  unsigned long end, unsigned long pfn,  const struct mem_type *type){        /* 此时申请页表的地址(一个页表项4byte,一个pmd有512个页表项)页表的基地址会赋值给pmd ,__pmd_populate中完成赋值*/pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);do {/*为每一页配置页表属性,注意此时用到了mem_types定义的属性*/set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);pfn++;} while (pte++, addr += PAGE_SIZE, addr != end);}
4 页表属性配置函数:
无论内核态还是用户态,linux最后都是通过cpu_v7_set_pte_ext接口设置页表属性的:
ENTRY(cpu_v7_set_pte_ext)/*r0:页表项地址r1:页表属性|物理地址偏移将页表属性r1配置给页表项r0*/strr1, [r0]@ linux versionbicr3, r1, #0x000003f0bicr3, r3, #PTE_TYPE_MASKorrr3, r3, r2orrr3, r3, #PTE_EXT_AP0 | 2tstr1, #1 << 4orrner3, r3, #PTE_EXT_TEX(1)eorr1, r1, #L_PTE_DIRTYtstr1, #L_PTE_RDONLY | L_PTE_DIRTYorrner3, r3, #PTE_EXT_APXtstr1, #L_PTE_USERorrner3, r3, #PTE_EXT_AP1tstr1, #L_PTE_XNorrner3, r3, #PTE_EXT_XNtstr1, #L_PTE_YOUNGtstner1, #L_PTE_VALIDeorner1, r1, #L_PTE_NONEtstner1, #L_PTE_NONEmoveqr3, #0 ARM(strr3, [r0, #2048]! ) THUMB(addr0, r0, #2048 ) THUMB(strr3, [r0] )ALT_SMP(W(nop))ALT_UP (mcrp15, 0, r0, c7, c10, 1)@ flush_ptebxlrENDPROC(cpu_v7_set_pte_ext)
以上函数实际上就是把页表属性配置到页表项中,C/B位也在此设置。其中页表项各字段含义如下:

注意:cpu_v7_set_pte_ext设置页表属性的时机一般为:
1)  驱动中io地址映射: ioremap->ioremap_pte_range->set_pte_ext
2)  用户空间mmap : mmap->mmap_mem->remap_pfn_range;
3)  系统初始化过程: map_lowmem/iotable_init->create_mapping
4)  等等,在此不逐一列举了。
5 在实际linux系统软件开发中如何修改页表属性,给出两种常见方式:

1) 可以通过配置全局变量mem_types属性修改低端内存cache的开启情况create_mapping中真正使用该配置。内核在初始化中的会为低端内存、io地址等地址空间设置页表属性,如果要修改这一部分内存的属性包括cache开启关闭情况,mem_types中定义了初始页表项属性,形如:
prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY
其实在内核启动过程的build_mem_type_table()函数里,还会根据不同arm版本做调整。

2) 可以在映射一段物理内存之前通过系统的标志宏来修改cache使用情况。

举例:/dev/mem驱动中实现物理内存的重新映射(即mmap函数的实现过程).
mmap_mem->remap_pfn_range中:vm_page_prot = pgprot_writecombine(pVma->vm_page_prot);这段代码就是配置cache属性的。

内存映射前关闭cache的宏:
#define pgprot_noncached(prot) \
    __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED)
#define pgprot_writecombine(prot) \
    __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE)
关闭cache时使用的具体标志位:
#define L_PTE_MT_UNCACHED   (_AT(pteval_t, 0) << 2) /* strongly ordered */
#define L_PTE_MT_BUFFERABLE (_AT(pteval_t, 1) << 2) /* normal non-cacheable */

【总结】

以上分析了linux系统实现分页机制的过程。为突出分页的主线,其他内存管理的知识直接给出了结论,未做源码上的细致分析。以后有时间可以详细介绍下linux系统的内存管理方法。
【拓展】
笔者曾经开发过两款调试工具:
一是可以通过命令查询一个内存地址是否打开cache。
二是可以查询一个地址被修改的情况,包括何时被修改,修改为何值等。
这两款调试工具的实现原理都和分页机制大有关联。







1 0
原创粉丝点击