Linux内核ARM32页表映射

Linux内核ARM32页表映射,在32bit的Linux内核中一般采用3层的映射模型,第1层是页面目录(PGD),第2层是页面中间目录(PMD),第3层才是页面映射表(PTE)。但在\2RM32系统中只用到两层映射,因此在实际代码中就要在3层的映射模型中合并1层。在ARM32架构中,可以按段(section)来映射,这时采用单层映射模式。使用页面映射需要两层映射结构,页面的选择可以是64KB的大页面或4KB的小页面,如图所示。Linux内核通常默认使用4KB大小的小页面。

ARM32处理器查询页表

Linux内核ARM32页表映射

如果采用单层的段映射,内存中有个段映射表,表中有4096个表项,每个表项的大小是4Byte,所以这个段映射表的大小是16KB,而且其位置必须与16KB边界对齐。每个段表项可以寻址1MB大小的地址空间。当CPU访问内存时,32位虚拟地址的高12位(bit[31:20])用作访问段映射表的索引,从表中找到相应的表项。每个表项提供了一个12位的物理段地址,以及相应的标志位,如可读、可写等标志位。将这个12位物理地址和虚拟地址的低20位拼凑在一起,就得到32位的物理地址。

如果采用页表映射的方式,段映射表就变成一级映射表(First Level table,在Linux内核中称为PGD),其表项提供的不再是物理段地址,而是二级页表的基地址。32位虚拟地址的高12位(bit[31:20])作为访问一级页表的索引值,找到相应的表项,每个表项指向一个二级页表。以虚拟地址的次8位(bit[19:12])作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。最后将这20位物理页面地址和虚拟地址的低12位拼凑在一起,得到最终的32位物理地址。这个过程在ARM32架构中由MMU硬件完成,软件不需要接入。

      [arch/arm/include/asm/pgtable-2level.h]

      #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))

ARM32架构中一级页表PGD的偏移量应该从20位开始,为何这里的头文件定义从21位开始呢?

我们从ARM Linux内核建立具体内存区间的页面映射过程来看页表映射是如何实现的。create_mapping()函数就是为一个给定的内存区间建立页面映射,这个函数使用map_desc数据结构来描述一个内存区间。

      struct map_desc {
            unsigned long virtual;    //虚拟地址的起始地址
            unsigned long pfn;        //物理地址的开始地址的页帧号
            unsigned long length;     //内存区间大小
            unsigned int type;
      };

其中,virtual表示这个区间的虚拟地址起始点,pfn表示起始物理地址的页帧号,length表示内存区间的长度,type表示内存区间的属性,通常有个struct mem_type[]数组来描述内存属性。struct mem_type数据结构描述内存区间类型以及相应的权限和属性等信息,其数据结构定义如下:

      struct mem_type {
            pteval_t prot_pte;
            pteval_t prot_pte_s2;
            pmdval_t prot_|1;
            pmdval_t prot_sect;
            unsigned int domain;
      };

其中,domain成员用于ARM中定义的不同的域,ARM中允许使用16个不同的域,但在ARM Linux中只定义和使用3个。

      #define DOMAIN_KERNEL      2
      #define DOMAIN_TABLE      2
      #define DOMAIN_USER      1
      #define DOMAIN IO  0

DOMAIN_KERNEL和DOMAIN_TABLE其实用于系统空间,DOMAIN_IO用于I/O地址域,实际上也属于系统空间,DOMAIN_USER则是用户空间。

prot_pte成员用于页面表项的控制位和标志位,具体定义在:

      #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)

      #definePROT_PTE_DEVICE          L_PTE_PRESENT|L_PTE_YOUNG|L_PTE_DIRTY|L_PTE_XN
      #define PROT_PTE_S2_DEVICE     PROT_PTE_DEVICE
      #define PROT SECT DEVICE      PMD TYPE SECT|PMD SECT AP WRITE

prot__|1成员用于一级页表项的控制位和标志位,具体定义如下:

      #define PMD_TYPE_MASK        (_AT(pmdval_t, 3) << 0)
      #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 */

