欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

【原创】ARMv8 MMU及Linux页表映射

程序员文章站 2023-11-02 18:44:58
背景 By 鲁迅 By 高尔基 说明: 1. Kernel版本:4.14 2. ARM64处理器,Contex A53,双核 3. 使用工具:Source Insight 3.5, Visio 1. 介绍 要想理解好Linux的页表映射,MMU的机制是需要去熟悉的,因此将这两个模块放到一起介绍。 关 ......

背景

  • read the fucking source code! --by 鲁迅
  • a picture is worth a thousand words. --by 高尔基

说明:

  1. kernel版本:4.14
  2. arm64处理器,contex-a53,双核
  3. 使用工具:source insight 3.5, visio

1. 介绍

要想理解好linux的页表映射,mmu的机制是需要去熟悉的,因此将这两个模块放到一起介绍。
关于armv8 mmu的相关内容,主要参考文档:《arm cortex-a series programmer’s guide for armv8-a》

2. armv8 mmu

2.1 mmu/tlb/cache概述

  1. mmu:完成的工作就是虚拟地址到物理地址的转换,可以让系统中的多个程序跑在自己独立的虚拟地址空间中,相互不会影响。程序可以对底层的物理内存一无所知,物理地址可以是不连续的,但是不妨碍映射连续的虚拟地址空间。
  2. tlbmmu工作的过程就是查询页表的过程,页表放置在内存中时查询开销太大,因此专门有一小片访问更快的区域用于存放地址转换条目,用于提高查找效率。当页表内容有变化的时候,需要清除tlb,以防止地址映射出错。
  3. cache:处理器和存储器之间的缓存机制,用于提高访问速率,在armv8上会存在多级cache,其中l1 cache分为指令cache数据cache,在cpu core的内部,支持虚拟地址寻址;l2 cache容量更大,同时存储指令和数据,为多个cpu core共用,这多个cpu core也就组成了一个cluster

下图浅黄色部分描述的就是一个地址转换的过程。
【原创】ARMv8 MMU及Linux页表映射

由于上图没有体现出l1和l2 cachemmu的关系,所以再来一张图吧:
【原创】ARMv8 MMU及Linux页表映射

那具体是怎么访问的呢?再来一张图:
【原创】ARMv8 MMU及Linux页表映射

2.2 虚拟地址到物理地址的转换

虚拟地址到物理地址的映射通过查表的机制来实现,armv8中,kernel space的页表基地址存放在ttbr1_el1寄存器中,user space页表基地址存放在ttbr0_el0寄存器中,其中内核地址空间的高位为全1,(0xffff0000_00000000 ~ 0xffffffff_ffffffff),用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000ffff_ffffffff)
【原创】ARMv8 MMU及Linux页表映射

armv8中:

  • 虚拟地址支持
    64位虚拟地址中,并不是所有位都用上,除了高16位用于区分内核空间和用户空间外,有效位的配置可以是:36, 39, 42, 47。这可决定linux内核中地址空间的大小。比如我使用的内核中有效位配置为config_arm64_va_bits=39,用户空间地址范围:0x00000000_00000000 ~ 0x0000007f_ffffffff,大小为512g,内核空间地址范围:0xffffff80_00000000 ~ 0xffffffff_ffffffff,大小为512g。

  • 页面大小支持
    支持3种页面大小:4kb, 16kb, 64kb

  • 页表支持
    支持至少两级页表,至多四级页表,level 0 ~ level 3

结合有效虚拟地址位, 页面大小,页表的级数,可以组合成不同的页表映射方式。
我使用的内核配置为:39位有效位,4kb大小页面,3级页表,所以我会以这个组合来介绍。
在armv8的手册中刚好找到了下图,描述了整个translation的过程,简直完美:
【原创】ARMv8 MMU及Linux页表映射

  1. 虚拟地址[63:39]用于区分内核空间与用户空间,从而选择不同的ttbrn寄存器来获取level 1页表基地址
  2. 虚拟地址[38:30]放置level 1页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取level 2页表基地址;
  3. 虚拟地址[29:21]level 2页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取level 3页表基地址;
  4. 虚拟地址[20:12]level 3页表中的索引,从而找到对应的描述符地址并获取描述符内容,根据描述符中的内容获取物理地址的高36位,以4k地址对齐;
  5. 虚拟地址[11:0]放置的是物理地址的偏移,结合获取的物理地址高位,最终得到物理地址。

讲到这里还没有完,是时候看一下table descriptor了,也就是页表中存放的内容,有以下四种类型:
【原创】ARMv8 MMU及Linux页表映射

