Linux深入理解内存管理2(基于Linux6.6)---分页机制
一、分页实现原理
在分段的映射方法中,并没有解决内存使用效率的问题。如果应用程序过多,或者内存碎片过多,又或者曾经被换出到硬盘的内存段需要再重新装载到内存,可内存中找不到合适大小的区域,要如何解决这个问题,就引入了分页机制。
Linux 操作系统采用了硬件分页(hardware paging)来管理虚拟内存。硬件分页机制利用硬件支持(通常是 CPU 的 MMU,内存管理单元)将虚拟地址转换为物理地址。分页是虚拟内存管理的核心机制之一,旨在实现内存隔离、内存共享和高效的内存利用。
1.1、分页的基本概念
分页是将虚拟内存和物理内存划分为固定大小的块,这些块称为页面(page)。当程序访问内存时,它使用虚拟地址,操作系统通过硬件支持将虚拟地址转换为物理地址。
- 虚拟地址空间(Virtual Address Space):每个进程都有一个独立的虚拟地址空间,操作系统通过硬件和内核管理该空间。
- 物理地址空间(Physical Address Space):物理内存中的实际内存地址。
- 页面(Page):虚拟内存和物理内存的基本单位,通常是 4KB 或 2MB(大页),甚至更大(如 1GB 大页)。
- 页表(Page Table):用来存储虚拟地址和物理地址之间映射关系的数据结构。
分页的基本方法是将地址空间等分成某一个固定大小的页;每一页大小由硬件来决定,或者是由操作系统来决定(如果硬件支持多种大小的页)。
- 1.将进程的逻辑地址空间分成若干个大小相等的片,称为页面或页
- 2.内存空间分成与页大小相等的若干个存储块,称为物理块或页框
- 3.在为进程分配内存时,以块为单位,将进程中的若干页分别装入多个可以不相邻的块中
关于进程分页,当我们把进程的虚拟地址空间按页来分割,常用的数据和代码会被装在到内存;暂时没用到的是数据和代码则保存在磁盘中,需要用到的时候,再从磁盘中加载到内存中即可.
1.2、硬件分页机制
对于最简单的分页机制,硬件上使用一级页表的方式是最简单的,访问效率也最高,页面的大小一般为 4KB。为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了,其访问流程图如下:
虚拟地址分为两部分,页号§和页内偏移(o)。页号(用高 20 位表示)作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移(低 12 位)的组合就形成了物理内存地址。一级页表这么简单,只要经过一次的地址转换就能找到对应的物理地址,访问效率应该是最好的。
我们假设在32位环境下,虚拟的地址空间为4GB,如果采用一级页表,采用4KB为一个页,那就需要1M个页表。每一个页表需要4个字节来存储,那么整个4GB的地址空间的映射就需要4MB的内存来存储映射表。如果每个进程都有自己的映射表,100个进程就需要400MB的内存,对于内核来说,确实有点大。
这个问题在64位体系结构下, 情况会更加糟糕. 而每个进程都需要自身的页表, 这导致系统中大量的所有内存都用来保存页表。
1.3、多级页表
对于页表中所有页表项必须提前建好,并且要求是连续的。如果不连续,就没有办法通过虚拟地址里面的页号找到对应的页表项了。为减少页表的大小并容许忽略不需要的区域, 计算机体系结构的就使用了多级页表,下面以二级页表,看硬件上怎么实现的。
- 第一级表称为页目录,存放在一页 4K 大小的页面中,具有 2^10 个 4 字节长度的表项。 这些表象指向对应的二级表。 线性地址的最高 10 位(31-22)用作以及表中的索引。
- 第二级称为页表,长度也是 4K 大小的一个页面,最多有 1K 个 4 字节的表项。 每个 4 字节的表项含有相关页面的 20 位物理基地址。 二级页表使用线性地址的中间 10 位(21-12)作为表项索引值,以获取含有页面 20 物理地址基地址的表项。 该20位页面物理基地址和线性地址中的低12位(页内偏移)组合在一起就得到了分页转换过程的输出值,即对应的的最终物理地址。
对于给定的线性地址,CR3 寄存器指定页目录表的基地址。线性地址的高10位用于索引这个页目录表,以获得指向相关第二级页表的指针。线性地址空间中间10位用于索引二级页表,以获得物理地址的高20位。线性地址的低12位直接作为物理地址的低12位,从而组成一个完整的32位物理地址。
那么二级页表怎么解决页表过大的问题呢?我们假设只给这个进程分配了一个数据页。如果只使用页表,也需要完整的 1M 个页表项共 4M 的内存,但是如果使用了页目录,页目录需要 1K 个全部分配,占用内存 4K,但是里面只有一项使用了。到了页表项,只需要分配能够管理那个数据页的页表项页就可以了,也就是说,最多 4K,这样内存就节省多了。
页目录和页表的表项格式如下图所示,其中位32-12含有物理地址的高20位,用于定位物理地址空间中一个页面(也叫页帧)的物理基地址。表项的低 12 位含有页属性信息。
上图就是页目录项和页表项的格式。可以看出,由于页表或者页的物理地址都是4KB对齐的(低12位全是零),所以上图中只保留了物理基地址的高20位(bit[31:12])。低12位可以安排其他用途。
- 【P】存在位,表示该页是在内存还是在磁盘。为1表示页表或者页位于内存中。否则,表示不在内存中,必须先予以创建或者从磁盘调入内存后方可使用。
- 【R/W】:读写标志。为1表示页面可以被读写,为0表示只读。当处理器运行在0、1、2特权级时,此位不起作用。页目录中的这个位对其所映射的所有页面起作用。
- 【PWT】:缓冲写策略。Page级的Write-Through标志位。为1时使用Write-Through的Cache类型;为0时使用Write-Back的Cache类型。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。对于我们的实验,此位清零。
- 【PCD】:禁止缓存位。Page级的Cache Disable标志位。为1时,物理页面是不能被Cache的;为0时允许Cache。当CR0.CD=1时,此标志被忽略。对于我们的实验,此位清零。
- 【D】:修改位。该位由处理器固件设置,用来指示此表项所指向的页是否写过数据。
- 【A】:访问位。该位由处理器固件设置,用来指示此表项所指向的页是否已被访问(读或写),一旦置位,处理器从不清这个标志位。这个位可以被操作系统用来监视页的使用频率。
正常来说, 对于32位的系统两级页表已经足够了, 但是对于64位系统的计算机, 这远远不够.
首先假设一个大小为4KB的标准页, 所以offset字段需要12位.这样线性地址空间就剩下64-12=52位分配给页中间表Table和页目录表Directory。如果我们现在决定仅仅使用64位中的48位来寻址(这个限制其实已经足够了, 2^48=256TB,即可达到256TB的寻址空间)。剩下的48-12=36位被分配给Table和Directory字段, 即使我们现在决定位两个字段各预留18位,那么每个进程的页目录和页表都包含218个项, 即超过256000个项.
基于这个原因, 所有64位处理器的硬件分页系统都使用了额外的分页级别. 使用的级别取决于处理器的类型
二、总结
1. 分页的工作流程
硬件分页的基本过程包括以下几个步骤:
-
虚拟地址访问:当程序访问一个虚拟地址时,CPU 会将该虚拟地址分为两部分:
- 页号(Page Number):用于定位页表条目。
- 页内偏移(Page Offset):用于定位页内的具体位置。
-
页表查找:CPU 使用页号在页表中查找对应的物理页面。
- 页表项包含虚拟地址到物理地址的映射关系。如果该页表项存在并且有效,则继续执行;否则触发 缺页异常(Page Fault)。
-
地址转换:一旦找到页表项,它将虚拟页号映射到物理页号。结合页内偏移,CPU 计算出最终的物理地址。
-
内存访问:通过计算出的物理地址,CPU 可以访问对应的物理内存。
2. 页表结构
Linux 操作系统依赖硬件提供的页表结构。现代 CPU 通常采用多级页表结构,以优化内存使用和提升查找效率。典型的多级页表结构有以下几种:
- 单级页表(Single-Level Page Table):早期的简单设计,但不适用于大规模的虚拟内存。
- 多级页表(Multi-Level Page Table):采用多个级别的页表来减少内存占用。例如,x86 架构通常使用三层或四层页表结构。
在 Linux 中,常见的页表结构包括:
- 页目录(Page Directory):指向下一级页表。
- 页表(Page Table):每个条目对应一个物理页面。
典型的 x86-64 架构页表结构:
在 64 位 x86 架构中,虚拟地址空间被分为 4 层:
- PGD(Page Global Directory)
- PUD(Page Upper Directory)
- PMD(Page Middle Directory)
- PTE(Page Table Entry)
每一级的页表都包含指向下一级页表的指针,最终在最低级的页表(PTE)找到虚拟地址对应的物理地址。
3. 页表项(Page Table Entry,PTE)
每个页表项(PTE)保存了虚拟地址到物理地址的映射。PTE 包含以下信息:
- 物理页框号(Physical Page Frame Number):指向物理内存的页面。
- 有效位(Present bit):指示该页是否有效。如果该位为 0,表示该虚拟页面尚未映射到物理内存,访问时会触发缺页异常。
- 修改位(Dirty bit):指示该页面是否被写入过。若页面被写入,则会被标记为脏页。
- 访问位(Accessed bit):指示该页面是否被访问过。
- 用户模式/内核模式访问位(User/Supervisor bit):表示该页是否可以在用户模式下访问。
- 页大小(Page Size bit):指示该页是否为大页面(如 2MB 或 1GB 页面)。
4. 页表缓存(TLB)
为了提高分页的效率,CPU 通常配有 TLB(Translation Lookaside Buffer),即快表缓存。TLB 是一个缓存,用于存储最近访问的虚拟地址到物理地址的映射。它避免了每次访问内存时都需要查找完整的页表,从而提高性能。
- TLB Miss:如果虚拟地址不在 TLB 中,CPU 将通过页表查找转换。
- TLB Reload:当页表更新时,TLB 需要刷新,以确保虚拟地址映射是最新的。
5. 缺页异常(Page Fault)
缺页异常是当程序访问一个尚未映射到物理内存的虚拟地址时发生的。Linux 操作系统会捕获这一异常,并根据具体情况执行以下操作:
- 无效访问:如果程序访问的是一个无效的虚拟地址(例如,访问未分配的内存),内核会终止该进程,产生段错误(Segmentation Fault)。
- 懒惰分配:如果虚拟页面尚未分配给物理内存,操作系统会根据需要分配物理内存并更新页表。
- 交换(Swap):如果物理内存不足,操作系统可能会将页面交换到磁盘,并将所需页面加载到内存。
6. 大页(Huge Pages)
为了减少多级页表的查找次数并提高性能,现代操作系统支持大页面(Huge Pages),通常是 2MB 或 1GB 大小的大页。大页面减少了页表的级别,提高了内存的访问效率,但也增加了内存的浪费(因为内存以较大块分配)。
7. 内核空间与用户空间
在 32 位系统中,通常将虚拟地址空间的一部分划分给内核空间,另一部分划分给用户空间。内核空间通常是高地址(例如 3GB 到 4GB),而用户空间则是低地址。Linux 的内核通过硬件分页机制对这两个空间进行隔离,避免用户程序直接访问内核内存。
在 64 位系统中,虚拟地址空间的大小更大,但同样分为内核空间和用户空间,用户空间的访问受到严格限制。