深入理解Linux内核页表映射分页机制原理

news2024/11/24 19:00:54

深入理解Linux内核页表映射分页机制原理

前言

操作系统用于处理内存访问异常的入口操作系统的核心任务是对系统资源的管理,而重中之重的是对CPU和内存的管理。

为了使进程摆脱系统内存的制约,用户进程运行在虚拟内存之上,每个用户进程都拥有完整的虚拟地址空间,互不干涉。

而实现虚拟内存的关键就在于建立虚拟地址(Virtual Address,VA)与物理地址(Physical Address,PA)之间的关系,因为无论如何数据终究要存储到物理内存中才能被记录下来。

如下图所示,进程1和进程2拥有完整的虚拟地址空间,虚拟地址空间分为了用户空间和内核空间,对于不同的进程面对的都是同一个内核其内核空间的地址对于的物理地址都是一样的,因而进程1和进程2中内核空间的VA K地址都映射到了物理内存的PA K地址

而不同的进程的用户空间是不同的进程1和进程2相同的虚拟地址VA 1和VA 2分别映射到了不同的物理地址PA 1和PA 2上。

而虚拟地址到物理地址映射关系的实现可以称之为地址转换(Address Translation)。

为了实现上述地址转换,操作系统需要借助硬件的帮助,即内存管理单元(Memory Management Unit,MMU)的帮助。


对于MMU应当有如下功能:

要求说明
特权模式区分内核空间和用户空间,用户进程无法直接访问内核地址空间
基址/界限寄存器记录地址转换基址的寄存器,用于寻址地址转换映射表
地址转换完成地址转换过程
异常处理特权操作指令操作系统用于处理内存访问异常的入口

MMU配合操作系统完成了诸多功能:

用户空间和内核空间,通过特权模式划分了内核空间和用户空间,用户空间无法直接访问内核空间,必须通过某些手段(系统调用,异常,中断等)切换到特权模式才能间接访问内核。

地址转换,通过基址/界限寄存器记录的转换映射表基址,结合虚拟地址,可以完成地址转换的功能,从而实现通过虚拟地址访问到物理地址。

进程独立的虚拟地址空间,通过基址/界限寄存器的访问指令,在进程切换时修改基址/界限寄存器的值,从而使MMU在做地址转换时找到各个进程对应的地址映射表,从而实现不同进程虚拟地址完全独立。

缺页异常,对于进程申请的内存,并不需要在其申请内存时即建立地址转换映射表,同时分配对应的物理空间,而是在进程真正访问内存地址时,MMU上报缺页异常再分配对应的物理空间。

当然虚拟地址到物理地址映射表中的一些标志区可以实现更多的缺页异常类型,例如读写权限错误,特权错误,越界错误等异常。

一、分页

分页即将内存划分为固定长度的单元,每个单元就是一页。对于虚拟地址空间,分页机制将地址空间分割成固定大小的单元,每个单元称为一页。

对于物理地址空间**,物理内存被抽象成固定大小的单元,每个单元称为页帧(frame)**。通过分页管理内存可以避免分段带来的内存外碎片问题。

分页管理内存的核心问题是虚拟地址页到物理地址页帧的映射关系。虚拟地址到物理地址的转换可以抽象简化成下图,假设地址是32位的。

为了将虚拟地址转换成物理地址,将虚拟地址分割成两部分:

  • 虚拟页面号,高31-X位组成,VPN(virtual page number);
  • 虚拟地址偏移,低X位组成,VA Offset(virtual address offset);

物理地址也抽象成两部分:

  • 物理页帧号,高31-X位组成,PFN(physical frame number);
  • 物理地址偏移,低X位组成,PA Offset(physical address offset);

虚拟页面号VPN用于索引物理页帧号PFN,VPN索引PFN的过程就是地址转换的核心。VA offset通常就是PA offset,即PFN + VA offset就是最终物理地址。

所以,可以说分页机制的核心就是VPN到PFN的映射。而VPN到PFN的映射关系是通过页表记录的。MMU通过页表记录的映射关系完成VPN到PFN的转换,即找到了页表就找到了物理地址。

1.1 页表存在哪里?

以32位地址空间为例,分页大小为4KB(最常用的分页大小),上述抽象例子中的X为12,那么VPN长度就是20bit,偏移量为12bit。

20bit的VPN意味着操作系统需要2^20个地址转换映射,假设每个转换映射需要4Byte空间存储,那么所有映射关系需要4MB空间。

开篇我们提到,进程的虚拟地址到物理地址的转换是不同的,所以每个进程的映射关系也是不同的,就是说每个进程都需要4MB的空间来存储页表。如果操作系统运行100个进程,则需要400MB空间。

可见页表所需要的空间是很大的,所以页表都存储在物理内存中。即MMU通将虚拟地址转换为物理地址,需要访问物理内存中对应的页表。

