内存-页表结构详细说明

1. 内存地址的分解

我们知道linux采用了分页机制,通常采用四级页表,页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)。如下:
在这里插入图片描述

其含义定义在arch/arm64/include/asm/pgtable-hwdef.h文件中:

CONFIG_PGTABLE_LEVELS				//页表级数

//offset
#define PAGE_SHIFT		CONFIG_ARM64_PAGE_SHIFT			//页offset,为12
#define PAGE_SIZE		(_AC(1, UL) << PAGE_SHIFT)		//页大小,为4k

//PTE
#define PTRS_PER_PTE		(1 << (PAGE_SHIFT - 3))		//PTE位数,为9

#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n)	((PAGE_SHIFT - 3) * (4 - (n)) + 3)

//PMD
#if CONFIG_PGTABLE_LEVELS > 2
#define PMD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(2)
#define PMD_SIZE		(_AC(1, UL) << PMD_SHIFT)		//2M
#define PMD_MASK		(~(PMD_SIZE-1))
#define PTRS_PER_PMD		PTRS_PER_PTE				//PMD位数,为9
#endif

//PUD
#if CONFIG_PGTABLE_LEVELS > 3
#define PUD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(1)
#define PUD_SIZE		(_AC(1, UL) << PUD_SHIFT)		//1G
#define PUD_MASK		(~(PUD_SIZE-1))
#define PTRS_PER_PUD		PTRS_PER_PTE				//PUD位数,为9
#endif

//PGD
#define PGD_SIZE	(PTRS_PER_PGD * sizeof(pgd_t))  //4096 
#define VA_BITS			(CONFIG_ARM64_VA_BITS)		//4级页表为48,3级页表为39
#define PGDIR_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
#define PGDIR_SIZE		(_AC(1, UL) << PGDIR_SHIFT)		
#define PGDIR_MASK		(~(PGDIR_SIZE-1))
#define PTRS_PER_PGD		(1 << (VA_BITS - PGDIR_SHIFT))

看到这里我们知道4级页表虚拟地址为48位,页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)都是9位,offset是12位。但是有些系统是使用3级页表的,那么他的虚拟地址为39位,页全局目录(PGD),页中间目录(PMD),页表(PTE)都是9位,offset是12位,也就是说,少了页上级目录(PUD)。其实我们使用3级页表就够了,39为的虚拟地址可以操作512g的内存,是完全够用的。我们国产操作系统就是使用3级页表的。最后说明一下。无论是3级页表还是4级页表,我们的物理地址都是48位的。

1.2 页表的数据结构

我们已经确立了页表项的数目,但没有定义其结构。内核提供了4个数据结构(定义在 arch/arm64/include/asm/pgtable-types.h中)来表示页表项的结构。

typedef struct { pteval_t pte; } pte_t;
#define pte_val(x)	((x).pte)
#define __pte(x)	((pte_t) { (x) } )

#if CONFIG_PGTABLE_LEVELS > 2
typedef struct { pmdval_t pmd; } pmd_t;
#define pmd_val(x)	((x).pmd)
#define __pmd(x)	((pmd_t) { (x) } )
#endif

#if CONFIG_PGTABLE_LEVELS > 3
typedef struct { pudval_t pud; } pud_t;
#define pud_val(x)	((x).pud)
#define __pud(x)	((pud_t) { (x) } )
#endif

typedef struct { pgdval_t pgd; } pgd_t;
#define pgd_val(x)	((x).pgd)
#define __pgd(x)	((pgd_t) { (x) } )

typedef struct { pteval_t pgprot; } pgprot_t;
#define pgprot_val(x)	((x).pgprot)
#define __pgprot(x)	((pgprot_t) { (x) } )

typedef u64 pteval_t;
typedef u64 pmdval_t;
typedef u64 pudval_t;
typedef u64 pgdval_t;

从代码中我们看到了以下东西:

  1. 页表项的结构
    pgd_t 用于全局页目录项。
    pud_t 用于上层页目录项。
    pmd_t 用于中间页目录项。
    pte_t 用于直接页表项。
