Linux的页表管理

PFNLinux中经常用物理帧号引用物理页:page frame number( pfn) = PA / PAGE_SIZE。

如果PAGE_SIZE = 4K,pfn 0所在的PA是0x00000000,pfn 1所在的PA是0x0000_1000。

Linux中的页表结构:

只有启动5级页表时,p4d才有用,否则没有p4d

pte_t占一个4K页,一个Page Table Entry在32位系统下是4字节(32位),64位系统下是8字节(64位)。

一个pte_t里面有PTRS_PER_PTE个(32位:4K/4字节 = 1024,64位:4K/8字节 = 512)pteval_t类型元素组成的数组。

每个元素将一个虚拟内存页映射到一个物理内存页。

关于每个page table entry中的每个位定义,在 arch/x86/include/asm/pgtable_types.h定义:

这里描述的第x个位的作用都是什么。

32位操作系统使用的是二级页表,64位操作系统使用的是四级页表。32位操作系统使用的是PDE-PTE层级架构,64位操作系统使用的是(PGD-PUD-PMD-PTE)架构。

Page Fault

是一种异常(exception,当MMU发现某一级的Entry是空的(not present)就触发Page Fault异常。这可能有多种原因,比如:有可能是 CPU 试图去访问当前进程没有权限访问的内存,或者因为访问的数据还不在物理内存中。

PF用于通知 CPU 暂停当前的执行并运行一个特殊的函数去处理这些异常。

page fault有几种常见且在预期范围内的原因,这些原因由Lazy allocation和Copy-on-write等进程管理优化技术引发的(这两个)。也可能发生在page被swap到storage了。

用户进程的代码bug或指示 CPU 访问的精心制作的恶意地址也可能导致缺页异常也可能导致PF,kernel会向当前线程发送 `段错误` (SIGSEGV)信号,导致该进程终止。

对于ZONE_DMA这种线性映射的内存,没办法swap到其他地方去。那当内存不足的时候,内核会调用避免OOM杀手(out-of-memory killer)进程,中止优先级低的进程,直到内存压力下降到安全阈值之下。

x86下Page Fault的中断处理函数叫handle_page_fault(),handle_page_fault通常会调用__handle_mm_fault()分配页表。如果不能调用__handle_mm_fault,意味着VA指向了无权访问的PA,这种情况就是段错误,发送SIGSEGV。

__handle_mm_fault最终调用 handle_pte_fault(),通过do_fault()执行。

数据结构

内核有自己的pgd,每个进程有它自己的pgd,它位于struct mm_struct中。所以每个进程拥有一个struct mm_struct描述内存空间,而这个结构体中有一个struct pgt_t *pgd的指针,指向它自己的pgd。CR3要用到PA,切换进程的时候把mm_struct里面的pgd变成PA才能放到CR3里去。

 

struct task_struct:

定义在include/linux/sched.h中

struct mm_struct:

定义在include/linux/mm_types.h中:

pgd_t:

定义在arch/x86/include/asm/pgtable_types.h中:

CR3加载PGD

在切换进程的时候,调用context_switch函数进行进程上下文的切换时会更新CR3寄存器。调用栈如下:

其中build_cr3是构建CR3寄存器的值,write_cr3是把这个值写入CR3寄存器。

build_cr3:

定义在arch/x86/mm/tlb.c中:

(1)__sme_pa的作用是把pgd的VA转化成PA, 并且加上加密掩码。

(2)lam的作用:是否使能liner address map。

(3)PCID(process context identifier):是Intel为了解决进程切换时TLB flush导致的性能下降引入的,即针对每个进程分配专用的ID标识,用于区分TLB中不同进程对应的entry。

问题:内核是怎么在切换进程的时候,保持内核页表不变,进程页表更换的?

内核页表

定义内核页表

init_top_pgt

是内核的顶级页表,在head_64.S中创建路径:arch\x86\kernel\head_64.S

如果Xen 半虚拟化(Paravirtualization)或者混合虚拟化enable

.quad <value>:在当前地址处写入一个 64 位的整数值(value),并自动将地址指针后移 8 字节。

.org <offset>:将当前指针设置到offset处。后续的代码或数据将从该地址开始排放。

.fill <count>, <size>, <value>:重复生成<count>个,<size>字节的<value>,填充到当前地址。

init_top_pgt是一个8K页表,1024个entry。①从这个页表的开始,也就是这个pgt的第一个entry,填充数据。

(1)第一个entry放level3_ident_pgt页表的物理地址。

  level3_ident_pgt是VA,所以要计算它的PA。

从物理地址0开始的512M是内核的代码段,这段内存线性映射虚拟空间VA=0xffffffff80000000处。

Linux给它起名叫__START_KERNEL_map,所以__START_KERNEL_map = 内核代码段PA(0) + 线性映射offest = 这段地址的线性映射偏移。

内核页表就存放在这段内存中,所以level3_ident_pgtVA-__START_KERNEL_map(offset) = level3_ident_pgtdPA

_KERNPG_TABLE_NOENC是page table entry的属性值,也就是page table entry中除了物理地址以外的描述性bit:

所以行代码表示的就是:

init_top_pgt的第一个entry填充指向level3_ident_pgtentrylevel3_ident_pgtPA+页表项描述性)。

 