当然页表占用物理内存大的问题还是需要解决的,这是分页相对于分段的一个劣势,解决方案是多级页表配合缺页异常的方式,后面再详细介绍多级页表的机制。

1.2 页表长啥样?

页表是如何完成VPN到PFN的转换的,要知道这个问题就得清楚页表的基本内容,即页表记录了什么信息。

页表的作用就是通过VPN找到PFN,那么页表最基本的组成部分需要包含如下内容:

  • PFN物理页帧号;
  • 有效位(valid),用于标记页面是否有效
  • 存在位(present),指示该页是否存在于物理内存,用于页面换入换出(swap);
  • 特权标记,指示页面访问的特权等级;
  • Dirty位,写操作时设置该位,表示页面被写过,页面交换时使用

1.3 分页机制如何完成进程地址空间切换?

每个进程都拥有自己独立的地址空间,进程切换时地址空间也会切换。

不同进程都拥有自己的一套页表,因而即使两个进程虚拟地址相同,映射的物理地址也是不同的

切换地址空间相当于控制MMU访问不同进程拥有的页表,MMU找到了页表就找到了物理地址。

通常CPU会提供若干寄存器供操作系统使用,用于为MMU指示页表的基地址。

如下图所示,进程切换时,只需要设置页表基址寄存器即可完成页表的切换,也就完成了进程地址空间的切换。

所以CPU会为操作系统提供页表基址寄存器用于进程地址空间的切换。

  • X86体系架构提供的寄存器是CR3(Control Register 3);

  • ARM-v7体系架构提供的寄存器是协处理器CP15寄存器TTBR(Tranlation Table Base Register);

  • ARM-v8体系架构提供的寄存器是系统寄存器TTBR(Tranlation Table Base Register)。

1.4 实际使用的分页机制

考虑到分页机制占用内存过多的问题,实际的分页机制是多级分页

以二级页表为例,如下图所示:

  • MMU通过页表基址寄存器配合虚拟地址中的**PGD index(Page Global Directory)**找到一级页表,
  • 通过一级页表配合虚拟地址中的**PTE index(Page Table Entry)**找到二级页表,通过二级页表配合虚拟地址中Offset找到物理地址。

多级页表要做到节省内存,还需要配合缺页异常,进程往往只需将一级页表保持到内存中,二级页表在缺页异常时再分配。

下图示例中,一级页表一共4096项(212),二级页表一共512项(29)。

因此进程页表可以只使用4096 X 4Byte空间即可。

如果使用一级页表,则需要2097152 X 4Byte空间。

因此多级页表带来的最大好处就是降低了内存空间的占用。

1.5 多级页表的缺点

多级页表带来了好处,降低了操作系统进程管理,内存管理对内存空间的占用。

当然计算机领域总是没有那么完美的方案,多级分页也逃避不了这个宿命,获得了空间的优势,也带来时间上的损失。

多级分页时间上的损失主要体现在如下几个方面:

  • 用时下发的耗时:对于子进程写时复制(COW)技术大家比较熟悉,其实多级页表也利用了类似的思想。多级页表的后几级页表映射关系没有存在内存中,MMU地址转换中发现页表不存在需要向操作系统上报缺页异常,操作系统需要在缺页异常中下发页表到内存;

  • 额外的内存访问:MMU进行地址转换需要通过页表基址寄存器找到一级页表,再依次找到下级页表,所有的页表都存放在内存中,访问内存是需要额外的时间消耗的,相对于CPU对寄存器的访问,Cache的访问速度而言,内存的访问速度是灾难性的,何况还是多次访问。当然额外的内存访问本身是分页机制相对分段机制的缺陷,一级页表映射也存在这样的缺陷,只是多级页表映射将这个缺点再次放大。

1.6 Translation Lookside Buffer

Translation Lookside Buffer简称TLB,按其真实作用应当翻译为地址转换缓存。

方才抨击了多级页表映射基址,提出了它可能导致系统变慢的缺点,那么如何解决这一问题呢?

如果使MMU做页表转换时不访问内存,是不是就解决问题了?

TLB就是干这个事的。

TLB之所以可以解决这个问题是因为TLB是Cache,它将CPU访问内存替换为CPU访问Cache,也就是说MMU做页表转换时不再访问内存的页表,而是访问缓存在TLB中的页表,因而降低了时间的消耗。

TLB要实现这个替换,其需要实现的基本工作原理是:

  • 从虚拟地址中提取页号(VPN),检查TLB是否有该VPN的转换映射。

  • 如果有,则表示TLB命中(TLB hit),意味着从TLB中找到VPN对应的物理页框号(PFN)。PFN与虚拟地址的偏移量组成成物理地址(PA)。

  • 如果没有,表示TLB未命中(TLB miss),则需要处理TBL miss。

TLB miss处理有两种方法:

  • 一种是硬件处理
  • 一种是软件处理