系统中定义了一个全局的mem_type[ ]数组来描述所有的内存区间类型。例如, MT_DEVICE_CACHED、MT_DEVICE_WC、MT_MEMORY_RWX和MT_MEMORY_RW类型的内存区间的定义如下:

      static struct mem_type mem_types[] = {
            …
            [MT_DEVICE_CACHED] = {       /* ioremap_cached */
              .prot_pte      = PROT_PTE_DEVICE | L_PTE_MT_DEV_CACHED,
              .prot__|1      = PMD_TYPE_TABLE,
              .prot_sect      = PROT_SECT_DEVICE | PMD_SECT_WB,
              .domain            = DOMAIN_IO,
            },
            [MT_DEVICE_WC] = {     /* ioremap_wc */
              .prot_pte      = PROT_PTE_DEVICE | L_PTE_MT_DEV_WC,
              .prot__|1      = PMD_TYPE_TABLE,
              .prot_sect      = PROT_SECT_DEVICE,
              .domain            = DOMAIN_IO,
            },
            [MT_MEMORY_RWX] = {
              .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,
              .prot_|1   = 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__|1   = PMD_TYPE_TABLE,
                .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
                .domain      = DOMAIN_KERNEL,
            },
      };

这样一个map_desc数据结构就完整地描述了一个内存区间,调用create_mapping()时以此数据结构指针为调用参数。
[start_kernel()->setup_arch()->paging_init()->map_lowmem()->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;

          type = &mem_types[md->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);
          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);
      }

在create_mapping()函数中,以PGDIR_SIZE为单位,在内存区域[virtual,virtual +length]中通过调用alloc_init_pud()来初始化PGD页表项内容和下一级页表PUD。pgd_addr_end()以PGDIR_SIZE为步长。

在第7行代码中,通过md->type来获取描述内存区域属性的mem_type数据结构,这里只需要通过查表的方式获取mem_type数据结构里的具体内容。
在第13行代码中,通过pgd_offset_k()函数获取所属的页面目录项PGD。内核的页表存放在swapper_pg_dir地址中,可以通过init_mm数据结构来获取。

      [mm/init-mm.c]

      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)
      };

内核页表的基地址定义在arch/arm/kernel/head.S汇编代码中。

      [arch/arm/kernel/head.S]

      #define KERNEL_RAM_VADDR            (PAGE_OFFSET + TEXT_OFFSET)
      #define PG_DIR_SIZE       0x4000
      .globl  swapper_pg_dir
        .equ      swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

      [arch/arm/Makefile]
        textofs-y         := 0x00008000
        TEXT OFFSET :=$(textofs-y)

从上面代码中可以推算出页表的基地址是0xc0004000。

pgd_offset_k()宏可以从init_mm数据结构所指定的页面目录中找到地址addr所属的页面目录项指针pgd。首先通过init_mm结构体得到页表的基地址,然后通过addr右移PGDIR_SHIFT得到pgd的索引值,最后在一级页表中找到相应的页表项pgd指针。pgd_offset_k()宏定义如下:

      #define PGDIR_SHIFT            21
      #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)

create_mapping()函数中的第15~22行代码,由于ARM Vexpress平台支持两级页表映射,所以PUD和PMD设置成与PGD等同了。

      static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
      {
            return (pud_t *)pgd;
      }

      static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
      {
            return (pmd_t *)pud;
      }
       因此alloc_init_pud()函数一路调用到alloc_init_pte()函数。
      static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
                              unsigned long end, unsigned long pfn,
                              const struct mem_type *type)
      {
            pte_t *pte = early_pte_alloc(pmd, addr, type->prot_|1);
            do {
                set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
                pfn++;
            } while (pte++, addr += PAGE_SIZE, addr ! = end);
      }

alloc_init_pte()首先判断相应的PTE页表项是否已经存在,如果不存在,那就要新建PTE页表项。接下来的while循环是根据物理地址的pfn页帧号来生成新的PTE表项(PTE entry),最后设置到ARM硬件页表中。

      [create_mapping-> alloc_init_pud-> alloc_init_pmd-> alloc_init_pte->
      early_pte_alloc]
      static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned
      long prot)
      {
            if (pmd_none(*pmd)) {
                pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
                __pmd_populate(pmd, __pa(pte), prot);
            }
            BUG_ON(pmd_bad(*pmd));
            return pte_offset_kernel(pmd, addr);
      }