(2)把指针移到第273项。

0xffff888000000000是内核虚拟空间中的direct mapping area的起始VA。

l4_index就是取这个地址的4739位(从0位开始)。

所以.org    init_top_pgt + L4_PAGE_OFFSET*8, 0这行代码,就是根据direct mapping areaVA,找到它在init_top_pgt的第273entry,并且乘上每个entry的长度(8字节),就得到了这个entry的地址,把指针移到这个地址

 

(3)给第273项也填上和第一项一样的内容:指向level3_ident_pgtentry

(4)把指针移到第511项(最后一项)。

  0xffffffff80000000就是刚刚说的内核代码段__START_KERNEL_map,算出来它在顶级页表里对应第511项。

(5)给第511项填上指向level3_ident_pgt的entry。

(6)剩下的512项都填0。

level3_ident_pgt和level2_ident_pgt

level3_ident_pgt的第一项指向level2_ident_pgt,剩下的511项都填0。

level2_ident_pgt是最后一级页表:

经过上述代码,level2_ident_pg的填充如下:

从0-511依次填充进entry,__PAGE_KERNEL_LARGE_EXEC标志了一页是2M,512 * 2M = 1G。

所以这里就是先映射了directly mapping area的前1G的内存。

如果Xen 半虚拟化(Paravirtualization)或者混合虚拟化没启用(比较通用的情况)

init_top_pgt的1024项都填0。(感觉像不用init_top_pgt)

early_top_pgt

首先定义了两个table:early_top_pgt和early_dynamic_pgts

early_top_pgt其中定义的内容和level4_kernel_pgt一样。

level4_kernel_pgt

从gdb实际调试的情况看,level4_kernel_pgt的地址比init_top_pgt的地址靠后了0x200(8K),正好是init_top_pgt的大小。

所以level4_kernel_pgt紧挨在init_top_pgt的后面。

level4_kernel_pgt和level3_kernel_pgt和level2_kernel_pgt大小是4K。

所以内存排列可总结为如下:

level4_kernel_pgt第511项指向level3_kernel_pgt,其他都填0。

level3_kernel_pgt

L3_START_KERNEL代表的是内核代码段在level3_kernel_pgt对应的entey(0xffffffff80000000的第30:38位):510

510项指向level2_kernel_pgt,对应物理地址:0xffffffff80000000

511项(最后一项):指向level2_fixmap_pgt,对应物理地址:0xffffffffC0000000

level2_kernel_pgt:0xffffffff80000000

这里调用了PMDS宏,用于设置PMD中的entry。

总结:从物理地址0x0000_0000_0000_0000开始,映射一块512m的线性区域,页属性为_PAGE_KERNEL_LARGE_EXEC,重复写KERNEL_IMAGE_SIZE/PMD_SIZE = 512m/2m = 256次。

实则就是映射了512M的空间,用于存放内核的代码段+数据段+BSS段。也就是这一段,后面是module area。

总结:

初始化内核页表

一开始定义的页表覆盖的内存很小,只有内核代码区的512m。其他地址区域在内核初始化的时候进行映射。

上面定义完了内核页表,接下来是初始化内核页表。

在内核启动时:

x86_64_start_kernel定义在arch/x86/kernel/head64.c中:

(1)把early_top_pgt[511]的entry装给init_top_pgt[511]。

(2)调用x86_64_start_reservations后面会调用到start_kernel。

swapper_pg_dir就是init_top_pgt,定义在arch/x86/include/asm/pgtable_64.h中:

 

用户态进程页表,会有 mm_struct 指向进程顶级目录 pgd,对于内核来讲,也定义了一个 mm_struct,指向 swapper_pg_dir。

这个是init 进程(内核的第一个进程)的页表。

内核页表与用户页表关联

对于所有进程来说,内核空间都是相同的,这部分页表就来自于内核页表即每个进程的“进程页表”中内核态地址相关的页表项都是“内核页表”的一个拷贝。

内核通过do_fork函数创建一个线程,就从这个函数下手看看是怎么实现的。在5.0以后的内核中,do_fork被删除,取而代之的是kernel_clone函数。

 

kernel_clone()定义在kernel/fork.c中:

pgd_ctor

clone_pgd_range

就是从src,copy count条pgd entry到dst。

其中

PAGE_OFFSET = 0xffff888000000000

KERNEL_PGD_BOUNDARY = pgd_index(PAGE_OFFSET) = (0xffff888000000000 >> 39) & 511 = 273

KERNEL_PGD_PTRS = 512 - 273 = 239

也就是说把swapper_pg_dir的第273条开始,一直到511条,copy到用户进程的页表。

也就是从0xffff888000000000开始到最后copy给了用户空间,并不是把内核空间的128T页表都copy到用户空间,跳过了蓝框部分。用于防止用户空间越界问题到内核空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值