硬件处理TLB miss会自动更新TLB。

软件处理则是由硬件抛出一个TLB miss异常,软件进入异常处理程序,查找物理页表中转换映射,再由指令更新TLB,并从异常中返回。

软件处理TLB miss异常与其他异常不同,异常处理返回后,应继续执行陷入异常之后的那条指令,而TLB miss异常处理返回后,从导致陷入异常的执行开始执行。这样保证TLB一定命中。

诚然,TLB是好,但是也引入了一些麻烦事(既然是Cache,就有一致性问题):

  • 进程切换时TLB如何处理?
  • TLB表项满了如何处理?
  • mmap映射的内存被munmap解除TLB怎么处理?
  • ……

针对这些话题本文不做深入探讨,可以阅读另一篇为其量身定做的博文《深入Linux内核(内存篇)—TLB》。

1.7 页表多大合适?

  • 大页表的好处:

    • 省内存:可以解决分页机制占用内存的问题,取得和多级页表一样节省内存的效果;

    • 对TLB友好:大页表意味着地址转换时需要更少的页表映射表项,页表映射表项少了意味着TLB缓存的表项少,这样就提高了TLB的命中率;

  • 大页表的坏处:

    • 内存内碎片:操作系统申请内存时总是申请一大块内存,哪怕实际只需要很小的内存,导致大页内存得不到充分利用;而且内存很快会被这些大页侵占。

    • 显然小页表的好处和坏处正好与大页表对立。

    • 因此页表不是越大越好,也不是越小越好,找到折中的大小是才最适合。通常操作系统的使用的页大小是4KB。

各种体系架构的CPU都支持很多种页大小。因此实际页表的应用可能会更“聪明”,用户进程在请求地址空间时,可以因需求选择合适的页大小,这样既可以满足数据的存放,同时占用更少的TLB表项。

一个典型的例子,DPDK使用了1GB的大页内存,这样DPDK进程的页表映射只占用一个TLB表项,在进程执行过程中杜绝了TLB miss情况的发生,保障了性能。


DPDK (Data Plane Development Kit): DPDK 是一个开源库集合,它允许开发者通过用户空间应用直接访问网络硬件,从而实现高性能的数据包处理。这通常用于快速的数据包转发和处理,例如在路由器、交换机或防火墙中。

当DPDK使用1GB的大页内存时,这意味着DPDK进程的虚拟地址空间中的每1GB部分都映射到物理内存的一个连续区域。因此,对于1GB的虚拟地址范围,DPDK进程的页表只需要一个条目来描述这一映射。

由于页表条目减少了,TLB中需要的表项也减少了。在这个例子中,DPDK进程的整个1GB虚拟地址范围只需要一个TLB表项。这意味着,当DPDK进程尝试访问这1GB范围内的任何地址时,它都可以直接从TLB中获取虚拟到物理地址的转换,而不需要查询页表。

这杜绝了TLB miss情况的发生,因为DPDK进程在访问其虚拟地址空间时总是能够在TLB中找到所需的转换。避免了TLB miss意味着避免了查询页表的开销,从而显著提高了性能。

总之,通过使用大页内存,DPDK能够减少页表和TLB的大小,降低TLB miss率,从而提高数据包处理的性能。

二、X86中的分页

X86中定义分页即将每个线性地址转换为物理地址,并确定对于每个转换,允许对线性地址的何种访问(地址的访问权限)以及用于此类访问的缓存类型(地址的内存类型)。

X86支持如下四种分页模式:

分页模式的选择主要由control register CR0,control register CR4,IA32_EFER MSR控制。

由上表可以看出:

  • CR0.PG = 0,关闭分页单元,线性地址被直接解释成物理地址;
  • CR0.PG = 1 && CR4.PAE = 0,使用32-bit分页机制,线性地址大小是232,物理地址大小可以达到240,支持的页大小是4KB和4MB;
  • CR0.PG = 1 && CR4.PAE = 1,使用PAE(Physical Address Extension)分页机制,线性地址大小是232,物理地址大小可以达到252,支持的页大小是4KB和2MB;
  • CR0.PG = 1 && CR4.PAE = 1 && IA32_EFER.LME = 1,使用4级分页机制,线性地址大小是248,物理地址大小可以达到252,支持的页大小是4KB、2MB和1GB;
  • CR0.PG = 1 && CR4.PAE = 1 && IA32_EFER.LME = 1 && CR4.LA57 = 1,使用5级分页机制,线性地址大小是257,物理地址大小可以达到252,支持的页大小是4KB、2MB和1GB;

三、ARM中的分页

直接上ARMv8

3.1 ARMv8 分页配置

ARMv8架构AArm64支持三种页大小:64KB,16KB和4KB。

页大小选择由系统寄存器TCR控制,如下图所示为TCR_EL1寄存器。

