引入
采用 「分段」 的方式,将空间切成 不同长度的分片,会出现 碎片化 问题,随着时间推移,分配内存会越来越困难。
因此,值得考虑「分页」的方法:
-
将空间分割成 固定长度的分片 ;
-
将物理内存看成是定长槽块的阵列,叫作 页帧 (page frame,PF),每个页帧包含一个虚拟内存页。
「分页」具有许多优点:
-
灵活性
通过完善的分页方法,操作系统能够高效地提供地址空间的抽象,不管进程如何使用地址空间。
例如,不用假定「堆」和「栈」的增长方向,以及它们如何使用。
-
简单性
假设有一个 64 字节的地址空间,且一个页帧为 16 字节,则只需要在物理地址空间中找到 4 个空闲页。
线性页表
地址转换
为了实现地址转换,需要将虚拟地址看作两个部分:*虚拟页面号(VPN)*和 页内偏移量(offset)。
假设进程的虚拟地址空间是 64 字节,页帧大小为 16 字节:
-
页帧大小 16 字节,对应 2 的 4 次方,则 offset 占 4 位;
-
地址空间 64 字节,对应 2 的 6 次方,则 VPN 占 2 位。
转换虚拟地址,只需要将*「虚拟页面号 VPN」替换成「页帧号 PFN」*。
页表结构
为了记录地址空间的虚拟页在物理内存中的位置,OS 为每个进程保存一个数据结构,称为 页表(page table)。
页表不由硬件存储,它通常存放在内存中,甚至可以被交换到磁盘上。
对于下面例子,页表中应该具有 4 个条目:(VP 0 → PF 3)、(VP 1 → PF 7)、(VP 2 → PF 5)、(VP 3 → PF 2) 。
那么,页表的结构究竟是怎么样的呢?
最简单的形式为 线性页表(linear page table),即一个数组。
操作系统通过「虚拟页面号 VPN」检索该数组,在索引处查找**「页表项 PTE」**,再找到对应的「页帧号 PFN」。
对于一个「页表项 PTE」,具有着许多的位,比如:
-
有效位(valid bit)
用于指示特定的地址转换是否有效。
通过将地址空间中所有未使用的页面标记为无效,则不再需要为这些页面分配物理帧,从而节省大量内存;
如果进程尝试访问这部分无效空间,就会陷入操作系统,可能会导致进程终止。
-
保护位(protection bit)
表明该页是否可以读取、写入、执行。
同样,以不被允许的方式访问该页,则会陷入操作系统。
-
存在位(present bit)
表明该页是在物理内存中还是在磁盘上。
-
访问位(accessed bit)
用于追踪页是否被访问,也用于确定哪些页比较受欢迎,应该保留在内存中。
-
脏位(dirty bit)
表明该页被带入内存后是否被修改过。
下面是 x86 架构的页表项:
包含了存在位(P),读/写位(R/W),用户/超级用户位(U/S),访问位(A),脏位(D);
(PWT、PCD、PAT 和 G)用来确定硬件缓存如何为这些页面工作,最后是页帧号(PFN)。
可以阅读「英特尔架构手册」,以获取有关 x86 分页支持的更多详细信息。
硬件 TLB
对于每个内存引用(取指令、显式加载、存储),分页需要执行一个额外的内存引用,以便从页表中获取地址转换。
这使得 额外的内存引用开销大,在这种情况下,可能会导致 系统运行速度减慢两倍或更多。
想要加速虚拟地址转换,自然要借助硬件的帮忙,即 地址转换旁路缓冲存储器(TLB),简称 地址转换缓冲 。
对每次内存访问,硬件先检查 TLB 中是否有期望的转换映射,如果没有,再访问页表。
TLB 带来了巨大的性能提升,实际上,因此它使得虚拟内存成为可能。
基本算法
现在假定使用「线性页表」和硬件管理的「TLB」,则算法的大体流程如下:
// get VPN
VPN = (VirtualAddress & VPN_MASK) >> SHIFT
// look up TLB
(Success, TlbEntry) = TLB_Lookup(VPN)
if (Success == True) // TLB Hit
{
if (CanAccess(TlbEntry.ProtectBits) == True)
{
Offset = VirtualAddress & OFFSET_MASK
PhysAddr = (TlbEntry.PFN << SHIFT) | Offset // get physical address from TLB
AccessMemory(PhysAddr) // access physical memory
}
else
{
RaiseException(PROTECTION_FAULT)
}
}
else // TLB Miss
{
PTEAddr = PTBR + (VPN * sizeof(PTE)) // get PTE address
PTE = AccessMemory(PTEAddr) // get PTE
if (PTE.Valid == False)
{
RaiseException(SEGMENTATION_FAULT)
}
else if (CanAccess(PTE.ProtectBits) == False)
{
RaiseException(PROTECTION_FAULT)
}
else
{
TLB_Insert(VPN, PTE.PFN, PTE.ProtectBits) // renew TLB
RetryInstruction() // retry this instruction
}
}
补充:TLB 未命中的情况,可能由硬件处理,也可能由软件(操作系统)处理。
假设有一个 8 位的虚拟地址空间,页帧大小为 16 字节,虚拟地址划分为 4 位的 VPN 和 4 位的 offset;
现在,我们要遍历一个整型数组 a[10]
,它在地址空间中的分布如下:
在访问元素 a[0]
、a[3]
、a[7]
时,TLB 会出现「未命中」的情况,整体的命中率为 70%。
对于典型的 4KB 大小的页来说,这种密集的数组访问会实现极好的 TLB 性能,每个页的访问只有一次「未命中」。
TLB 结构
典型的 TLB 有 32 项、64 项、128 项,并且是全相联的(fully associative)。
基本上,这就意味着一条地址映射可能存在 TLB 中的任意位置,硬件会并行地查找 TLB,找到期望的转换映射。
一条 TLB 项的结构:[ VPN | PFN | 其他位 ]
TLB 项中可能包括以下位:
-
有效位(valid bit)
表明该 TLB 项是不是有效的地址映射。
-
保护位(protection bit)
表明该页是否可以读取、写入、执行。
-
脏位(dirty bit)
表明该页被带入内存后是否被修改过。
-
全局位(Global bit)
表明该页是不是所有进程全局共享的。
-
地址空间标识符(ASID)
用于区分不同的地址空间,ASID 位的引入允许 TLB 维护多个地址空间的映射。
管理 TLB
首先,在进行上下文切换时,TLB 会面临一些问题。
假设进程 P1 在 TLB 中缓存了有效的地址映射:VPN 10 → PFN 100;
操作系统进行上下文切换,运行进程 P2,P2 也在 TLB 中缓存一条地址映射:VPN 10 → PFN 170;
那么,在上下文切换时,如何管理 TLB 的内容?
- **清空 TLB:**将所有项的有效位(valid)置为 0
这是可行的解决方案,进程不会读到错误的地址映射;
每次切换进程后,访问数据和页,都会触发 TLB 未命中;如果 OS 频繁切换进程,产生的开销很高。
- **添加 ASID:**区分不同的地址空间
如前面讲到的,ASID 使得 TLB 可以维护多个地址空间的映射。
还有一个问题:在向 TLB 添加新项时,应该替换哪个旧项?
- 最近最少使用
LRU 算法利用了内存引用流中的局部性,假定最近没有用过的项,可能是好的换出候选项。
- 随机策略
随机选择一项换出去,该策略不仅简单,并且可以避免一种极端情况:
程序循环访问 n+1 个页,但 TLB 只能存放 n 个页;该情况下,LRU 每次访问内存时都会触发 TLB 未命中。
多级页表
前面的讨论都是基于「线性页表」,但是*「线性页表」占用的内存很大* 。
假设一个 32 位地址空间,4KB 的页和一个 4B 的页表项;一个地址空间中大约有一百万个虚拟页面,乘以页表项的大小,则一个页表大小为 4MB。那么,一百个进程,就要占用数百兆的内存!
因此,要寻找新的技术来减轻这种沉重的负担,即:多级页表(multi-level page table)。
基本思路
「多级页表」的思路如下:
-
首先,将页表分成页大小的单元;
-
通过 **页目录(page directory)**来追踪页表的页表项是否有效;
-
如果在「页目录」中记录的页无效,则不为该页的页表分配内存。
以一个两级页表为例,**页目录项(Page Directory Entries,PDE)**中记录着一个 有效位 和 页帧号 PFN:
通过增加一个间接层「页目录」,我们可以将「页表的页单元」放在物理内存中的任何地方!
地址转换
现在,我们来构建一个二级页表。
假设有一个 16KB 的虚拟地址空间,页帧大小为 64 字节,虚拟地址划分为 8 位的 VPN 和 6 位的 offset;
那么,应该有 2 8 = 256 2^8=256 28=256个「页表项」,假设每个 PTE 的大小为 4 字节,则页表的大小为 1KB,占 16 个页帧。
我们将 VPN 的前 4 位划分为 页目录索引(PDIndex),剩余的位为 页表索引(PTIndex),如下所示:
-
PDEAddr = PageDirBase + (PDIndex * sizeof(PDE))
-
PTEAddr = (PDE.PFN << SHIFT) + (PTIndex * sizeof(PTE))
// get VPN
VPN = (VirtualAddress & VPN_MASK) >> SHIFT
// look up TLB
(Success, TlbEntry) = TLB_Lookup(VPN)
if (Success == True) // TLB Hit
{
if (CanAccess(TlbEntry.ProtectBits) == True)
{
Offset = VirtualAddress & OFFSET_MASK
PhysAddr = (TlbEntry.PFN << SHIFT) | Offset
Register = AccessMemory(PhysAddr) // access physical memory
}
else
{
RaiseException(PROTECTION_FAULT)
}
}
else // TLB Miss
{
// first, get page directory entry
PDIndex = (VPN & PD_MASK) >> PD_SHIFT
PDEAddr = PDBR + (PDIndex * sizeof(PDE))
PDE = AccessMemory(PDEAddr) // get PDE
if (PDE.Valid == False)
{
RaiseException(SEGMENTATION_FAULT)
}
else
{
// PDE is valid: now fetch PTE from page table
PTIndex = (VPN & PT_MASK) >> PT_SHIFT
PTEAddr = (PDE.PFN << SHIFT) + (PTIndex * sizeof(PTE))
PTE = AccessMemory(PTEAddr) // get PTE
if (PTE.Valid == False)
{
RaiseException(SEGMENTATION_FAULT)
}
else if (CanAccess(PTE.ProtectBits) == False)
{
RaiseException(PROTECTION_FAULT)
}
else
{
TLB_Insert(VPN, PTE.PFN, PTE.ProtectBits)
RetryInstruction()
}
}
}
时空折中
「多级页表」是有成本的。
当 TLB 未命中时,需要从内存加载两次(先访问页目录,后访问 PTE),才能从页表中获取正确的地址转换信息。
因此,「多级页表」是 时间—空间折中 的。