页表项的结构pgd_t 用于全局页目录项。pud_t 用于上层页目录项。pmd_t 用于中间页目录项。pte_t 用于直接页表项。

1.3 特定于页表的信息

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

/*
 * Level 3 descriptor (PTE).
 */
#define PTE_TYPE_MASK		(_AT(pteval_t, 3) << 0)
#define PTE_TYPE_FAULT		(_AT(pteval_t, 0) << 0)
#define PTE_TYPE_PAGE		(_AT(pteval_t, 3) << 0)
#define PTE_TABLE_BIT		(_AT(pteval_t, 1) << 1)
#define PTE_USER		(_AT(pteval_t, 1) << 6)		/* AP[1] */
#define PTE_RDONLY		(_AT(pteval_t, 1) << 7)		/* AP[2] */
#define PTE_SHARED		(_AT(pteval_t, 3) << 8)		/* SH[1:0], inner shareable */
#define PTE_AF			(_AT(pteval_t, 1) << 10)	/* Access Flag */
#define PTE_NG			(_AT(pteval_t, 1) << 11)	/* nG */
#define PTE_DBM			(_AT(pteval_t, 1) << 51)	/* Dirty Bit Management */
#define PTE_CONT		(_AT(pteval_t, 1) << 52)	/* Contiguous range */
#define PTE_PXN			(_AT(pteval_t, 1) << 53)	/* Privileged XN */
#define PTE_UXN			(_AT(pteval_t, 1) << 54)	/* User XN */
#define PTE_HYP_XN		(_AT(pteval_t, 1) << 54)	/* HYP XN */

/*
 * Level 2 descriptor (PMD).
 */
#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, 3) << 0)
#define PMD_TYPE_SECT		(_AT(pmdval_t, 1) << 0)
#define PMD_TABLE_BIT		(_AT(pmdval_t, 1) << 1)

#define PUD_TYPE_TABLE		(_AT(pudval_t, 3) << 0)
#define PUD_TABLE_BIT		(_AT(pudval_t, 1) << 1)
#define PUD_TYPE_MASK		(_AT(pudval_t, 3) << 0)
#define PUD_TYPE_SECT		(_AT(pudval_t, 1) << 0)

还有一些用于处理内存页的体系结构相关状态的函数:

函数描述
pte_present页在内存中吗
pte_read从用户空间可以读取该页吗
pte_write可以写入到该页吗
pte_exec该页中的数据可以作为二进制代码执行吗
pte_dirty页是脏的吗?其内容是否修改过
pte_file该页表项属于非线性映射吗
pte_young访问位(通常是_ PAGE_ACCESS )设置了吗
pte_rdprotect清除该页的读权限
pte_wrprotect清除该页的写权限
pte_exprotect清除执行该页中二进制数据的权限
pte_mkread设置读权限
pte_mkwrite设置写权限
pte_mkexec允许执行页的内容
pte_mkdirty将页标记为脏
pte_mkclean“清除”页,通常是指清除 _PAGE_DIRTY 位
pte_mkyoung设置访问位,在大多数体系结构上是 _PAGE_ACCESSED
pte_mkold清除访问位
以上函数一般是用于设置、删除、查询某个特定的属性(例如,页的写权限)。

1.4 页表的格式

ARM64 处理器把页表称为转换表( translation table ),最多 4 级。分别是页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)。ARM64 处理器把表项称为描述符( descriptor ),使用 64 位的长描述符格式。描述符的第 0 位指示描述符是不是有效的: 0 表示无效, 1 表示有效;第 1 位指定描述符类型。

  1. 页目录项,就是在页全局目录(PGD),页上级目录(PUD),页中间目录(PMD)中, 0 表示块( block )描述符, 1 表示表( table )描述符。块描述符存放一个内存块(即巨型页)的起始地址,表描述符存放下一级转换表的地址。
  2. 页目表项,就是页表(PTE), 0 表示保留描述符, 1 表示页描述符。