比较重要的bit位说明:

  • T0SZ[5:0]:The size offset of the memory region addressed by TTBR0_EL1,TTBR0_EL1寻址的内存区域的大小偏移量,内存区域大小计算方法2(64-T0SZ);
  • T1SZ[21:16]:The size offset of the memory region addressed by TTBR1_EL1,TTBR1_EL1寻址的内存区域的大小偏移量,内存区域大小计算方法2(64-T0SZ);
  • IRGN0[9:8]/IRGN1[25:24]:Inner cacheability attribute for memory associated,控制内部Cache访问模式,直写和回写;
  • ORGN0[11:10]/ORGN1[27:26]:Outer cacheability attribute for memory associated,控制外部Cache访问模式,直写和回写;
  • TG0[15:14]:Granule size for the TTBR0_EL1,页大小,为0表示4KB,1表示64KB,2表示16KB;
  • TG1[31:30]:Granule size for the TTBR1_EL1,页大小,为0表示4KB,1表示64KB,3表示16KB;
  • A1[22]:ASID选择,为0选择TTBR0_EL1.ASID ,为1选择TTBR1_EL1.ASID;
  • IPS[34:32]:Intermediate Physical Address Size,中间物理地址大小;
  • AS[36]:ASID Size,ASID大小,为0表示8bit,为1表示16bit;
  • HA[39]:Hardware Access flag update,Access使能位;
  • HD[40]:Hardware management of dirty state,Dirty使能位;

说明:

  • ARM架构提供了两个页表基址寄存器TTBR0和TTBR1,可以分别用于用户态和内核态。
  • ASID用于标识进程,可以根据ASID划分TLB entry,避免TLB entry频繁Flush。
  • 显然系统寄存器TCR控制了页表映射的参数,其中TCR.TG0/TG1决定了页大小。
  • 当页大小为4KB时,分页单元每级页表的地址范围如下,其中TnSZmin和TnSZmax分别表示TCR_ELx.TnSZ的最小最大值,IA表示Input Address,即虚拟地址:

3.2 ARMv8 Paging

以页大小为4KB,虚拟地址位宽为48bit为例,符合上一节中TCR_ELx.TnSZ为最小值的情况,如下图所示。

ARMv8对IA(input address)划分成了五部分:

  • Index the level 0 translation table[47:39]:最高9bit;
  • Index the level 1 translation table[38:30]:中间9bit;
  • Index the level 2 translation table [29:21]:中间9bit;
  • Index the level 3 translation table[20:12]:中间9bit;
  • OA[11:0]:output address,最低12bit。
    这个划分方法与X86 4-Level Paging一样。

其地址转换过程,与前述的地址转换过程并无差别,从页表基址寄存器TTBR_ELx开始逐级查找到物理地址.

3.3 Kernel中的ARMv8分页

Linux Kernel分页为了支持不同的CPU体系架构,设计了五级分页模型,如下图所示。五级分页模型是为了兼容X86-64体系架构中的5-Level Paging分页模式,见第二节。

五级分页每级命名分别为页全局目录(PGD)、页4级目录(P4D)、页上级目录(PUD)、页中间目录(PMD)、页表(PTE)。对应的相关宏定义命名如下:

#define PGDIR_SHIFT
#define P4D_SHIFT
#define PUD_SHIFT
#define PMD_SHIFT
#define PAGE_SHIFT

这些宏定义与具体体系架构相关,如果体系架构只使用了4级,3级或者更少的分级映射,则将其中的某几个定义忽略即可。

Linux对于页表的操作主要定义了以下函数或宏。这些操作方法也是与体系架构相关的,因此需要按照体系架构的硬件定义去实现。

宏或函数说明
pgd_offset(mm, addr)根据入参内存描述符mm和虚拟地址address,找到address在页全局目录中相应表项的线性地址。
pgd_offset_k(addr)根据入参虚拟地址address和init_mm,找到address在页全局目录中相应表项的线性地址。仅用于内核页表。
p4d_offset(pgd, addr)根据入参pgd和虚拟地址address,找到address在页四级目录中相应表项的线性地址。
pud_offset(p4d,addr)根据入参p4d和虚拟地址address,找到address在页上级目录中相应表项的线性地址。
pmd_offset(pud, address)根据入参pud和虚拟地址address,找到address在页中间目录中相应表项的线性地址。
pte_index(address)根据入参虚拟地址address,找到address在页表中索引。
set_pgd(pgdp, pgd)向PGD写入指定的值
set_p4d(p4dp, p4d)向P4D写入指定的值
set_pud(pudp, pud)向PUD写入指定的值
set_pmd(pmdp, pmd)向PMD写入指定的值
set_pte(ptep, pte)向PTE写入指定的值
pte_dirty(pte)读Dirty标志
pte_mkdirty(pte)写Dirty标志

分页机制与CPU体系架构强相关,因此分析Linux Kernel分页时还是需要根据体系架构分析。