类型有低两位来决定,其中level 0中的table descriptor只能输出level 1页表的地址,level 3中的table descriptor只能输出block addresses
看到图中的attributes了吗,这些可以用于memory的权限控制,memory ordering,cache policy的操作等。

在armv8中,与页表相关的寄存器有:tcr_el1, ttbrx_el1.

3. linux页表映射

3.1 linux页表基本操作

看过《深入理解linux内核》的同学应该很熟悉下边这张图片,linux的分页模式(图中以x86为例,页表基地址由cr3寄存器指定):
【原创】ARMv8 MMU及Linux页表映射

在linux内核中支持4级页表的模型,同时适用于32位和64位系统。

那么armv8与linux内核是怎么结合的呢?以我实际使用的设置(39位有效位,4kb大小页面,3级页表)为例,如下图所示:
【原创】ARMv8 MMU及Linux页表映射

基本上内核中关于页表的操作都会围绕着上图进行操作,似乎脱离了代码有点不太合适,那么就来一波fucking source code解析吧,主要讲讲各类page table相关的api。

代码路径:
arch/arm64/include/asm/pgtable-types.h:定义pgd_t, pud_t, pmd_t, pte_t等类型;
arch/arm64/include/asm/pgtable-prot.h:针对页表中entry中的权限内容设置;
arch/arm64/include/asm/pgtable-hwdef.h:主要包括虚拟地址中pgd/pmd/pud等的划分,这个与虚拟地址的有效位及分页大小有关,此外还包括硬件页表的定义, tcr寄存器中的设置等;
arch/arm64/include/asm/pgtable.h:页表设置相关;

在这些代码中可以看到,

  • config_pgtable_levels=4时:pgd-->pud-->pmd-->pte;
  • config_pgtable_levels=3时,没有pud页表:pgd(pud)-->pmd-->pte;
  • config_pgtable_levels=2时,没有pudpmd页表:pgd(pud, pmd)-->pte

常用的宏定义
【原创】ARMv8 MMU及Linux页表映射

页表处理

/*描述各级页表中的页表项*/
typedef struct { pteval_t pte; } pte_t;
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pgdval_t pgd; } pgd_t;

/*  将页表项类型转换成无符号类型 */
#define pte_val(x)  ((x).pte)
#define pmd_val(x)  ((x).pmd)
#define pud_val(x)  ((x).pud)
#define pgd_val(x)  ((x).pgd)

/*  将无符号类型转换成页表项类型 */
#define __pte(x)    ((pte_t) { (x) } )
#define __pmd(x)    ((pmd_t) { (x) } )
#define __pud(x)    ((pud_t) { (x) } )
#define __pgd(x)    ((pgd_t) { (x) } )

/* 获取页表项的索引值 */
#define pgd_index(addr)     (((addr) >> pgdir_shift) & (ptrs_per_pgd - 1))
#define pud_index(addr)     (((addr) >> pud_shift) & (ptrs_per_pud - 1))
#define pmd_index(addr)     (((addr) >> pmd_shift) & (ptrs_per_pmd - 1))
#define pte_index(addr)     (((addr) >> page_shift) & (ptrs_per_pte - 1))

/*  获取页表中entry的偏移值 */
#define pgd_offset(mm, addr)    (pgd_offset_raw((mm)->pgd, (addr)))
#define pgd_offset_k(addr)  pgd_offset(&init_mm, addr)
#define pud_offset_phys(dir, addr)  (pgd_page_paddr(*(dir)) + pud_index(addr) * sizeof(pud_t))
#define pud_offset(dir, addr)       ((pud_t *)__va(pud_offset_phys((dir), (addr))))
#define pmd_offset_phys(dir, addr)  (pud_page_paddr(*(dir)) + pmd_index(addr) * sizeof(pmd_t))
#define pmd_offset(dir, addr)       ((pmd_t *)__va(pmd_offset_phys((dir), (addr))))
#define pte_offset_phys(dir,addr)   (pmd_page_paddr(read_once(*(dir))) + pte_index(addr) * sizeof(pte_t))
#define pte_offset_kernel(dir,addr) ((pte_t *)__va(pte_offset_phys((dir), (addr))))

3.2 head.s中的页表映射

3.2.1 idmap_pg_dir和swapper_pg_dir临时页表