1.4.1 页目录项

  1. 无效描述符:无效描述符的第 0 位是 0 。
    在这里插入图片描述

  2. 块描述符:块描述符的最低两位是 01。
    在这里插入图片描述

  3. 表描述符:表描述符的最低两位是 11。
    在这里插入图片描述

1.4.2 页目表项

  1. 无效描述符:无效描述符的第 0 位是 0。
    在这里插入图片描述

  2. 保留描述符:保留描述符的最低两位是 01。
    在这里插入图片描述

  3. 页描述符:页描述符的最低两位是 11
    在这里插入图片描述

无论是页目录项的块描述符还是页表项的页描述符,都是表示一个整体的内存;块描述符一般用于巨型页,而页描述符用于普通页。
他们的内存属性被拆分成一个高属性块和一个低属性块,具体可以看下面的图和表:
在这里插入图片描述

位数含义
59 ~ 62基于页的硬件属性( Page-Based Hardware Attributes ),如果没有实现 ARMv8.2-TTPBHA ,忽略。
55 ~ 58保留给软件使用。
54在异常级别 0 ,表示 UXN ( Unprivileged execute-Never ),即不允许异常级别0 执行内核代码;在其他异常级别,表示 XN ( execute-Never ),不允许执行。
53PXN ( Privileged execute-Never ),不允许在特权级别(即异常级别 1/2/3 )执行。
52连续( Contiguous ),指示这条转换表项属于一个连续表项集合,一个连续表项集合可以被缓存在一条 TLB 表项里面
51脏位修饰符( Dirty Bit Modifier , DBM ),指示页或内存块是否被修改过。
11非全局( not global , nG )。 nG 位是 1 ,表示转换不是全局的,是进程私有的,有一个关联的地址空间标识符( Address Space Identifier , ASID ); nG 位是 0 ,表示转换是全局的,是所有进程共享的,内核的页或内存块是所有进程共享的。
10访问标志( Access Flag , AF ),指示页或内存块自从相应的转换表描述符中的访问标志被设置为 0 以后是否被访问过。
8 ~ 9可共享性( SHareability , SH ), 00 表示不共享, 01 是保留值, 10 表示外部共享, 11 表示内部共享。
6 ~ 7AP[2:1] ( Data Access Permissions ,数据访问权限)。AP[2] 用来选择只读或读写, 1 表示只读, 0 表示读写; AP[1] 用来选择是否允许异常级别 0 访问, 1 表示允许异常级别 0 访问, 0 表示不允许异常级别 0 访问。
5非安全( Non-Secure , NS )。对于安全状态的内存访问,指定输出地址在安全地址映射还是在非安全地址映射。
2 ~ 4内存属性索引( memory attributes index , AttrIndx ),指定寄存器 MAIR_ELx中内存属性字段的索引,内存属性间接寄存器( Memory Attribute Indirection Register ,MAIR_ELx )有 8 个 8 位内存属性字段: Attr , n 等于 0 ~ 7 。

看到这里我们已经看到了内存的读写和执行的权限是AP[2:1] 控制的,但是对于内存的cacheable和sharealbe属性,并不是直接保存在页表中,而是在2-4位的AttrIndx去MAIR_EL1寄存器找到对应的属性。
MAIR_EL1寄存器64位,每8位一个属性组,一共8个属性组。AttrIndx的含义就是选择某一个属性组的属性。MAIR_EL1会在linux内核初始化,每一个属性组记录的可以设备内存还是普通内存。如果是设备内存会记录GRE情况。如果是普通内存会记录读写是否缓存和内存shareable情况。

1.5 内存的属性

上面我们看了页表,无论是页表项还是页目录项的表示内存属性索引,表示内存属性, 内存属性并没有直接存放在页表项中,而是存放在MAIR_EL1中。页表项中使用2 ~ 4位这么一个3位的索引值来查找MAIR_EL1。其含义我们在这里说说。
ARMv8架构处理器主要提供两种类型的内存属性,分别是普通(normal)内存和设备(device)内存。
普通内存包括物理内存和只读存储器;普通内存是弱一致性的,没有额外的约束,可以提供最高的内存访问性能。通常代码段、数据段以及其他数据都会放在普通内存中。普通内存可以让处理器做很多的优化,如分支预测、数据预取、高速缓存行预取和填充、乱序加载等硬件优化。设备内存指分配给外围设备寄存器的物理地址区域。处理器访问设备内存会有很多限制,如不能进行预测访问等。
设备内存是严格按照指令顺序来执行的。通常设备内存留给设备来访问。若系统中所有内存都设置为设备内存,就会有很大的副作用。

