ARMv8页表

内核页表,即是0号进程(静态宏定义创建init_task, 然后演变成idle进程, comm一直是‘swapper’)。

其task_struct->mm 为空,task_struct->active_mm  为init_mm。 

记住内核页表是0号进程的active_mm->pgd,  不是1号init进程的。

 

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),
        .user_ns        = &init_user_ns,
        INIT_MM_CONTEXT(init_mm)
};

 

0号进程

/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

 

#define INIT_TASK(tsk)  \
{                                                                       \
        INIT_TASK_TI(tsk)                                               \
        .state          = 0,                                            \
        .stack          = init_stack,                                   \
        .usage          = ATOMIC_INIT(2),                               \
        .flags          = PF_KTHREAD,                                   \
        .prio           = MAX_PRIO-20,                                  \
        .static_prio    = MAX_PRIO-20,                                  \
        .normal_prio    = MAX_PRIO-20,                                  \
        .policy         = SCHED_NORMAL,                                 \
        .cpus_allowed   = CPU_MASK_ALL,                                 \
        .nr_cpus_allowed= NR_CPUS,                                      \
        .mm             = NULL,                                         \
        .active_mm      = &init_mm,                                     \

        .comm           = INIT_TASK_COMM,

}

#define INIT_TASK_COMM "swapper"

 

 

2.  TLB 

 

虚拟地址到物理地址的映射通过查表的机制来实现,ARMv8中,Kernel Space的页表基地址存放在TTBR1_EL1寄存器中,User Space页表基地址存放在TTBR0_EL0寄存器中,其中内核地址空间的高位为全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF),用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)

 

TLBMMU工作的过程就是查询页表的过程,页表放置在内存中时查询开销太大,因此专门有一小片访问更快的区域用于存放地址转换条目,用于提高查找效率。当页表内容有变化的时候,需要清除TLB,以防止地址映射出错。

发生TLB miss后,hardware page table walk自动启动开始扫描内存中的pagetable,若找到相应PTE(page table entry),则自动完成TLB entry的重填工作;如果找不到,则发出一个page fault异常,然后OS接管处理page fault。内核中有do_page_fault函数,该函数从硬盘中调换页面进内存,更新页表,然后重新执行发生TLB miss的那条指令,hardware page table walk重新执行,完成TLB重填的工作
 

 

 

下面转载自  https://www.cnblogs.com/LoyenWang/p/11406693.html

说明:

  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

下图浅黄色部分描述的就是一个地址转换的过程。

由于上图没有体现出L1和L2 CacheMMU的关系,所以再来一张图吧:

那具体是怎么访问的呢?再来一张图:

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

虚拟地址到物理地址的映射通过查表的机制来实现,ARMv8中,Kernel Space的页表基地址存放在TTBR1_EL1寄存器中,User Space页表基地址存放在TTBR0_EL0寄存器中,其中内核地址空间的高位为全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF),用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)

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的过程,简直完美:

  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了,也就是页表中存放的内容,有以下四种类型:

类型有低两位来决定,其中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寄存器指定):

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

那么ARMv8与Linux内核是怎么结合的呢?以我实际使用的设置(39位有效位,4KB大小页面,3级页表)为例,如下图所示:

基本上内核中关于页表的操作都会围绕着上图进行操作,似乎脱离了代码有点不太合适,那么就来一波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

常用的宏定义

页表处理

/*描述各级页表中的页表项*/
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文件中查看这些代码:

  • 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的页表地址。

上述三个孤立的函数并不直观,所以,图来了:

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

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

 

 

 

 

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ARMv8架构参考手册是由ARM公司发布的一份权威文档,用于详细描述ARMv8架构的技术细节和功能特性。ARMv8ARM公司设计的一种64位指令集架构(ISA),被广泛应用在现代的处理器中。 该参考手册包含了ARMv8架构的各个方面,包括指令集、内存管理、异常处理、乱序执行等。它为软件开发者、系统设计师和硬件工程师提供了丰富的信息和指导,帮助他们充分理解和利用ARMv8架构的优势。 首先,该手册详细介绍了ARMv8指令集的设计和编码规范。它列出了所有指令的操作码、格式和功能,并提供了指令的精确说明,使开发者可以编写高效的汇编代码。 其次,手册解释了各种内存管理技术和机制,如虚拟内存、TLB和页表。这对于操作系统和虚拟机的开发者来说尤为重要,因为它们需要理解和控制内存的分配、映射和访问。 此外,手册还介绍了ARMv8架构中的异常处理和中断机制。它详细说明了不同类型的异常和中断的处理流程和优先级,以及异常处理程序的编写和调用方法。 最后,手册还讨论了性能优化和乱序执行的相关技术。这对于系统设计师和硬件工程师来说非常重要,因为他们需要了解如何通过调整指令顺序和预测执行路径来提高处理器的性能和效率。 总之,ARMv8架构参考手册是一份重要的技术文档,对于任何与ARMv8架构相关的软件或硬件开发者来说都是必备的参考资料。它提供了全面的理论和实践指导,帮助开发者充分利用ARMv8架构的优势,并优化他们的产品性能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值