pmd_none()检查这个参数对应的PMD表项的内容,如果为0,说明页面表PTE还没建立,所以要先去建立页面表。这里会去分配(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE)个PTE页面表项,即会分配512+512个PTE页面表。但是ARM32架构中,二级页表也只有256个页面表项,为何要分配这么多呢?

      #define PTRS_PER_PTE            512
      #define PTRS_PER_PMD            1
      #define PTRS_PER_PGD            2048
      #define PTE_HWTABLE_PTRS      (PTRS_PER_PTE)
      #define PTE_HWTABLE_OFF          (PTE_HWTABLE_PTRS * sizeof(pte_t))
      #define PTE HWTABLE SIZE     (PTRS PER PTE * sizeof(u32))

先回答刚才的问题:ARM结构中一级页表PGD的偏移量应该从20位开始,为何这里的头文件定义从21位开始呢?

  • 这里分配了两个\2TRS_PER_PTE(512)个页面表项,也就是分配了两份页面表项。因为Linux内核默认的PGD是从21位开始,也就是bit[31:21],一共2048个一级页表项。而ARM32硬件结构中,PGD是从20位开始,页表项数目是4096,比Linux内核的要多一倍,那么代码实现上取巧了,以PTE_HWTABLE_OFF为偏移来写PGD表项。也就是在ARM Linux中,一个PGD页表项,映射512个PTE表项。而在真实硬件中,一个PGD页表项,只有256个PTE。也就是说,前512个PTE页面表项是给OS用的(也就是Linux内核用的页表,可以用于模拟L_PTE_DIRTY、L_PTE_YOUNG等标志位),后512个页面表是给ARM硬件MMU使用的。
  • 一次映射两个相邻的一级页表项,也就是对应的两个相邻的二级页表都存放在一个page中。

然后把这个PTE页面表的基地址通过__pmd_populate()函数设置到PMD页表项中。

      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] = __pmd(pmdval);
            pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
            flush_pmd_entry(pmdp);
      }