他们的属性都定义在arch/arm64/include/asm/memory.h文件中:

#define MT_NORMAL               0
#define MT_NORMAL_TAGGED        1
#define MT_NORMAL_NC            2
#define MT_NORMAL_WT            3
#define MT_DEVICE_nGnRnE        4
#define MT_DEVICE_nGnRE         5
#define MT_DEVICE_GRE           6

上面的代码前4个是描述普通内存的内存属性,后面3个是描述设备内存的内存属性:

  1. MT_NORMAL:普通内存属性,默认使用cache和高速缓存的回写策略为写回(write back )策略。
  2. MT_NORMAL_TAGGED :属性上跟MT_NORMAL一样,但是这种内存我们会记录额外的标签信息。
  3. MT_NORMAL_NC:普通内存属性,关闭高速缓存,其中NC是Non-Cacheable的意思。
  4. MT_NORMAL_WT :普通内存属性,高速缓存的回写策略为直写(write through)策略
  5. MT_DEVICE_nGnRnE:设备内存属性,不支持聚合操作,不支持指令重排,不支持提前写应答。
  6. MT_DEVICE_nGnRE :设备内存属性,不支持聚合操作,不支持指令重排,支持提前写应答 。
  7. MT_DEVICE_GRE:设备内存属性,支持聚合操作,支持指令重排,支持提前写应答。

我们已经知道了内存属性大概情况了,我们也知道这属性是记录在MAIR_EL1寄存器中,那么我们看看他们是怎么初始化MAIR_EL1的,他的是在linux内核启动的第一时间初始化的,函数调用路劲为_head -> primary_entry -> __cpu_setup :

SYM_FUNC_START(__cpu_setup)
...
	/*
	 * Memory region attributes
	 */
	mov_q	x5, MAIR_EL1_SET	//设置nGnRnE等内存属性
	msr	mair_el1, x5	//对内存的8个区域写入属性
...
SYM_FUNC_END(__cpu_setup)

#define MAIR_EL1_SET                                                    \
        (MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) |      \
         MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE, MT_DEVICE_nGnRE) |        \
         MAIR_ATTRIDX(MAIR_ATTR_DEVICE_GRE, MT_DEVICE_GRE) |            \
         MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC, MT_NORMAL_NC) |              \
         MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL) |                    \
         MAIR_ATTRIDX(MAIR_ATTR_NORMAL_WT, MT_NORMAL_WT) |              \
         MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL_TAGGED))

#define MAIR_ATTR_DEVICE_nGnRnE         UL(0x00)
#define MAIR_ATTR_DEVICE_nGnRE          UL(0x04)
#define MAIR_ATTR_DEVICE_GRE            UL(0x0c)
#define MAIR_ATTR_NORMAL_NC             UL(0x44)
#define MAIR_ATTR_NORMAL_WT             UL(0xbb)
#define MAIR_ATTR_NORMAL_TAGGED         UL(0xf0)
#define MAIR_ATTR_NORMAL                UL(0xff)
#define MAIR_ATTR_MASK                  UL(0xff)

MAIR_EL1寄存器分为8段,每一段占用8个bit,每一段都可以用于描述不同的内存属性,我们只用了前7段,分别是前面的7中内存属性。每一段的8个bit的数据含义:
在这里插入图片描述
从上图我们知道普通内存高4位表示外部共享,低4位表示内部共享。这个4个bit的含义:

第n位含义
0缓存写申请策略,0表示写不申请;1表示写申请
1缓存读申请策略,0表示读不申请;1表示读申请
2写策略,0表示写穿;1表示写回
3缓存时间,0表示永久占用;1表示随时可以flush
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小坚学Linux

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值