arm-linux 内存管理之一级页表及二级页表

arm32 一个 vma 中 有 两个 index //  armv6 RM ref P730
	L1(一级页表,即页目录表)index 为 12,所以一级页表的大小为 2^12*4B=16KB
	L2(二级页表,页表)index为8,所以二级页表的大小为2^8*4B=1KB
	// 所以一套(一级和二级)页表的"最大"大小为
	// 16KB 		+ 16KB/4B * 1KB = 4MB + 16KB
	// 一级页表大小  + 所有的 二级页表大小
arm32 硬件上 有 TTBR0 TTBR1 寄存器,但是linux不使用 Linux内核只使用TTBR1寄存器 // CONFIG_ARM_LPAE 的时候才会用 TTBR0
	// https://www.tiehichi.site/2021/11/15/ARMv7%E8%BF%9B%E7%A8%8B%E9%A1%B5%E8%A1%A8/
CP15 C2 用于 存储 页目录表的物理地址(基址)

arm-linux 内存管理之一级页表及二级页表_第1张图片

  • X 可为0
    arm-linux 内存管理之一级页表及二级页表_第2张图片

  • 一级页表和二级页表的位置

我们把 一套 (一级页表和二级页表) 称为 一套表,一套表对应一个pgd
如果有N个用户进程,那么有多少套表呢?
答案是N+1套表
	N个表
		对应N个用户进程,每个用户分别有一套表,其中包括(内核表+用户表)
	1个表
		对应M个内核进程(包括idle),只对应一套表,其中包括(内核表,基址为swapper_pg_dir)
		这一套表的大小远远小于 4MB+16KB

// 每个进程有自己的pgd,pgd中包含了用户虚拟地址空间映射和内核虚拟地址空间映射
// 不同进程的内核虚拟地址空间映射是相同的,在arm32上都是从swapper_pg_dir复制过来的

  • swapper_pg_dir
arch/arm/kernel/head.S
 48     .globl  swapper_pg_dir                                                       
 49     .equ    swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

// 在 ok6410中 , KERNEL_RAM_VADDR  = 0x50008000
// PG_DIR_SIZE 为 0x00004000
// 则 swapper_pg_dir 为 0x50004000

内核页目录表基址在swapper_pg_dir , 大小为16K
具体物理地址范围 为 0x50004000 - 0x50007FFF

二级页表的地址范围就比较随意了, alloc获取的地址是哪里,就在哪里
  • 页表的填充本质是什么
本质是 对 写内存 和 CP15 C2 寄存器的改变

写 CP15 C2 寄存器的本质来说
	1. 将一级页表(有N+1)的基址写入该寄存器

写内存详细一点来说是
	1. 确定写哪个  一级页表项 (即确定即将写入的一级页表项 的地址),并写入值
	2. 确定写哪个  二级页表项 (即确定即将写入的二级页表项 的地址),并写入值

  • 页表的复制
内核页表是什么时候创建的
	内核初始化的时候(从_stext到start_kernel),创建了
		一级页表和二级页表
			包括 Image atags mmu_turn_on附近代码的映射
	从start_kernel 到 运行时,创建了
		1. 设备映射
		2. 内存管理相关映射等

为什么要复制
	ARM32位处理器,由于无法通过TTBR0、TTBR1同时设置内核页表项地址和用户空间页表项地址,所以采用创建进程时拷贝内核空间页表的方式来实现共享内核空间的操作。
	



参考代码 kernel/fork.c 中的 copy_mm

idle进程进程创建的时候有没有复制内核页表?
	没有,页目录表基址还是为swapper_pg_dir ,0x50007000// 内核进程直接使用swapper_pg_dir作为基址
其他(kthreadd)内核进程的时候有没有赋值内核页表
	没有,页目录表基址还是为swapper_pg_dir ,0x50007000// 内核进程直接使用swapper_pg_dir作为基址
init 内核进程 转换为 用户进程 的时候有没有赋值内核页表
	有,创建了一套页表,并复制了内核页表中的内容到新的一套页表中
用户进程创建的
	有,创建了一套页表,并复制了内核页表中的内容到新的一套页表中