ARMv8页表支持三种粒度:4KB,16KB和64KB。

ARMv8支持48bit虚拟地址空间,实现ARMv8.2-LVA( Large Virtual Address)并使用64KB页大小时虚拟地址寻址空间可达52bit。

当使用64KB页大小时,ARMv8使用三级页表;当使用4KB和16KB页大小时,ARMv8使用四级页表。

正如下图所示。

ARMv8采用4KB页大小,使用4级页表时,内存分布如下,内核空间和用户空间大小分别为256TB。

内核空间地址范围从0xffff000000000000到0xffffffffffffffff,共256TB空间,用户空间地址范围从0x0000000000000000到0x0000ffffffffffff,共256TB空间。

AArch64 Linux memory layout with 4KB pages + 4 levels (48-bit)::

  Start			End			Size		Use
  -----------------------------------------------------------------------
  0000000000000000	0000ffffffffffff	 256TB		user
  ffff000000000000	ffff7fffffffffff	 128TB		kernel logical memory map
  ffff800000000000	ffff9fffffffffff	  32TB		kasan shadow region
  ffffa00000000000	ffffa00007ffffff	 128MB		bpf jit region
  ffffa00008000000	ffffa0000fffffff	 128MB		modules
  ffffa00010000000	fffffdffbffeffff	 ~93TB		vmalloc
  fffffdffbfff0000	fffffdfffe5f8fff	~998MB		[guard region]
  fffffdfffe5f9000	fffffdfffe9fffff	4124KB		fixed mappings
  fffffdfffea00000	fffffdfffebfffff	   2MB		[guard region]
  fffffdfffec00000	fffffdffffbfffff	  16MB		PCI I/O space
  fffffdffffc00000	fffffdffffdfffff	   2MB		[guard region]
  fffffdffffe00000	ffffffffffdfffff	   2TB		vmemmap
  ffffffffffe00000	ffffffffffffffff	   2MB		[guard region]

ARM提供了两个页表基址寄存器TTBR0和TTBR1,在Linux中分别用于用户空间和内核空间,内核空间地址高16位全为1,用户空间地址高16位全为0。

如下图所示,TTBR1和TTBR0分别管理0xffff000000000000到0xffffffffffffffff和0x0000000000000000到0x0000ffffffffffff两部分地址空间,其余地址空间访问则会发生异常。

MMU做地址转换时选择TTBR1和TTBR0是根据虚拟地址VA[63],如果63bit为1则选择TTBR1,为0则选择TTBR0。

ARMv8采用4KB页大小,4级页表映射,其虚拟地址划分为,在3.4节中已经做过说明。

Linux上述 Index the level 0 & 1 & 2 & 3 translation table等数据的定义位于ARM体系架构目录,如下所示。

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

#define PTRS_PER_PTE		(1 << (PAGE_SHIFT - 3))

/*
 * PMD_SHIFT determines the size a level 2 page table entry can map.
 */
#if CONFIG_PGTABLE_LEVELS > 2
#define PMD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(2)
#define PMD_SIZE		(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK		(~(PMD_SIZE-1))
#define PTRS_PER_PMD		PTRS_PER_PTE
#endif

/*
 * PUD_SHIFT determines the size a level 1 page table entry can map.
 */
#if CONFIG_PGTABLE_LEVELS > 3
#define PUD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(1)
#define PUD_SIZE		(_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK		(~(PUD_SIZE-1))
#define PTRS_PER_PUD		PTRS_PER_PTE
#endif

/*
 * PGDIR_SHIFT determines the size a top-level page table entry can map
 * (depending on the configuration, this level can be 0, 1 or 2).
 */
#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))

PGDIR_SHIFT宏对应了 Index the level 0 translation table,当4KB页大小,4级页表映射时(PAGE_SHIFT = 12;CONFIG_PGTABLE_LEVELS = 4),通过计算可得PGDIR_SHIFT宏为39,与硬件分页定义一致。

PGDIR_SHIFT	= (12-3)*(4-0)+3 = 39

CONFIG_PGTABLE_LEVELS表示使用页表级数,当前使用的是4级页表,所以CONFIG_PGTABLE_LEVELS>3。因此PUD定义为:

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

PUD_SHIFT宏对应了 Index the level 1 translation table,带入计算可知PUD_SHIFT为30,与硬件分页定义一致。

PUD_SHIFT = (12-3)*(4-1)+3 = 30

CONFIG_PGTABLE_LEVELS表示使用页表级数,当前使用的是4级页表,所以CONFIG_PGTABLE_LEVELS>2。因此PMD定义为:

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

PMD_SHIFT宏对应了 Index the level 3 translation table,带入计算可知PUD_SHIFT为21,与硬件分页定义一致。

PMD_SHIFT = (12-3)*(4-2)+3 = 21

至此,PGD,PUD,PMD都已获悉,对应的其他宏的值也可以顺利计算出结果。

