Linux内核ARM64页表映射

Linux内核ARM64页表映射,对于ARM64架构来说,目前基于ARMv8-A架构的处理器最大可以支持到48根地址线,也就是寻址248的虚拟地址空间,即虚拟地址空间范围为0x0000_0000_0000_0000~0x0000_FFFF_FFFF_FFFF,共256TB。理论上完全可以做到64根地址线,那么最大就可以寻找到264的虚拟地址空间。但是对于目前的应用来说,256TB的虚拟地址空间已经足够使用了。因为如果支持64位虚拟地址空间,意味着处理器设计需要考虑更多的地址线, CPU的设计复杂度会增大。

基于ARMv8-A架构的处理器的虚拟地址分成两个区域。一个是从0x0000_0000_0000_0000到0x0000_FFFF_FFFF_FFFF,另外一个是从0xFFFF_0000_0000_0000到0xFFFF_FFFF_FFFF_FFFF。

基于ARMv8-A架构的处理器可以通过配置CONFIG_ARM64_ VA_BITS这个宏来设置虚拟地址的宽度。

      [arch/arm64/Kconfig]

      config ARM64_VA_BITS
              int
              default 39 if ARM64_VA_BITS_39
              default 42 if ARM64_VA_BITS_42
              default 48 if ARM64 VA BITS 48

另外基于ARMv8-A架构的处理器支持的最大物理地址宽度也是48位。

Linux内存空间布局与地址映射的粒度和地址映射的层级有关。基于ARMv8-A架构的处理器支持的页面大小可以是4KB、16KB或者64KB。映射的层级可以是3级或者4级。

下面是页面大小为4KB,地址宽度为48位,4级映射的内存分布图:

      AArch64 Linux memory layout with 4KB pages + 4 levels:
      Start             End             Size     Use
      -----------------------------------------------------------------
      0000000000000000     0000ffffffffffff 256TB       user
      ffff000000000000    ffffffffffffffff  256TB       kernel

下面是页面大小为4KB,地址宽度为48位,3级映射的内存分布图:

      AArch64 Linux memory layout with 4KB pages + 3 levels:
      Start             End             Size     Use
      ------------------------------------------------------------
      0000000000000000  0000007fffffffff      512GB            user
      ffffff8000000000  ffffffffffffffff      512GB            kernel

Linux内核的documentation/arm64/memory.txt文件中还有其他不同配置的内存分布图。

我们的QEMU实验平台配置4KB大小页面,48位地址宽度,4级映射,下面以此为蓝本介绍ARM64的地址映射过程。

如下图所示,地址转换过程如下。

基于ARMv8-A架构的处理器虚拟地址查找(4KB页)

Linux内核ARM64页表映射

(1)如果输入的虚拟地址最高位bit[63]为1,那么这个地址是用于内核空间的,页表的基地址寄存器用TTBR1_EL1(Translation Table Base Register 1)。如果bit[63]等于0,那么这个虚拟地址属于用户空间,页表基地址寄存器用TTBR0。
(2)TTBRx寄存器保存了第0级页表的基地址(L0 Table base address, Linux内核中称为PGD), L0页表中有512个表项(Table Descriptor),以虚拟地址的bit[47:39]作为索引值在\20页表中查找相应的表项。每个表项的内容含有下一级页表的基地址,即\21页表(Linux内核中称为PUD)的基地址。
(3)PUD页表中有512个表项,以虚拟地址的bit[38:30]为索引值在PUD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L2页表(Linux内核中称为PMD)的基地址。
(4)PMD页表中有512个表项,以虚拟地址的bit[29:21]为索引值在PMD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L3页表(Linux内核中称为PTE)的基地址。
(5)在PTE页表中,以虚拟地址的bit[20:12]为索引值在PTE表中查找相应的表项,每个PTE表项中含有最终的物理地址的bit[47:12],和虚拟地址中bit[11:0]合并成最终的物理地址,完成地址翻译过程。

在内核初始化阶段会对内核空间的页表进行一一映射,实现的函数依然是create_mapping()。

        [start_kenrel-> setup_arch->paging_init->map_mem->__map_memblock->
        create_mapping]

        static void __ref create_mapping(phys_addr_t phys, unsigned long virt,
                              phys_addr_t size, pgprot_t prot)
        {
              if (virt < VMALLOC_START) {
                    pr_warn("BUG: not creating mapping for %pa at 0x%016lx - outside
        kernel range\n",
                        &phys, virt);
                    return;
              }
              __create_mapping(&init_mm, pgd_offset_k(virt & PAGE_MASK), phys, virt,
                      size, prot, early_alloc);
        }

