面试真题 | 深入理解Linux内核页表映射分页机制原理
深入理解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在页全局目录中相应表项的线性地址。仅用于内核页表。 | ||||
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。