注意这里是把刚分配的1024个PTE页面表中的第512个页表项的地址作为基地址,再加上一些标志位信息prot作为页表项内容,写入上一级页表项PMD中。
相邻的两个二级页表的基地址分别写入PMD的页表项中的pmdp[0]和pmdp[1]指针中。

      typedef struct { pmdval_t pgd[2]; } pgd_t;

      /* 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))

PGD的定义其实是pmdval_t pgd[2],长度是两倍,也就是pgd包括两份相邻的PTE页表。所以pgd_offset()在查找pgd表项时,是按照pgd[2]长度来进行计算的,因此查找相应的pgd表项时,其中pgd[0]指向第一份PTE页表,pgd[1]指向第二份PTE页表。

pte_offset_kernel()函数返回相应的PTE页面表项,然后通过__pgprot()和pfn组成PTE entry,最后由set_pte_ext()完成对硬件页表项的设置。

      static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
                              unsigned long end, unsigned long pfn,
                              const struct mem_type *type)
      {
            pte_t *pte = early_pte_alloc(pmd, addr, type->prot_|1);
            do {
                set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
                pfn++;
            } while (pte++, addr += PAGE_SIZE, addr ! = end);
      }

set_pte_ext()对于不同的CPU有不同的实现。对于基于ARMv7-A架构的处理器,例如Cortex-A9,它的实现是在汇编函数cpu_v7_set_pte_ext中:
[arch/arm/mm/proc-v7-2level.S]

      0    ENTRY(cpu_v7_set_pte_ext)
      1    #ifdef CONFIG_MMU
      2              str    r1, [r0]                       @ linux version
      3
      4              bic    r3, r1, #0x000003f0
      5              bic    r3, r3, #PTE_TYPE_MASK
      6              orr    r3, r3, r2
      7              orr    r3, r3, #PTE_EXT_AP0 | 2
      8
      9              tst    r1, #1 << 4
      10             orrne r3, r3, #PTE_EXT_TEX(1)  //设置TEX
      11
      12
      13             eor    r1, r1, #L_PTE_DIRTY
      14             tst    r1, #L_PTE_RDONLY | L_PTE_DIRTY
      15             orrne r3, r3, #PTE_EXT_APX //设置AP[2]
      16
      17             tst    r1, #L_PTE_USER
      18             orrne r3, r3, #PTE_EXT_AP1 //设置AP[1:0]
      19
      20             tst    r1, #L_PTE_XN
      21             orrne r3, r3, #PTE_EXT_XN  //设置PXN位
      22
      23             tst    r1, #L_PTE_YOUNG
      24             tstne r1, #L_PTE_VALID
      25             eorne r1, r1, #L_PTE_NONE
      26             tstne r1, #L_PTE_NONE
      27             moveq r3, #0
      28
      29      ARM(  str  r3, [r0, #2048]! )  //写入硬件页表,硬件页表在软件页表+2048Byte
      30             ALT_SMP(W(nop))
      31             ALT_UP (mcr     p15, 0, r0, c7, c10, 1)         @ flush_pte
      32      #endif
      33             bx      lr
      34      ENDPROC(cpu v7 set pte ext)

cpu_v7_set_pte_ext()函数参数r0表示PTE entry页面表项的指针,注意ARM Linux中实现了两份页表,硬件页表的地址r0 + 2048。因此r0指Linux版本的页面表地址,r1表示要写入的Linux版本的PTE页面表项的内容,这里指Linux版本的页面表项的内容,而非硬件版本的页面表项内容。该函数的主要目的是根据Linux版本的页面表项内容来填充ARM硬件版本的页表项。

首先把Linux版本的页面表项内容写入Linux版本的页表中,然后根据mem_type数据结构prot_pte的标志位来设置ARMv7-A硬件相关的标志位。prot_pte的标志位是Linux内核中采用的,定义在arch/arm/include/asm/pgtable-2level.h头文件中,而硬件相关的标志位定义在arch/arm/include/asm/pgtable-2level-hwdef.h头文件。这两份标志位对应的偏移是不一样的,所以不同架构的处理器需要单独处理。ARM32架构硬件PTE页面表定义的标志位如下:

      [arch/arm/include/asm/pgtable-2level-hwdef.h]
      /*
        *   - 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页面表相关的软件标志位如下:
[arch/arm/include/asm/pgtable-2level.h]

      /*
        * "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)

第9~10行代码设置ARM硬件页表的PTE_EXT_TEX比特位。
第13~15行代码设置ARM硬件页表的PTE_EXT_APX比特位。
第17~18行代码设置ARM硬件页表的PTE_EXT_AP1比特位。
第20~21行代码设置ARM硬件页表的PTE_EXT_XN比特位。
第23~27行代码,在旧版本的Linux内核代码中(例如Linux 3.7),等同于如下代码片段:

      tst  r1, #L_PTE_YOUNG
      tstne      r1, #L_PTE_PRESENT
      moveq      r3, #0

如果没有设置L_PTE_YOUNG并且L_PTE_PRESENT置位,那就保持Linux版本的页表不变,把\2RM32硬件版本的页面表项内容清零。代码中的\2_PTE_VALID和L_PTE_NONE这两个软件比特位是后来添加的,因此在Linux 3.7及以前的内核版本中更容易理解一些。

为什么这里要把ARM硬件版本的页面表项内容清零呢?我们观察ARM32硬件版本的页面表的相关标志位会发现,没有表示页面被访问和页面在内存中的硬件标志位。Linux内核最早是基于x86体系结构设计的,所以Linux内核关于页表的很多术语和设计都针对x86架构,而ARM Linux只能从软件架构上去跟随了,因此设计了两套页表。在x86的页面表中有3个标志位是ARM32硬件页面表没有提供的。

  • PTE_DIRTY:CPU在写操作时会设置该标志位,表示对应页面被写过,为脏页。
  • PTE_YOUNG:CPU访问该页时会设置该标志位。在页面换出时,如果该标志位置位了,说明该页刚被访问过,页面是young的,不适合把该页换出,同时清除该标志位。
  • PTE_PRESENT:表示页在内存中。

因此在ARM Linux实现中需要模拟上述3个比特位。

如何模拟PTE_DIRTY呢?在ARM MMU硬件为一个干净页面建立映射时,设置硬件页表项是只读权限的。当往一个干净的页面写入时,会触发写权限缺页中断(虽然Linux版本的页面表项标记了可写权限,但是ARM硬件页面表项还不具有写入权限),那么在缺页中断处理handle_pte_fault()中会在该页的Linux版本PTE页面表项标记为“dirty”,并且发现PTE页表项内容改变了,ptep_set_access_flags()函数会把新的Linux版本的页表项内容写入硬件页表,从而完成模拟过程。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!