zoukankan      html  css  js  c++  java
  • Linux内存管理 (2)页表的映射过程

     专题:Linux内存管理专题

     关键词:swapper_pd_dir、ARM PGD/PTE、Linux PGD/PTE、pgd_offset_k

    Linux下的页表映射分为两种,一是Linux自身的页表映射,另一种是ARM32 MMU硬件的映射。

    1. ARM32页表映射

     由于ARM32和Linux内核维护的页表项有所不同,所以维护了两套PTE。

    PGD存放在swapper_pd_dir中,一个PGD目录项其实包含了两份ARM32 PGD。

    所以再分配PTE的时候,共分配了1024个PTE,512个给Linux OS维护用;512个给ARM32 MMU用,对应两个PGD的页表数目。

    由于Linux OS和ARM32的PTE紧邻,所以两者的转换也方便进行。

    1.1 ARM32处理器查询页表

    32bit的Linux采用三级映射:PGD-->PMD-->PTE,64bit的Linux采用四级映射:PGD-->PUD-->PMD-->PTE,多了个PUD。

    缩写是PGD:Page Global Directory、PUD:Page Upper Directory、PMD:Page Middle Directory、PTE:Page Table Entry。

    在ARM32 Linux采用两层映射,省略了PMD,除非在定义了CONFIG_ARM_LPAE才会使用3级映射。

    在ARM32架构中,可以按段(section)来映射,这是采用单层映射模式

    使用页面映射需要两层映射结构,页面可以是64KB或4KB大小。

    1.1.1 ARM32架构MMU4KB页面映射过程

    如果采用页表映射的方式,段映射表就变成一级映射表(Linux中称为PGD),其页表项提供的不再是物理地址,而是二级页表的基地址。

    32位虚拟地址的高12位(bit[31:20])作为访问一级页表的索引值,找到相应的表项,每个表项指向一个二级页表。

    以虚拟地址的次8位(bit[19:12])作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。

    最后将这20位物理页面地址和虚拟地址的低12位拼凑在一起,得到最终的32位物理地址。

    这个过程在ARM32架构中由MMU硬件完成,软件不需要接入。

    ARM32架构MMU页表映射过程

    1.1.2 ARMv7-AR中关于Short Descriptor映射概览图

    关于4K页表的映射过程在ARMv7-AR用户架构手册有关介绍。

    一个地址映射的概览图,32位虚拟地址从TTBR1中找到First-level table地址,然后取虚拟地址VA[31:20]作为序号找到Second-level table地址。

    取虚拟地址VA[19:12]作为序号找到Page地址。

    规格书中Small Page映射过程

    Figure B3-11 Small page address translation是映射的细节:

    1.2 Linux页表映射相关数据结构

    我们知道在map_lowmem()使用create_mapping()创建页表映射,这个函数的参数结构是struct map_desc。

    下面来研究它的相关结构,有助于理解内核是如何处理页表映射的。

    archarmincludeasmmachmap.h:
    
    struct map_desc {
        unsigned long virtual;------虚拟地址起始地址
        unsigned long pfn;----------物理地址开始页帧号
        unsigned long length;-------内存空间大小
        unsigned int type;----------mem_types中的序号
    };

    map_desc中的type指向类型为struct mem_type的mem_types数组:

    archarmmmmm.h:
    struct mem_type {
        pteval_t prot_pte;------------PTE属性
        pteval_t prot_pte_s2;---------定义CONFIG_ARM_LPAE才有效
        pmdval_t prot_l1;-------------PMD属性
        pmdval_t prot_sect;-----------Section类型映射
        unsigned int domain;----------定义ARM中不同的域
    };
    
    archarmmmmmu.c:
    static struct mem_type mem_types[] = {
    ...
        [MT_MEMORY_RWX] = {
            .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,----------------------注意这里都是L_PTE_*类型,需要在写入MMU对应PTE时进行转换。
            .prot_l1   = PMD_TYPE_TABLE,
            .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
            .domain    = DOMAIN_KERNEL,
        },
        [MT_MEMORY_RW] = {
            .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
                     L_PTE_XN,
            .prot_l1   = PMD_TYPE_TABLE,
            .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
            .domain    = DOMAIN_KERNEL,
        },
    ...
    }

    下面重点关注Page Table类型的一级页表和二级页表的细节,以及Linux内核中的定义:

     ARM32中PGD定义

     下面是First-level descriptor详细说明:

    /*
     * Hardware page table definitions.
     *
     * + Level 1 descriptor (PMD)
     *   - common
     */
    #define PMD_TYPE_MASK        (_AT(pmdval_t, 3) << 0)---------------------------01对应PageTable
    #define PMD_TYPE_FAULT        (_AT(pmdval_t, 0) << 0)
    #define PMD_TYPE_TABLE        (_AT(pmdval_t, 1) << 0)
    #define PMD_TYPE_SECT        (_AT(pmdval_t, 2) << 0)
    #define PMD_PXNTABLE        (_AT(pmdval_t, 1) << 2)     /* v7 */
    #define PMD_BIT4        (_AT(pmdval_t, 1) << 4)
    #define PMD_DOMAIN(x)        (_AT(pmdval_t, (x)) << 5)
    #define PMD_PROTECTION        (_AT(pmdval_t, 1) << 9)        /* v5 */

     ARM32中PTE定义

    下面是Second-level descriptor详细说明:

    /*
     * + Level 2 descriptor (PTE)
     *   - common
     */
    #define PTE_TYPE_MASK        (_AT(pteval_t, 3) << 0)
    #define PTE_TYPE_FAULT        (_AT(pteval_t, 0) << 0)
    #define PTE_TYPE_LARGE        (_AT(pteval_t, 1) << 0)
    #define PTE_TYPE_SMALL        (_AT(pteval_t, 2) << 0)
    #define PTE_TYPE_EXT        (_AT(pteval_t, 3) << 0)        /* v5 */
    #define PTE_BUFFERABLE        (_AT(pteval_t, 1) << 2)
    #define PTE_CACHEABLE        (_AT(pteval_t, 1) << 3)
    
    /*
     *   - extended small page/tiny page
     */
    #define PTE_EXT_XN        (_AT(pteval_t, 1) << 0)        /* v6 */
    #define PTE_EXT_AP_MASK        (_AT(pteval_t, 3) << 4)
    #define PTE_EXT_AP0        (_AT(pteval_t, 1) << 4)
    #define PTE_EXT_AP1        (_AT(pteval_t, 2) << 4)
    #define PTE_EXT_AP_UNO_SRO    (_AT(pteval_t, 0) << 4)
    #define PTE_EXT_AP_UNO_SRW    (PTE_EXT_AP0)
    #define PTE_EXT_AP_URO_SRW    (PTE_EXT_AP1)
    #define PTE_EXT_AP_URW_SRW    (PTE_EXT_AP1|PTE_EXT_AP0)
    #define PTE_EXT_TEX(x)        (_AT(pteval_t, (x)) << 6)    /* v5 */
    #define PTE_EXT_APX        (_AT(pteval_t, 1) << 9)        /* v6 */
    #define PTE_EXT_COHERENT    (_AT(pteval_t, 1) << 9)        /* XScale3 */
    #define PTE_EXT_SHARED        (_AT(pteval_t, 1) << 10)    /* v6 */
    #define PTE_EXT_NG        (_AT(pteval_t, 1) << 11)    /* v6 */

     Linux中PTE定义

    由于Linux对于PTE的定义和ARM硬件不一致,下面的L_开头的定义都是针对Linux的,L_MT开头的是bit[5:2]表示的内存类型。

    /*
     * "Linux" PTE definitions.
     *
     * We keep two sets of PTEs - the hardware and the linux version.
     * This allows greater flexibility in the way we map the Linux bits
     * onto the hardware tables, and allows us to have YOUNG and DIRTY
     * bits.
     *
     * The PTE table pointer refers to the hardware entries; the "Linux"
     * entries are stored 1024 bytes below.
     */
    #define L_PTE_VALID        (_AT(pteval_t, 1) << 0)        /* Valid */
    #define L_PTE_PRESENT        (_AT(pteval_t, 1) << 0)
    #define L_PTE_YOUNG        (_AT(pteval_t, 1) << 1)
    #define L_PTE_DIRTY        (_AT(pteval_t, 1) << 6)
    #define L_PTE_RDONLY        (_AT(pteval_t, 1) << 7)
    #define L_PTE_USER        (_AT(pteval_t, 1) << 8)
    #define L_PTE_XN        (_AT(pteval_t, 1) << 9)
    #define L_PTE_SHARED        (_AT(pteval_t, 1) << 10)    /* shared(v6), coherent(xsc3) */
    #define L_PTE_NONE        (_AT(pteval_t, 1) << 11)
    
    /*
     * These are the memory types, defined to be compatible with
     * pre-ARMv6 CPUs cacheable and bufferable bits:   XXCB
     */
    #define L_PTE_MT_UNCACHED    (_AT(pteval_t, 0x00) << 2)    /* 0000 */
    #define L_PTE_MT_BUFFERABLE    (_AT(pteval_t, 0x01) << 2)    /* 0001 */
    #define L_PTE_MT_WRITETHROUGH    (_AT(pteval_t, 0x02) << 2)    /* 0010 */
    #define L_PTE_MT_WRITEBACK    (_AT(pteval_t, 0x03) << 2)    /* 0011 */
    #define L_PTE_MT_MINICACHE    (_AT(pteval_t, 0x06) << 2)    /* 0110 (sa1100, xscale) */
    #define L_PTE_MT_WRITEALLOC    (_AT(pteval_t, 0x07) << 2)    /* 0111 */
    #define L_PTE_MT_DEV_SHARED    (_AT(pteval_t, 0x04) << 2)    /* 0100 */
    #define L_PTE_MT_DEV_NONSHARED    (_AT(pteval_t, 0x0c) << 2)    /* 1100 */
    #define L_PTE_MT_DEV_WC        (_AT(pteval_t, 0x09) << 2)    /* 1001 */
    #define L_PTE_MT_DEV_CACHED    (_AT(pteval_t, 0x0b) << 2)    /* 1011 */
    #define L_PTE_MT_VECTORS    (_AT(pteval_t, 0x0f) << 2)    /* 1111 */
    #define L_PTE_MT_MASK        (_AT(pteval_t, 0x0f) << 2)

    ARM PMD描述符bit[8:5]用于描述Domain,但ARM Linux只定义使用三个:

    #define DOMAIN_KERNEL    2---------用于内核空间
    #define DOMAIN_TABLE    2
    #define DOMAIN_USER    1-----------用于用户空间
    #define DOMAIN_IO    0-------------用于I/O地址域

    1.3 设置PGD页面目录

    create_mapping的参数是struct map_desc类型,用于描述一个虚拟地址区域线性映射到物理区域。基于这块区域创建PGD/PTE。

    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;
    ...
        type = &mem_types[md->type];------------------------------找到对应的struct mem_type
    ...
        addr = md->virtual & PAGE_MASK;---------------------------对齐到页
        phys = __pfn_to_phys(md->pfn);----------------------------页到物理地址转换
        length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
    ...
    pgd
    = pgd_offset_k(addr);---------------------------------根据addr找到对应虚拟地址对应的pgd地址 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);-----------------------------遍历区间地址,步长是PGDIR_SIZE,即2MB大小的空间。 }

    这里面有三个地方需要解释:

    pgd_offset_k

    将虚拟地址进行转换得到PMD的指针。

    #define PGDIR_SHIFT 21
    
    /* 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))
    
    
    #define pgd_offset_k(addr)    pgd_offset(&init_mm, addr)
    
    struct mm_struct init_mm = {
        .mm_rb        = RB_ROOT,
        .pgd        = swapper_pg_dir,
        .mm_users    = ATOMIC_INIT(2),
        .mm_count    = ATOMIC_INIT(1),
        .mmap_sem    = __RWSEM_INITIALIZER(init_mm.mmap_sem),
        .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
        .mmlist        = LIST_HEAD_INIT(init_mm.mmlist),
        INIT_MM_CONTEXT(init_mm)
    };

    由虚拟内存布局图中swapper_pg_dir可知,大小为16KB,里面有详细的解释。init_mm.pgd指向swapper_pg_dir。

    pgd_addr_end

    includeasm-genericpgtable.h:
    
    #define pgd_addr_end(addr, end)                        
    ({    unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;    
        (__boundary - 1 < (end) - 1)? __boundary: (end);        
    })
    
    archarmincludeasmpgtable-2level.h:
    /*
     * PMD_SHIFT determines the size of the area a second-level page table can map
     * PGDIR_SHIFT determines what a third-level page table entry can map
     */
    #define PMD_SHIFT        21
    #define PGDIR_SHIFT        21
    
    #define PMD_SIZE        (1UL << PMD_SHIFT)
    #define PMD_MASK        (~(PMD_SIZE-1))
    #define PGDIR_SIZE        (1UL << PGDIR_SHIFT)
    #define PGDIR_MASK        (~(PGDIR_SIZE-1))

    由于PGDIR_SHIFT为21,所以一个PGD页表目录对应2MB大小的空间,即[addr, addr+PGDIR_SIZE)。所以PGD的数目为2^11,2028个。整个PGD页表占用空间为2048*4B=8KB。

    这和ARM硬件的4096 PGD不一致。这里涉及到Linux实现技巧,在创建PTE中进行分析。

    所以此处按照2MB步长,遍历[virtual, virtual+length)空间创建PDG页表和PTE

    alloc_init_pte

    由于ARM-Linux采用两级页表映射,跳过PUD/PMD,直接到alloc_init_pte创建PTE。

    alloc_init_pud-->alloc_init_pmd-->alloc_init_pte
    
    archarmmmmmu.c:
    static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,------------这里的pmd=pud=pgd。
                      unsigned long end, unsigned long pfn,
                      const struct mem_type *type)
    {
        pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);------------------使用prot_l1作为参数,创建PGD页表目录,返回addr对应的pte地址。
        do {
            set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);---------调用体系结构相关汇编,配置PTE。
            pfn++;
        } while (pte++, addr += PAGE_SIZE, addr != end);-------------------------遍历[addr, end)区间内存,以PAGE_SIZE为步长。
    }

    下面看看如何分配PGD页表目录:

    static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
    {
        if (pmd_none(*pmd)) {---------------------------------------------------如果PGD的内容为空,即PTE还没有创建,择取建立页面。
            pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);-------分配512+512个PTE页表项
            __pmd_populate(pmd, __pa(pte), prot);-------------------------------生成pmd页表目录,并刷入RAM
        }
        BUG_ON(pmd_bad(*pmd));
        return pte_offset_kernel(pmd, addr);------------------------------------返回当前addr对应的PTE地址
    }
    
    early_alloc-->early_alloc_aligned:
    static void __init *early_alloc_aligned(unsigned long sz, unsigned long align)
    {
        void *ptr = __va(memblock_alloc(sz, align));-------------------------------------基于memblock进行分配,这里分配4096B,刚好是一页大小。
        memset(ptr, 0, sz);
        return ptr;
    }

    所以存放PGD需要的空间通过memblock进行申请,PTE_HWTABLE_OFF和PTE_HWTABLE_SIZE都为512,所以一个1024个PTE。

    下面是early_pte_alloc分配的空间示意图:前面512个表项是给Linux OS使用的,后512个表项是给ARM硬件MMU用的。

    Linux内核PGD/PTE映射关系

    static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
                      pmdval_t prot)
    {
        pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;------------------生成pmdp[0]的内容
        pmdp[0] = __pmd(pmdval);
    #ifndef CONFIG_ARM_LPAE
        pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));---------------------生成紧邻的pmdp[1]的内容
    #endif
        flush_pmd_entry(pmdp);---------------------------------------------将pmdp两个刷入到RAM中
    }

     Linux的PGD页表目录和ARM32不同,总数和ARM32是一样的。

     

    在arm_mm_memblock_reserve中,通过swapper_pg_dir可以知道其大小为16KB。

    就来看看SWAPPER_PG_DIR_SIZE,一共2048个PGD,但是每个PGD包含了两个相邻的PGD页面目录项。

    
    

    typedef pmdval_t pgd_t[2];---------------------------------------------8字节

    #define SWAPPER_PG_DIR_SIZE (PTRS_PER_PGD * sizeof(pgd_t))-------------2048*8B=16KB


    /*
    * Reserve the special regions of memory */ void __init arm_mm_memblock_reserve(void) { /* * Reserve the page tables. These are already in use, * and can only be in node 0. */ memblock_reserve(__pa(swapper_pg_dir), SWAPPER_PG_DIR_SIZE); ... }

      

    1.4 设置PTE表项

    要理解是如何设置PTE表项,就需要参照B3.3.1 Translation table entry formants中关于Second-level descriptors的描述。

     

     

      

    archarmincludeasmpgtable-2level.h:
    #define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
    
    archarmincludeasmglue-proc.h:
    #ifndef MULTI_CPU
    ...
    #define cpu_set_pte_ext            __glue(CPU_NAME,_set_pte_ext)
    ...
    #endif
    archarmmmproc-v7-2level.S: /* * cpu_v7_set_pte_ext(ptep, pte) * * Set a level 2 translation table entry. * * - ptep - pointer to level 2 translation table entry----------放入r0 * (hardware version is stored at +2048 bytes) * - pte - PTE value to store----------------------------------放入r1 * - ext - value for extended PTE bits------------------------放入r2 */ ENTRY(cpu_v7_set_pte_ext) #ifdef CONFIG_MMU str r1, [r0] @ linux version----------将r1的值存入r0地址的内存中 bic r3, r1, #0x000003f0--------------------------清除r1的bit[9:4],存入r3 bic r3, r3, #PTE_TYPE_MASK-----------------------PTE_TYPE_MASK为0x03,记清除低2位 orr r3, r3, r2-----------------------------------r3与r2或,存入r3 orr r3, r3, #PTE_EXT_AP0 | 2---------------------这里将bit1和bit4置位,所以是Small page。 tst r1, #1 << 4----------------------------------判断r1的bit4是否为0 orrne r3, r3, #PTE_EXT_TEX(1)--------------------设置TEX为1 eor r1, r1, #L_PTE_DIRTY tst r1, #L_PTE_RDONLY | L_PTE_DIRTY orrne r3, r3, #PTE_EXT_APX-----------------------设置AP[2] tst r1, #L_PTE_USER orrne r3, r3, #PTE_EXT_AP1-----------------------设置AP[1] tst r1, #L_PTE_XN orrne r3, r3, #PTE_EXT_XN------------------------设置XN位 tst r1, #L_PTE_YOUNG tstne r1, #L_PTE_VALID eorne r1, r1, #L_PTE_NONE tstne r1, #L_PTE_NONE moveq r3, #0 ARM( str r3, [r0, #2048]! )---------------------并没有写入r0,而是写入r0+2048Bytes的偏移。 THUMB( add r0, r0, #2048 ) THUMB( str r3, [r0] ) ALT_SMP(W(nop)) ALT_UP (mcr p15, 0, r0, c7, c10, 1) @ flush_pte #endif bx lr ENDPROC(cpu_v7_set_pte_ext)
  • 相关阅读:
    OpenJDK源码研究笔记(十二):JDBC中的元数据,数据库元数据(DatabaseMetaData),参数元数据(ParameterMetaData),结果集元数据(ResultSetMetaDa
    Java实现 LeetCode 257 二叉树的所有路径
    Java实现 LeetCode 257 二叉树的所有路径
    Java实现 LeetCode 257 二叉树的所有路径
    Java实现 LeetCode 242 有效的字母异位词
    Java实现 LeetCode 242 有效的字母异位词
    Java实现 LeetCode 242 有效的字母异位词
    Java实现 LeetCode 241 为运算表达式设计优先级
    Java实现 LeetCode 241 为运算表达式设计优先级
    Java实现 LeetCode 241 为运算表达式设计优先级
  • 原文地址:https://www.cnblogs.com/arnoldlu/p/8087022.html
Copyright © 2011-2022 走看看