#define PGDIR_SIZE		512GB
#define PTRS_PER_PGD	512

#define PUD_SIZE		1GB
#define PTRS_PER_PUD	512

#define PMD_SIZE		2MB
#define PTRS_PER_PMD	512

ARMv8 Linux下发PGD,PUD,PMD,PTE并没有使用汇编语言,而是使用C语言实现,对应的函数如下,其实现原理都是将对应的表项内容写入表项所在地址。

/* 向内存下发PGD页表,入参分别为pgd页表虚拟地址和pgd表项*/
static inline void set_pgd(pgd_t *pgdp, pgd_t pgd)
{
	if (in_swapper_pgdir(pgdp)) {
		set_swapper_pgd(pgdp, pgd);  /* 将pgd写入swapper_pg_dir所指地址 */
		return;
	}

	WRITE_ONCE(*pgdp, pgd); /* 将pgd写入pgdp所指地址 */
	dsb(ishst); /* 数据内存屏障 */
	isb(); /* 指令内存屏障 */
}

static inline void set_pud(pud_t *pudp, pud_t pud)
{
#ifdef __PAGETABLE_PUD_FOLDED
	if (in_swapper_pgdir(pudp)) {
		set_swapper_pgd((pgd_t *)pudp, __pgd(pud_val(pud)));
		return;
	}
#endif /* __PAGETABLE_PUD_FOLDED */

	WRITE_ONCE(*pudp, pud); /* 将pud写入pudp所指地址 */

	if (pud_valid(pud)) {
		dsb(ishst);
		isb();
	}
}

static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
{
#ifdef __PAGETABLE_PMD_FOLDED
	if (in_swapper_pgdir(pmdp)) {
		set_swapper_pgd((pgd_t *)pmdp, __pgd(pmd_val(pmd)));
		return;
	}
#endif /* __PAGETABLE_PMD_FOLDED */

	WRITE_ONCE(*pmdp, pmd); /* 将pmd写入pmdp所指地址 */

	if (pmd_valid(pmd)) {
		dsb(ishst);
		isb();
	}
}

static inline void set_pte(pte_t *ptep, pte_t pte)
{
	WRITE_ONCE(*ptep, pte); /* 将pte写入ptep所指地址 */

	/*
	 * Only if the new pte is valid and kernel, otherwise TLB maintenance
	 * or update_mmu_cache() have the necessary barriers.
	 */
	if (pte_valid_not_user(pte)) {
		dsb(ishst);
		isb();
	}
}

本文内核版本为Linux5.6.4。


四、举个栗子

页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)

  • 从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。
  • 第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出,即页上级页目录的物理基地址。
  • 从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。
  • 第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。
  • 从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。
  • 第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。
  • 从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。
  • 第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。
  • 从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。
  • 第五次读取内存得到最终要访问的数据。

整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。

不管是页表还是要访问的数据都是以页为单位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。

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))))

进程在切换的时候就是根据task_struct找到mm_struct里的PGD字段,取得新进程的页全局目录,然后填充到CR3寄存器,就完成了页的切换。

下面我们动手操作一下,通过代码来深度理解下虚拟地址是如何转化为物理地址的。

#include  <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/mm.h>
#include <asm/pgtable.h>
#include <asm/page.h>

MODULE_DESCRIPTION("vitual address to physics address");

static int pid;
static unsigned long va;

module_param(pid,int,0644); //从命令行传递参数(变量,类型,权限)
module_param(va,ulong,0644); //va表示的是虚拟地址