是时候来个实例分析了,看看页表的创建过程,代码路径:arch/arm64/kernel/head.s
内核启动过程中,在真正的物理内存尚未添加进系统,以及页表还未初始化之前,为了保证系统能正常运行,需要建立两个临时全局页表:idmap_pg_dirswapper_pg_dir
其中两个全局页表的定义在arch/arm64/kernel/vmlinux.lds.s中,放置在bss段之后:

    . = align(page_size);
    idmap_pg_dir = .;
    . += idmap_dir_size;
    swapper_pg_dir = .;
    . += swapper_dir_size;
/*  定义了连续的几个页,分别存放pgd,pmd,pte等,连续在一起,这个也是head.s中填充的 */
#define swapper_dir_size    (swapper_pgtable_levels * page_size)
#define idmap_dir_size      (idmap_pgtable_levels * page_size)
  • idmap_pg_dir
    从名字可以看出,identify map,也就是物理地址和虚拟地址是相等的。为什么需要这么一个映射呢?我们都知道在mmu打开之前,cpu访问的都是物理地址,那么当mmu打开后访问的就是虚拟地址了,这段页表的映射就是从cpu到打开mmu之前的这段代码物理地址的映射,防止开启mmu后,无法获取页表。可以从system.map文件中查看这些代码:
    【原创】ARMv8 MMU及Linux页表映射

  • swapper_pg_dir
    linux内核编译后,kernel image是需要进行映射的,包括text,data等各种段。

3.2.2 页表创建

head.s中,创建页表相关的有三个宏:

  1. create_pgd_entry
/*
 * macro to populate the pgd (and possibily pud) for the corresponding
 * block entry in the next level (tbl) for the given virtual address.
 *
 * preserves:   tbl, next, virt
 * corrupts:    tmp1, tmp2
 */
    .macro  create_pgd_entry, tbl, virt, tmp1, tmp2
    create_table_entry \tbl, \virt, pgdir_shift, ptrs_per_pgd, \tmp1, \tmp2
#if swapper_pgtable_levels > 3
    create_table_entry \tbl, \virt, pud_shift, ptrs_per_pud, \tmp1, \tmp2
#endif
#if swapper_pgtable_levels > 2
    create_table_entry \tbl, \virt, swapper_table_shift, ptrs_per_pte, \tmp1, \tmp2
#endif
    .endm

上述函数主要是调用create_table_entry,由于swapper_pgtables配置为3,因此相当于创建了pgd和pmd两级页表,此处需要注意一点,create_table_entry函数执行后,tbl参数会自动加上page_size,也就是说pgd和pmd两级页表是物理连续的。

  1. create_block_map
/*
 * macro to populate block entries in the page table for the start..end
 * virtual range (inclusive).
 *
 * preserves:   tbl, flags
 * corrupts:    phys, start, end, pstate
 */
    .macro  create_block_map, tbl, flags, phys, start, end
    lsr \phys, \phys, #swapper_block_shift
    lsr \start, \start, #swapper_block_shift
    and \start, \start, #ptrs_per_pte - 1   // table index
    orr \phys, \flags, \phys, lsl #swapper_block_shift  // table entry
    lsr \end, \end, #swapper_block_shift
    and \end, \end, #ptrs_per_pte - 1       // table end index
9999:   str \phys, [\tbl, \start, lsl #3]       // store the entry
    add \start, \start, #1          // next entry
    add \phys, \phys, #swapper_block_size       // next block
    cmp \start, \end
    b.ls    9999b
    .endm

上述函数主要是往block中填充pte entry,真正创建虚拟地址到物理地址的映射,映射区域:start ~ end

  1. create_table_entry
/*
 * macro to create a table entry to the next page.
 *
 *  tbl:    page table address
 *  virt:   virtual address
 *  shift:  #imm page table shift
 *  ptrs:   #imm pointers per table page
 *
 * preserves:   virt
 * corrupts:    tmp1, tmp2
 * returns: tbl -> next level table page address
 */
    .macro  create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
    lsr \tmp1, \virt, #\shift
    and \tmp1, \tmp1, #\ptrs - 1    // table index
    add \tmp2, \tbl, #page_size
    orr \tmp2, \tmp2, #pmd_type_table   // address of next table and entry type
    str \tmp2, [\tbl, \tmp1, lsl #3]
    add \tbl, \tbl, #page_size      // next level table page
    .endm

上述函数创建页表项,并且返回下一个level的页表地址。

上述三个孤立的函数并不直观,所以,图来了:
【原创】ARMv8 MMU及Linux页表映射

总体来说,页表的创建过程相对来说还是比较易懂的,掌握好几级页表及各级页表index所占的位域,此外熟悉各个level页表中entry的格式,理解起来就会顺畅很多了。

一抠细节深似海,点到为止,防止一叶障目不见泰山,收工!