首先会做虚拟地址的检查,低于VMALLOC_START的地址空间不是有效的内核虚拟地址空间。VMALLOC_START等于0xffff_0000_0000_0000。

PGD页表的基地址和ARM32内核一样,通过init_mm数据结构的pgd成员来获取, swapper_pg_dir全局变量指向PGD页表基地址。

        [arch/arm64/kernel/vmlinux.lds.S]

       idmap_pg_dir = .;
       . += IDMAP_DIR_SIZE;
       swapper_pg_dir = .;
       . += SWAPPER_DIR_SIZE;

       [arch/arm64/include/asm/page.h]

       #define SWAPPER_PGTABLE_LEVELS  (CONFIG_ARM64_PGTABLE_LEVELS - 1)
       #define SWAPPER DIR SIZE     (SWAPPER PGTABLE LEVELS * PAGE SIZE)

假设CONFIG_ARM64_PGTABLE_LEVELS定义为4,那么SWAPPER_DIR_SIZE大小就等于3个PAGE_SIZE的大小。从vmlinux.lds.S链接文件可以看到,PGD页表的大小定义为3个PAGE_SIZE。swapper_pg_dir的起始地址由vmlinux.lds.S链接文件计算得来,在我们QEMU实验平台,它的地址是0xffff80000095f800。

下面要通过pgd_offset_k()宏来得到具体的PGD页面目录项的表项。首先通过init_mm数据结构的pgd成员来获取PGD页表的基地址,然后通过pgd_index()来计算PGD页表中的偏移量offset。

      /* to find an entry in a kernel page-table-directory */
      #define pgd_offset_k(addr)     pgd_offset(&init_mm, addr)

      #define pgd_offset(mm, addr)      ((mm)->pgd+pgd_index(addr))

      /* to find an entry in a page-table-directory */
      #define pgd index(addr)     (((addr) >> PGDIR SHIFT) & (PTRS PER PGD - 1))

在pgtable-hwdef.h头文件中,定义了PGDIR_SHIFT、PUD_SHIFT和PMD_SHIFT的宏。在我们QEMU的ARM64的实验平台上,定义了4级页表,也就是CONFIG_ARM64_PGTABLE_LEVELS等于4,另外VA_BITS定义为48。那么通过计算可以得到PGDIR_SHIFT等于39, PUD_SHIFT等于30, PMD_SHIFT等于21。每级页表的页表项数目分别用PTRS_PER_PGD、PTRS_PER_PUD、PTRS_PER_PMD和PTRS_PER_PTE来表示,都等于512。PGDIR_SIZE宏表示一个PGD页表项能覆盖的内存范围大小为512GB。PUD_SIZE等于1GB, PMD_SIZE等于2MB, PAGE_SIZE等于4KB。

      [arch/arm64/include/asm/pgtable-hwdef.h]

      #define PTRS_PER_PTE      (1 << (PAGE_SHIFT - 3))

      /*
        * PMD_SHIFT determines the size a level 2 page table entry can map.
        */
      #if CONFIG_ARM64_PGTABLE_LEVELS > 2
      #define PMD_SHIFT      ((PAGE_SHIFT - 3) * 2 + 3) //20
      #define PMD_SIZE        (_AC(1, UL) << PMD_SHIFT)
      #define PMD_MASK        (~(PMD_SIZE-1))
      #define PTRS_PER_PMD        PTRS_PER_PTE
      #endif

      /*
        * PUD_SHIFT determines the size a level 1 page table entry can map.
        */
      #if CONFIG_ARM64_PGTABLE_LEVELS > 3
      #define PUD_SHIFT      ((PAGE_SHIFT - 3) * 3 + 3)  //30
      #define PUD_SIZE    (_AC(1, UL) << PUD_SHIFT)
      #define PUD_MASK        (~(PUD_SIZE-1))
      #define PTRS_PER_PUD        PTRS_PER_PTE
      #endif

      /*
        * PGDIR_SHIFT determines the size a top-level page table entry can map
        * (depending on the configuration, this level can be 0, 1 or 2).
        */
      #define PGDIR_SHIFT        ((PAGE_SHIFT - 3) * CONFIG_ARM64_PGTABLE_LEVELS +
      3)  //39
      #define PGDIR_SIZE           (_AC(1, UL) << PGDIR_SHIFT)
      #define PGDIR_MASK           (~(PGDIR_SIZE-1))
      #define PTRS_PER_PGD             (1 << (VA_BITS - PGDIR_SHIFT))

      #define VA BITS              (CONFIG ARM64 VA BITS)