static int find_pgd_init(void)
{
        unsigned long pa = 0; //pa表示的物理地址
        struct task_struct *pcb_tmp = NULL;
        pgd_t *pgd_tmp = NULL;
        pud_t *pud_tmp = NULL;
        pmd_t *pmd_tmp = NULL;
        pte_t *pte_tmp = NULL;

        printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET);  //页表中有多少个项
    /*pud和pmd等等  在线性地址中占据多少位*/
        printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT);
    //注意:在32位系统中  PGD和PUD是相同的
        printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT);
        printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT);
        printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT);

        printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD); //每个PGD里面有多少个ptrs
        printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD);
        printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD); //PMD中有多少个项
        printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE);

        printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK); //页的掩码

  struct pid *p = NULL;
  p = find_vpid(pid); //通过进程的pid号数字找到struct pid的结构体
  pcb_tmp = pid_task(p,PIDTYPE_PID); //通过pid的结构体找到进程的task  struct
        printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd);
               // 判断给出的地址va是否合法(va&lt;vm_end)
    if(!find_vma(pcb_tmp->mm,va)){
                printk(KERN_INFO"virt_addr 0x%lx not available.\n",va);
                return 0;
        }
        pgd_tmp = pgd_offset(pcb_tmp->mm,va);  //返回线性地址va,在页全局目录中对应表项的线性地址
        printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp);
    //pgd_val获得pgd_tmp所指的页全局目录项
    //pgd_val是将pgd_tmp中的值打印出来
        printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp));
        if(pgd_none(*pgd_tmp)){  //判断pgd有没有映射
                printk(KERN_INFO"Not mapped in pgd.\n");
                return 0;
        }
        pud_tmp = pud_offset(pgd_tmp,va); //返回va对应的页上级目录项的线性地址
        printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp);
        printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp));
        if(pud_none(*pud_tmp)){
                printk(KERN_INFO"Not mapped in pud.\n");
                return 0;
        }
        pmd_tmp = pmd_offset(pud_tmp,va); //返回va在页中间目录中对应表项的线性地址
        printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp);
        printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp));
        if(pmd_none(*pmd_tmp)){
                printk(KERN_INFO"Not mapped in pmd.\n");
                return 0;
        }
        //在这里,把原来的pte_offset_map()改成了pte_offset_kernel
        pte_tmp = pte_offset_kernel(pmd_tmp,va);  //pte指的是  找到表

        printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp);
        printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp));
        if(pte_none(*pte_tmp)){ //判断有没有映射
                printk(KERN_INFO"Not mapped in pte.\n");
                return 0;
        }
        if(!pte_present(*pte_tmp)){
                printk(KERN_INFO"pte not in RAM.\n");
                return 0;
        }
        pa = (pte_val(*pte_tmp) & PAGE_MASK) ;//物理地址的计算方法
        printk(KERN_INFO"virt_addr 0x%lx in RAM Page is 0x%lx .\n",va,pa);
        //printk(KERN_INFO"contect in 0x%lx is 0x%lx\n",pa,*(unsigned long *)((char *)pa + PAGE_OFFSET));

        return 0;

}

static void __exit  find_pgd_exit(void)
{
        printk(KERN_INFO"Goodbye!\n");

}

module_init(find_pgd_init);
module_exit(find_pgd_exit);

MODULE_LICENSE("GPL");

运行结果如下:

可以看出虚拟地址ffff99b488d48000对应的物理地址是80000000c8d48000。这个过程也是mmu的过程。

参考资料

  • https://zhuanlan.zhihu.com/p/458935522
  • https://blog.csdn.net/liyuewuwunaile/article/details/108632620

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1531615.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

静态网络配置

一、查看网络命令 1.命令行查看网络配置 1、查看ip\硬件设备-网卡 ifconfig -a ifconfig ens160 网卡名称 ip addr show ip addr show ens160 nmcli device show ens160 nmcli con up ens160 2、主机名称 hostname hostname hfj.huaxia.com 3、查看路由和网关 rou…

(C++20) jthread中stop_token的基础使用

(C20) jthread中stop_token的基础使用 文章目录 (C20) jthread中stop_token的基础使用C20 jthread使用方式循环判断条件变量condition_variable_any stop回调 std::stop_callbackEND C20 jthread std::jthread - cppreference.com std::stop_token - cppreference.com std::sto…

springboot3以及上版本引入RocketMQTemplate显示could not be found.

1. 问题所在 springboot3以及上版本引入RocketMQTemplate显示could not be found&#xff1f; 在springboot3时&#xff0c;直接通过依赖来注入RocketMQTemplate会报错&#xff0c;会显示没有这个对象。 这是因为在Springboot3以前的版本&#xff0c;自动装配是通过读取所有jar…

Docker容器化技术(docker-compose安装部署案例)

docker-compose编排工具 安装docker-compose [rootservice ~]# systemctl stop firewalld [rootservice ~]# setenforce 0 [rootservice ~]# systemctl start docker[rootservice ~]# wget https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-…

xinput1_3.dll丢失如何修复,xinput1_3.dll的安装修复教程分享

在Windows操作系统环境下&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到xinput13.dll”。由于xinput1_3.dll是微软DirectX SDK的一部分&#xff0c;主要用于支持游戏手柄和其他外部设备的输入功能&#xff0c;缺失这一动态链接库文件可能导致某些依赖…

高顿咨询如何用国产CRM实现经验决策到数据决策的跨越

编者按 近日&#xff0c;Salesforce 移动应用在中国大陆苹果应用商店的下架&#xff0c;预示着今年CRM国产化替代即将迎来高潮。CRM作为距离业务最近的软件&#xff0c;被公认为是企业数字化转型、高质量发展的核心系统之一。“企业如何选择一款真正满足自身业务需求的本土化C…

邮件客户端 Thunderbird 简单配置

1. 基本情况介绍 原来使用的邮箱客户端是 Office 365 自带的 Outlook 365切换原因&#xff1a;新装电脑&#xff0c;发现原 Outlook 中的账号信息无法迁移&#xff0c;需要耗费大量时间手动配置邮箱使用的邮箱&#xff1a;微软 O365 邮箱、qq 邮箱、163 邮箱、公司私有邮箱 …

