【操作系统导论】内存篇——分页

引入

采用 「分段」 的方式,将空间切成 不同长度的分片,会出现 碎片化 问题,随着时间推移,分配内存会越来越困难。

因此,值得考虑「分页」的方法:

  • 将空间分割成 固定长度的分片

  • 将物理内存看成是定长槽块的阵列,叫作 页帧 (page frame,PF),每个页帧包含一个虚拟内存页。

「分页」具有许多优点:

  • 灵活性

    通过完善的分页方法,操作系统能够高效地提供地址空间的抽象,不管进程如何使用地址空间。

    例如,不用假定「堆」和「栈」的增长方向,以及它们如何使用。

  • 简单性

    假设有一个 64 字节的地址空间,且一个页帧为 16 字节,则只需要在物理地址空间中找到 4 个空闲页。

线性页表

地址转换

为了实现地址转换,需要将虚拟地址看作两个部分:*虚拟页面号(VPN)*和 页内偏移量(offset)

假设进程的虚拟地址空间是 64 字节,页帧大小为 16 字节:

  • 页帧大小 16 字节,对应 2 的 4 次方,则 offset 占 4 位;

  • 地址空间 64 字节,对应 2 的 6 次方,则 VPN 占 2 位。

【操作系统导论】内存篇——分页_第1张图片

转换虚拟地址,只需要将*「虚拟页面号 VPN」替换成「页帧号 PFN」*。

【操作系统导论】内存篇——分页_第2张图片

页表结构

为了记录地址空间的虚拟页在物理内存中的位置,OS 为每个进程保存一个数据结构,称为 页表(page table)

页表不由硬件存储,它通常存放在内存中,甚至可以被交换到磁盘上。

对于下面例子,页表中应该具有 4 个条目:(VP 0 → PF 3)、(VP 1 → PF 7)、(VP 2 → PF 5)、(VP 3 → PF 2) 。

【操作系统导论】内存篇——分页_第3张图片

那么,页表的结构究竟是怎么样的呢?

最简单的形式为 线性页表(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)。

【操作系统导论】内存篇——分页_第4张图片

可以阅读「英特尔架构手册」,以获取有关 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],它在地址空间中的分布如下:

【操作系统导论】内存篇——分页_第5张图片

在访问元素 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 的内容?

  1. **清空 TLB:**将所有项的有效位(valid)置为 0

这是可行的解决方案,进程不会读到错误的地址映射;

每次切换进程后,访问数据和页,都会触发 TLB 未命中;如果 OS 频繁切换进程,产生的开销很高。

  1. **添加 ASID:**区分不同的地址空间

如前面讲到的,ASID 使得 TLB 可以维护多个地址空间的映射。

【操作系统导论】内存篇——分页_第6张图片

还有一个问题:在向 TLB 添加新项时,应该替换哪个旧项?

  1. 最近最少使用

LRU 算法利用了内存引用流中的局部性,假定最近没有用过的项,可能是好的换出候选项。

  1. 随机策略

随机选择一项换出去,该策略不仅简单,并且可以避免一种极端情况:

程序循环访问 n+1 个页,但 TLB 只能存放 n 个页;该情况下,LRU 每次访问内存时都会触发 TLB 未命中。

多级页表

前面的讨论都是基于「线性页表」,但是*「线性页表」占用的内存很大* 。

假设一个 32 位地址空间,4KB 的页和一个 4B 的页表项;一个地址空间中大约有一百万个虚拟页面,乘以页表项的大小,则一个页表大小为 4MB。那么,一百个进程,就要占用数百兆的内存!

因此,要寻找新的技术来减轻这种沉重的负担,即:多级页表(multi-level page table)

基本思路

「多级页表」的思路如下:

  • 首先,将页表分成页大小的单元;

  • 通过 **页目录(page directory)**来追踪页表的页表项是否有效;

  • 如果在「页目录」中记录的页无效,则不为该页的页表分配内存。

以一个两级页表为例,**页目录项(Page Directory Entries,PDE)**中记录着一个 有效位页帧号 PFN

【操作系统导论】内存篇——分页_第7张图片

通过增加一个间接层「页目录」,我们可以将「页表的页单元」放在物理内存中的任何地方!

地址转换

现在,我们来构建一个二级页表。

假设有一个 16KB 的虚拟地址空间,页帧大小为 64 字节,虚拟地址划分为 8 位的 VPN 和 6 位的 offset;

那么,应该有 2 8 = 256 2^8=256 28=256个「页表项」,假设每个 PTE 的大小为 4 字节,则页表的大小为 1KB,占 16 个页帧。

我们将 VPN 的前 4 位划分为 页目录索引(PDIndex),剩余的位为 页表索引(PTIndex),如下所示:

【操作系统导论】内存篇——分页_第8张图片

  • 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),才能从页表中获取正确的地址转换信息。

因此,「多级页表」是 时间—空间折中 的。

你可能感兴趣的:(#,操作系统,linux)