这里CONFIG_ARM64_VA_BITS一般定义为48。假设页表的层数大于3, PGDIR_SHIFT为39,那么pgd_index()就是以虚拟地址中第39~48位作为偏移量,代码里先把虚拟地址右移39位,然后再与上PTRS_PER_PGD。

__create_mapping()函数中,以PGDIR_SIZE为步长遍历内存区域[virt, virt+size],然后通过调用alloc_init_pud()来初始化PGD页表项内容和下一级页表PUD。pgd_addr_end()以PGDIR_SIZE为步长。

      /*
       * Create the page directory entries and any necessary page tables for the
       * mapping specified by 'md'.
       */
      static void  __create_mapping(struct mm_struct *mm, pgd_t *pgd,
                            phys_addr_t phys, unsigned long virt,
                            phys_addr_t size, pgprot_t prot,
                            void *(*alloc)(unsigned long size))
      {
            unsigned long addr, length, end, next;

            addr = virt & PAGE_MASK;
            length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

            end = addr + length;
            do {
                next = pgd_addr_end(addr, end);
                alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);
                  phys += next - addr;
            } while (pgd++, addr = next, addr ! = end);
        }

下面看alloc_init_pud()函数。

      [create_mapping->__create_mapping-> alloc_init_pud]
      static void alloc_init_pud(struct mm_struct *mm, pgd_t *pgd,
                              unsigned long addr, unsigned long end,
                              phys_addr_t phys, pgprot_t prot,
                              void *(*alloc)(unsigned long size))
      {
            pud_t *pud;
            unsigned long next;
            if (pgd_none(*pgd)) {
                pud = alloc(PTRS_PER_PUD * sizeof(pud_t));
                pgd_populate(mm, pgd, pud);
            }

            pud = pud_offset(pgd, addr);
            do {
                next = pud_addr_end(addr, end);

                /*
                  * For 4K granule only, attempt to put down a 1GB block
                  */
                if (use_1G_block(addr, next, phys)) {
                      pud_t old_pud = *pud;
                      set_pud(pud, __pud(phys |
                                  pgprot_val(mk_sect_prot(prot))));

                      /*
                        * If we have an old value for a pud, it will
                        * be pointing to a pmd table that we no longer
                        * need (from swapper_pg_dir).
                        *
                        * Look up the old pmd table and free it.
                        */
                      if (! pud_none(old_pud)) {
                            flush_tlb_all();
                            if (pud_table(old_pud)) {
                                  phys_addr_t table = __pa(pmd_offset(&old_pud, 0));
                                  if (! WARN_ON_ONCE(slab_is_available()))
                                        memblock_free(table, PAGE_SIZE);
                            }
                      }
                  } else {
                      alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);
                  }
                  phys += next - addr;
            } while (pud++, addr = next, addr ! = end);
      }

alloc_init_pud()函数会做如下事情。