stable diffusion webui ubuntu 安装

1.git clone 下来 GitHub - AUTOMATIC1111/stable-diffusion-webui: Stable Diffusion web UIStable Diffusion web UI. Contribute to AUTOMATIC1111/stable-diffusion-webui development by creating an account on GitHub.https://github.com/AUTOMATIC1111/stable-diffus…

阻止默认行为 e.preventDefault()搭配passive:false才有效

正确情况 如果想阻止默认行为,那么 e.preventDefault()搭配passive:false才是正解 document.addEventListener(touchmove,(e)>{ e.preventDefault() console.log(123,123);},{passive:false}) 如果搭配 passive:false,则会报警告 e.preventDefault()搭配passive:true会报…

FREERTOS软件定时器

FreeRTOS 也提供了定时器功能&#xff0c;不过是软件定时器&#xff0c;软件定时器的精度肯定没有硬件定时器那么高&#xff0c;但是对于普通的精度要求不高的周期性处理的任务来说够了。当MCU的硬件定时器不够的时候就可以考虑使用 FreeRTOS 的软件定时器。 软件定时器允许设置…

HCIP【静态路由综合实验练习】

目录 实验要求&#xff1a; 实验过程&#xff1a; 一&#xff1a;首先设计实验 二&#xff1a;IP地址的划分&#xff08;基于192.168.1.0/24&#xff09; 在ensp中对路由器的相关命令进行配置&#xff1a; 三&#xff1a;配IP地址 &#xff08;1&#xff09;首先给所有设…

conda创建环境网络报错解决办法

文章目录 一、报错示例&#xff1a;二、解决办法&#xff1a;2.1 查看配置 conda config --show-sources2.2 修改文件 /home/XXXX/.condarc 一、报错示例&#xff1a; UnavailableInvalidChannel: HTTP 404 NOT FOUND for channel nvidia <http://mirrors.tuna.tsinghua.ed…

uniapp 开发微信小程序 出现启用组件按需注入问题如何解决

问题描述 在使用uniapp 开发微信小程序&#xff0c;进行上架发布时 代码质量栏 出现启用组件按需注入问题。 虽然现实代码上传成功&#xff0c;但是作为一个吹毛求疵的老猿人&#xff0c;肯定是无法容忍的。那么如何解决呢&#xff1f; 问题解决方案 在uniapp端&#xff0c…

[HackMyVM] Quick

kali:192.168.56.104 主机发现 arp-scan -l # arp-scan -l Interface: eth0, type: EN10MB, MAC: 00:0c:29:d2:e0:49, IPv4: 192.168.56.104 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.56.1 0a:00:27:00:00:05 (Un…

QT增加线程函数步骤流程

在使用线程的时候&#xff0c;不仅要关注线程开启的时机&#xff0c;同时还要关注线程安全退出&#xff0c;这样才能保证程序的健壮性&#xff0c;如果线程开启的较多&#xff0c;且开启关闭比较频繁&#xff0c;建议使用线程池来处理。开启线程有三种方式&#xff1a;第一种C的…

房屋租赁系统|基于JSP技术+ Mysql+Java+ B/S结构的房屋租赁系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

Milvus 社区在线寻找「北辰使者」!!!

Milvus 社区的朋友们&#xff0c;大家好&#xff0c;欢迎来到 Milvus 北极星计划发布现场&#xff01; 熟悉我们的朋友都知道&#xff0c;Milvus 起源于一种学名叫“赤鸢”的鸟类&#xff0c;鸟类飞行的方向判断依靠星星、太阳、磁场&#xff0c;而北极星在人类历史上长期被视为…

24考研数学史上最难!25该怎么学?

25考研的千万不要被以前的真题卷给蒙蔽了双眼&#xff0c;现在考研的和以前真的不一样了 做过24年考研数学试卷的&#xff0c;应该都有一个感受&#xff0c;平时训练的好像都没用上。这是为啥啊。 其实是因为&#xff0c;现在的考研数学更加注重基础的考察&#xff0c;并且计…

sqllab第二十八关通关笔记(附带28a)

知识点&#xff1a; union select 整体过滤 union all select 替换where id(输入)空格 过滤了&#xff0c;使用%09代替 经过不断的测试&#xff0c;发现原始语句为 where id(输入) 构造payload:id1)and%091(1 成功回显出了相关的信息 好&#xff0c;尝试进行错误注入 构造…

git 安装、创建仓库、常用命令、克隆下载、上传项目、删除分支 -- 一篇文章总结

一、git安装 1、git安装地址&#xff1a;https://git-scm.com/downloads 2、选择操作系统 3、安装自己系统对应的操作位数 4、等待下载完&#xff0c;一路next安装就可以了 5、安装完成后&#xff0c;在任意文件夹点击右键&#xff0c;看到下图说明安装成功 二、创建仓库 1…