当前位置: 首页 > 科技观察

Linux内核页表管理——那些鲜为人知的秘密

时间:2023-03-12 05:50:31 科技观察

本文转载自微信公众号《Linux内核航海者》,作者Linux内核航海者。转载本文请联系Linux内核航海者公众号。1.开场备注环境:处理器架构:arm64内核源码:linux-5.11ubuntu版本:20.04.1代码阅读工具:vim+ctags+cscope通用操作系统,通常启用mmu,支持虚拟内存管理,页表管理在虚拟内存管理中尤为重要。本文主要通过回答页表管理中的几个关键问题来分析Linux内核页表管理,一窥页表管理中鲜为人知的秘密。2、页表的作用是什么?1)地址翻译将虚拟地址转换为物理地址2)权限管理管理CPU对物理页面的访问权限,如读、写、执行权限3)隔离地址空间隔离各个进程的地址空间,使它们互不影响并提供系统安全。开启mmu后,在没有页表映射或者有页表映射但没有访问权限的情况下访问虚拟内存时会出现处理器异常。内核选择killer或panic;通过页表将一段内存设置为用户态不可访问,使处于用户态的用户进程无法访问内核地址空间的内容;并且由于用户进程直接拥有自己的一套页表,他们看不到彼此的地址空间,更谈不上访问,造成每个进程都认为自己拥有所有虚拟内存的错觉;通过页表为一段内存设置只读属性,则不允许修改这段内存的内容,从而保护这段内存不被改写;对应用户进程地址空间映射的物理内存,内核可以方便的进行页面迁移和页面交换,对使用虚拟地址的用户进程是透明的;通过页表,很容易实现内存共享,使一个共享库的多个进程可以映射到自己的地址空间中使用;通过页表,小内存可以加载大应用程序运行,运行时按需加载和映射……3、页表存放在哪里?页表存放在物理内存中,打开mmu后,如果需要修改页表,需要将页表所在的物理地址映射到一个虚拟地址,才能访问页表(例如,内核初始化后,会对物理内存进行线性映射,这样就可以通过物理地址和虚拟地址的偏移量得到page表的物理地址对应的虚拟地址)。4、页表项中的存储是虚的还是实的?页表的基地址和各级页表项存储在物理地址中,而不是虚拟地址。5、开启mmu后的地址转换过程?虚拟地址转换为物理地址的过程:开启mmu后,cpu访问虚拟地址。当cpu访问一个虚拟地址时,会通过cpu内部的mmu查询物理地址。mmu首先通过虚拟地址在tlb中查找。如果找到对应的表项,则直接获取物理地址;如果没有找到tlb,就会通过虚拟地址从页表基地址寄存器中存放的页表基地址开始查询多级页表,最后查询到对应的表项后,将表项缓存在tlb中,然后从entry中获取物理地址。6、Linux内核为什么要使用多级页表?1)使用一级页表结构的优缺点:优点:只有两次访问内存(一次访问页表,一次访问数据),效率高,实现简单缺点:连续一大块内存存储每个进程的页表(比如32位系统的每个进程需要一个4M的页表),浪费内存。虚拟内存越大,页表越大,内存碎片化时,很难分配连续的大块内存,大部分虚拟内存没有被使用。2)使用多级页表结构的优缺点:优点:1.节省内存;复杂度更高3)Linux内核综合考虑:在典型的时间换空间中,各级页表可以放在物理内存的任意位置,无论是硬件遍历还是内核遍历,都比第一种复杂级页表,但是为了节省内存,内核选择了多级页表结构。7、减少多级页表遍历的优化?1)在mmu中加入tlb,缓存最近访问过的页表项。根据程序时间和空间的局部性原则,tlb可以有很高的命中率。2)使用hugepages减少内存访问次数(比如使用1G或2M的hugepages),可以减少tlbmisses和pagefault异常。8、硬件有什么作用?遍历页表,将va转换为pa。涉及页权限管理的硬件是:mmu->function:查询tlb或遍历页表tlb->function:缓存最近转换的页表项页表基地址寄存器如ttbr0_el1ttbr1_el1->function:存储页表基地址(物理地址)作为mmu遍历多级页表的起点。mmu遍历多级页表时,发现虚拟地址最高位为1时,使用ttbr0_el1作为遍历起点,当最高位为0时,使用ttbr1_el1作为遍历起点。9.软件有什么作用?1)应用程序可以访问虚拟内存,如执行指令、读写内存,而无需管理页表,而不管虚拟内存是如何转换成物理内存的,对应用程序是透明的。2)Linux内核填写页表,告诉mmu内核页表的基地址,初始化构建内核页表,实现页错误异常等机制,为用户任务分配映射页表要求。当然内核也可以遍历页表,比如发生缺页时遍历进程页表。10、内核中涉及的页表基地址?kernel:idmap_pg_diridentitymappingpagetable(va=pamapping2M)init_pg_dircoarse-grainedkernelpagetableswapper_pg_dirmainkernelpagetableuser:tsk->mm->pgduserprocessfork分配私有pgd页时,用于保存pgd表项(只分配一级页表)。11、页表填充/切换时序1)内核页表填充内核初始化过程:物理地址->恒等映射(建立恒等映射页表和粗粒度内核页表)->开启mmu->paging_init(建立细粒度内核页表和内存线性映射)->...身份映射阶段:保存身份映射页表idmap_pg_dir地址到ttbr0_el1保存粗粒度内核页表init_pg_dir地址到ttbr1_el1paging_init阶段:保存内核主页表swapper_pg_dir地址Discard在ttbr1_el1paging_init之后使用idmap_pg_dir和init_pg_dir页表。2)访问时的用户页表填充和页错误填充:当用户进程访问已经申请的虚拟内存时,发生页错误,页错误处理程序为进程分配各级页表等物理页并建立页表映射关系。进程切换时切换进程页表:switch_mm时将tsk->mm->pgd切换到ttbr0_el1,asid切换到ttbr1_el1,从而完成进程地址空间切换。12、页表遍历过程以arm64处理器架构的多级页表遍历结束(使用4级页表,页大小为4K):在Linux内核中,页表可以扩展为5级,分别是页面全局目录(PageGlobalDirectory,PGD)、页面第四目录(Page4thDirectory,P4D)、页面上层目录(PageUpperDirectory,PUD)、页面中间目录(PageMiddleDirectory,PMD)、直接页表(PageTable,PT),而支持arm64的linux使用的是4级页表结构,即pgd、pud、pmd、pt。在arm64手册中,称为L0、L1、L2、L3层级转换表,所以用L0-L3来表示各级surface的页面。当tlb未命中时,mmu会进行多级页表遍历。遍历过程如下:1.mmu根据虚拟地址的最高位判断以哪个页表基地址寄存器为起点:当最高位为0时,以ttbr0_el1为起点(访问的是用户空间地址);当最高位为1时,以ttbr1_el1为起点(访问的是内核空间地址)mmu从对应的页表基地址寄存器中获取L0转换表基地址。2.找到L0级转换表,然后从虚拟地址中获取L0索引,通过L0索引找到对应的表项(arm64中称为L0表描述符,内核中称为PGD表项),得到L1从表条目表基地址转换。3.找到L1级翻译表,然后从虚拟地址中获取L1索引,通过L1索引找到对应的表项(在arm64中称为L1表描述符,在内核中调用PUD表项),得到L2转换自表项表基地址。4.找到L2级翻译表,然后从虚拟地址获取L2索引,通过L2索引找到对应的表项(arm64中称为L2表描述符,内核中称为PUD表项),获得L3翻译从表条目表基地址。5.找到L3级转换表,然后从虚拟地址中获取L3索引,通过L3索引找到页表项(在arm64中称为页描述符,在内核中称为页表项)。6、从页表项中取出物理页框号,加上物理地址偏移量(VA[11,0]),得到最终的物理地址。