(1)通过pgd_none()判断当前PGD表项内容是否为空。如果PGD表项内容为空,说明下一级页表为空,那么需要动态分配下一级页表。下一级页表PUD一共有PTRS_PER_PUD个页表项,即512个表项,然后通过pgd_populate()把刚分配的PUD页表设置到相应的PGD页表项中。
(2)通过pud_offset()来获取相应的PUD表项。这里会通过pud_index()宏来计算索引值,计算方法和pgd_index()函数类似,最终使用虚拟地址的bit[38~30]位来做索引值。
(3)接下来以PUD_SIZE(即1<<30, 1GB)为步长,通过while循环来设置下一级页表。
(4)use_1G_block()函数会判断是否使用1GB大小的block来映射?当这里要映射的大小内存块正好是PUD_SIZE,那么只需要映射到PUD就好了,接下来的PMD和PTE页表等到真正需要使用时再映射,通过set_pud()函数来设置相应的PUD表项。
(5)如果use_1G_block()函数判断不能通过1GB大小来映射,那么就需要调用alloc_init_pmd()函数来进行下一级页表的映射。

      static void alloc_init_pmd(struct mm_struct *mm, pud_t *pud,
                              unsigned long addr, unsigned long end,
                              phys_addr_t phys, pgprot_t prot,
                              void *(*alloc)(unsigned long size))
      {
            pmd_t *pmd;
            unsigned long next;
            /*
            * Check for initial section mappings in the pgd/pud and remove them.
            */
            if (pud_none(*pud) || pud_sect(*pud)) {
                pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));
                if (pud_sect(*pud)) {
                      /*
                      * need to have the 1G of mappings continue to be
                      * present
                      */
                      split_pud(pud, pmd);
                }
                pud_populate(mm, pud, pmd);
                flush_tlb_all();
            }

            pmd = pmd_offset(pud, addr);
            do {
                next = pmd_addr_end(addr, end);
                /* try section mapping first */
                if (((addr | next | phys) & ~SECTION_MASK) == 0) {
                      pmd_t old_pmd =*pmd;
                      set_pmd(pmd, __pmd(phys |
                                pgprot_val(mk_sect_prot(prot))));
                      /*
                      * Check for previous table entries created during
                      * boot (__create_page_tables) and flush them.
                      */
                      if (! pmd_none(old_pmd)) {
                          flush_tlb_all();
                          if (pmd_table(old_pmd)) {
                                phys_addr_t table = __pa(pte_offset_map(&old_pmd, 0));
                                if (! WARN_ON_ONCE(slab_is_available()))
                                      memblock_free(table, PAGE_SIZE);
                              }
                        }
                    } else {
                        alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),
                                      prot, alloc);
                    }
                    phys += next - addr;
              } while (pmd++, addr = next, addr ! = end);
          }

alloc_init_pmd()函数用于配置PMD页表,主要做如下事情。

(1)首先判断PUD页表项的内容是否为空?如果为空,表示PUD指向的下一级页表PMD不存在,需要动态分配PMD页表。分配PTRS_PER_PMD个页表项,即512个,然后通过pud_populate()来设置pud页表项。
(2)通过pmd_offset()宏来获取相应的PUD表项。这里会通过pud_index()来计算索引值,计算方法和pgd_index()函数类似,最终使用虚拟地址的bit[29:21]位来做索引值。
(3)接下来以PMD_SIZE(即1<<21, 2MB)为步长,通过while循环来设置下一级页表。
(4)如果虚拟区间的开始地址addr和结束地址next,以及物理地址phys都与SECTION_SIZE(2MB)大小对齐,那么直接设置PMD页表项,不需要映射下一级页表。下一级页表等到需要用时再映射也来得及,所以这里直接通过set_pmd()设置PMD页表项。
(5)如果映射的内存不是和SECTION_SIZE对齐的,那么需要通过alloc_init_pte()函数来映射下一级PTE页表。

      static void alloc_init_pte(pmd_t *pmd, unsigned long addr,
                              unsigned long end, unsigned long pfn,
                              pgprot_t prot,
                              void *(*alloc)(unsigned long size))
      {
            pte_t *pte;

            if (pmd_none(*pmd) || pmd_sect(*pmd)) {
                pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
                if (pmd_sect(*pmd))
                      split_pmd(pmd, pte);
                __pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);
                flush_tlb_all();
            }
            BUG_ON(pmd_bad(*pmd));

            pte = pte_offset_kernel(pmd, addr);
            do {
                set_pte(pte, pfn_pte(pfn, prot));
                pfn++;
            } while (pte++, addr += PAGE_SIZE, addr ! = end);
      }

PTE页表是4级页表的最后一级,alloc_init_pte()配置PTE页表项。

(1)首先判断PMD表项的内容是否为空?如果为空,说明下一级页表不存在,需要动态分配512个页表项,然后通过__pmd_populate()函数来设置PMD页表项。
(2)通过pte_offset_kernel()宏来索引到相应的PTE页表项。索引值可以通过pte_index()来计算,最终会使用虚拟地址bit[20:12]来做索引值。
(3)接下来以PAGE_SIZE即4KB大小为步长,通过while循环来设置PTE页表项。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!