复制之后怎么实时同步多套页表中的"内核页表内容"?
	Linux arm进程内核空间页表同步机制 https://codeleading.com/article/77246118335/
	
	性能分析
		内核空间的虚拟地址也是在不停的变换的,
		如果映射一段地址就去主动更新所有进程页表的内核部分,显然是相当不划算的。
		一个进程映射的地址,其他进程很大概率是不会用到的。
		所以采用缺页中断的方式,在需要使用时,去拷贝页表。
		init_mm的pgd,则保存了完整的内核空间页表。
	
	实际情况
		在vmalloc、vmap亦或者ioremap时,内核空间页表会进程调整
		会修改 init_mm.pgd(swapper_pg_dir)中的一套页表内容 (即N+1中的那1套表),
		并没有修改用户进程中的那套表中的该内容
		
		在用户进程访问时,会产生缺页异常,在异常中,"拷贝内核对应页表到进程中"
	
  • 页表的切换

进程陷入内核空间时,不进行页表切换 // 因为切不切换都一样
进程退出内核空间时,如果要切换到与之前不同的进程,则要切换,否则,不需要切换

当进程切换时,有可能进行 页表的切换
switch_mm // switch_mm 只在内核态下运行
有几种情况 // 只要即将切入的进程为用户进程,就切换
	内核进程->内核进程 不切换
	内核进程->用户进程 切换
	用户进程->内核进程 不切换
	用户进程->用户进程 切换 
  • early_pte_alloc

arch/arm/mm/mmu.c
early_pte_alloc 
	arm_pte_alloc
		pte_t *pte = alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE); 
			// 申请内存 , 512*4 + 512*4
			// 申请了 hwtable[0]和hwtable[1]
			// 申请了 linuxtable[0]和linuxtable[1] 
			// 为什么要申请 linuxtable??? ,查看 "linux 页表 与 arm32硬件页表是如何兼容的"
			// 即申请4KB空间(2个linux页表,2个arm页表)// 一个页表大小为1KB
		__pmd_populate(pmd, __pa(pte), prot);
			// 填充 pmd[0] 为 hwtable[0]的基址
			// 填充 pmd[1] 为 hwtable[1]的基址
			// 即将2个arm页表的基地址写入2个l1页表表项(pmd[0]和pmd[1])
		return pte_offset_kernel(pmd, addr);
  • linux 页表 与 arm32硬件页表是如何兼容的
https://elixir.bootlin.com/linux/v5.11/source/arch/arm/include/asm/pgtable-2level.h#L13

arm页表和"linux对页表的要求"有矛盾
	ARM32现状:Most of the bits in the second level entry are used by hardware, and there aren't any "accessed" and "dirty" bits.
	Linux要求:However, Linux also expects one "PTE" table per page, and at least a "dirty" bit.

解决方案:
	针对第一级页表的改变
		1.Therefore, we tweak the implementation slightly - we tell Linux that we have 2048 entries in the first level, each of which is 8 bytes (iow, two hardware pointers to the second level.)  
		// 这也是 pmd[0] 和 pmd[1]的由来
	针对第二级页表的改变
		2.The second level contains two hardware PTE tables arranged contiguously, preceded by Linux versions which contain the state information Linux needs.  We, therefore, end up with 512 entries in the "PTE" level.
		// 这里是 linuxtable[0] 和 linuxtable[1]的由来

现状:
	See L_PTE_xxx below for definitions of bits in the "Linux pt", 
	See PTE_xxx for definitions of bits appearing in the "h/w pt".
	PMD_xxx definitions refer to bits in the first level page table.
	
	dirty bit
		The "dirty" bit is emulated by only granting hardware write permission iff the page is marked "writable" and "dirty" in the Linux PTE.
		This means that a write to a clean page will cause a permission fault, and the Linux MM layer will mark the page dirty via handle_pte_fault().
	access bit
		For the hardware to notice the permission change, the TLB entry must be flushed, and ptep_set_access_flags() does that for us.
		The "accessed" or "young" bit is emulated by a similar method; we only allow accesses to the page if the "young" bit is set.
		Accesses to the page will cause a fault, and handle_pte_fault() will set the young bit for us as long as the page is marked present in the corresponding Linux PTE entry.
		Again, ptep_set_access_flags() will ensure that the TLB is up to date.
		However, when the "young" bit is cleared, we deny access to the page by clearing the hardware PTE.
		Currently Linux does not flush the TLB for us in this case, which means the TLB will retain the transation until either the TLB entry is evicted under pressure, or a context switch which changes the user space mapping occurs.

你可能感兴趣的:(杂七杂八总览,linux,arm,运维)