虚拟内存之页表

为何需要逻辑地址

由于系统中的物理内存是随分配不断在变化的,有时候这个程序使用,有时候那个程序在使用。如果不使用逻辑地址直接使用物理地址,那当前进程操作的地址被占用,则不能使用内存。通过将连续的逻辑地址映射成不连续的物理地址,程序将只用关系的连续的逻辑地址,而物理地址再通过一些方法找到并映射过去就行了。

考虑一种简单的映射方法:

如果简单的使用<物理地址起始地址,大小>实现:程序A申请前0-199的内存,程序B申请200-299的内存。但程序A的200内存并没有完成使用,大部份是空闲的,将会造成内存浪费。这种其实地址加大小的寻址方式就是所谓的 可变分区管理。容易产生外碎片。

如果此时程序A释放,程序C想要申请300的内存,但是程序A释放的0-199不足以满足C的要求,C需要寻找更大块的内存,而程序A释放导致的空闲内存一直没被用上,导致浪费。

  • 内存碎片
    • 内碎片:申请一段空间,但是没有完全使用,一部分内存的空闲的,造成浪费
      • 用不上

    • 外碎片:申请一段空间,但是前边剩余的一段不满足,需要找后边的大块内存。前边剩余的内存一直用不上操作从浪费
      • 塞不下

因此引入分页机制来优化内存碎片问题

分页

将程序的逻辑地址空间分为若干等大的页,称为 页/页面/虚页 。同样将物理内存分成若干项大小同虚页大小的页,称为 块/页帧/实页。虚页是连续的,实页是不连续的,页表就维护了虚页到实页的映射关系,页表中每一项称为页表项(PTE),表示一个虚页到实页是映射关系。每个进程都有自己的页表,称为进程页表

程序中使用的虚地址大致如下:

|页号|页内偏移|

页表中的内容大致如下(当然还可以有一些标志信息,如是否有读写权限、是否可用等):

页号1: |标记|物理页号|
页号2: |标记|物理页号|
页号3: |标记|物理页号|
页号4: |标记|物理页号|

标注页号仅表示页表项之间的位置关系(连续),不占用实际内存

每个进程为了找到自己的进程页表还需要 页表基地址寄存器(PTBR) 的帮忙,其指向进程页表的实页号/页表起始地址(PPN)。进程切换时会切换PTBR的值。

注意 ,页号表示页表项之间的位置关系(连续),实际不占物理内存,通过PTBR+页号x页表项大小算出目标地址

由虚地址到实际物理地址的变化叫做内存映射,过程如下:

┌────┐    ┌────────┬─────────┐
 │PTBR│    │虚拟页号│ 页内偏移│             主存
 └─┬──┘    └──┬─────┴────┬────┘        ┌─────────────┐
   │          │          │             │             │
   │          │          │             │             │
   │          │          │             │             │
   │          │          │             ├─────────────┤
   └──────────+──────────┼────────────►│             │
                         │   物理页号  │page table   │
                         x─◄───────────┤             │
                         │             ├─────────────┤
                         │             │             │
                         │             │             │
                         │             │             │
                         │  物理地址   │             │
                         └───────────► │             │
                                       │             │
                                       │             │
                                       └─────────────┘

如果对应地址缺页,则会触发缺页中断。进入内核态(异常/中断型),从磁盘换入或加载内存并填写到内存中,并填写帧号,然后重新进行寻址过程。

0x000011a3的地址映射过程

  • 逻辑地址32位=20位页号+12位页内偏移
  • 页表项32位=20位块号(与20位页号对应)+12位标记位
  • 物理地址=20位块号+12为页内偏移

页表如下:

起址:PTBR
 +页号        |标记| 帧号         |
              |----|--------------|
 +0x00001  -> |    | 0x000f3      |

第一次访问内存,通过PTBR+0x00001找到对应的页表项的地址,读取物理页号/页帧/实页号。再通过帧号0x000f3拼接页内偏移0x1a3得到物理地址(不需要加法,一部分高位,一部分低位,直接拼接更快),第二次访问内存读取数据。至少需要两次内存访问

分页机制的优化

要真正获取一个内存中的内容实际需要加载两次内存,第一次是查页表找到物理页号,第一次是根据物理页号加页内偏移到对应内存,因此可以进行时间上的优化。而每个进程都需要维护一个页表,页表本身也是占内存空间的,因此还可以进行空间优化。

空间优化与多级页表

引入多级页表有两个原因

  • 如果页表大小超过了一个页面的大小/容量,则可能将数据存放到两个不同的页面,将无法通过PTBR+页号的方式找到
    • 如通常一个页4KB,32位系统中一PTE有32位4B,故PTE数不应超过1k

  • 进程页表本身也是在内存中的,需要占用内存空间

如果不考虑页面容量问题,假设一个页能容纳所有页表项。一个32位的系统和程序,如果页内偏移12位,要访问全部4G内存则需要2^20个物理块/物理页面/物理页号,对应就有2^20个页号/虚页号,即2^20个页表项:

|12位标记位|20位表示物理页号|

这么一个页表项32bit=4B,则维护一个进程的页表需要占用2^20x4B=4MB的内存空间。4MB看起来很小,但是操作系统是要运行很多程序的。

如果使用多级页表,应当页表大小==页面容量 ,如将32位分为10位,10位,12位的分级方式。为何10位?10位能索引2^10个PTE,而每个PTE占4B,一个页表刚好就4KB,与页容量相同。如果小于页容量会产生内碎片,浪费;如果大于页容量则无法通过页表起址+offset寻址。

64位系统中则常用9位索引页表,64bit=8B,8Bx2^9=4KB,也刚好是一个页的大小。
虚地址:
| VPN[1] | VPN[0] | 页内偏移 |

PTE:
| PPN[1] | PPN[0] | 页内偏移 |

PTBR指向一级页表起始地址,加上VPN[1]可以索引到对应页表项,一级页表中的PPN[1]PPN[0]就是二级页表的起始地址,再通过VPN[0]索引二级也被即可得到物理页号,加上页内偏移得到物理地址。

┌────┐   ┌──────┬─────┬───────┐
│PTBR│   │ VPN1 │VPN0 │offset │
└─┬──┘   └──┬───┴──┬──┴───┬───┘               ┌─────────────┐
  │         │      │      │                   │             │
  │         │      │      │                   ├─────────────┤
  └─────────x──────┼──────┼──────────────────►│xxxxxxxxxxxxx│
                   │      │                   │x L0       xx│
                   │      │     PPN1,PPN0     │x          xx│
                   x──────┼─────◄─────────────┤xxxxxxxxxxxxx│
                   │      │                   ├─────────────┤
                   │      │                   │             │
                   │      │                   │             │
                   │      │                   │             │
                   │      │                   ├─────────────┤
                   └──────┼──────────────────►│xxxxxxxxxxxxx│
                          │                   │x L1      xxx│
                          │     PPN1,PPN0     │x         xxx│
                          x─────◄─────────────┤xxxxxxxxxxxxx│
                          │                   ├─────────────┤
                          │                   │             │
                          │                   │             │
                          │                   │             │
                          │                   │             │
                          │                   │             │
                          └──────────────────►│             │
                                              │             │
                                              │             │
                                              │             │
                                              │             │
                                              │             │
                                              └─────────────┘

如果发现第一级只有3项指向第二级,而第二级页表有2^10项,所以一共3个页表需要(2^10+3x2^10)x4B = 16KB的内存空间。

当然上限还是2^10x2^10个表项x4B占4M空间。

问题就是内存的访问次数增加了

时间优化

前面使用多级页表的方式虽然,减少了空间占用,但是内存的访问次数增加了。

TLB

将最常访问的几个(一般8-128个左右)页表项储存到访问速度更快的硬件中,如关联存储器(相当于哈希表),这个小表的名称为TLB (Translation Lookaside Buffer) 快表 。寻址会同时寻找TLB和页表,如果TLB命中则查页表寻址的操作作废。

使用大页

依旧上述多级页表的例子,但是使用某种机制让大页表地址连续,那么就可以使用起址+offset的方式寻址,而不需要多级页表多次访问内存。

虚地址:
| VPN[1] | VPN[0] | 页内偏移 |

PTE:
| PPN[1] | PPN[0] | 页内偏移 |

只是访问一级页表(L0)时其返回PPN[1]作为大页的索引(高10位),然后使用虚拟地址的<VPN[0],页内偏移>所谓大页的页内偏移。如此一来访问次数就降低了。

┌────┐ ┌──────┬─────┬───────┐
│PTBR│ │ VPN1 │VPN0 │offset │
└─┬──┘ └──┬───┴──┬──┴──┬────┘
  │       │      │     │
  │       │      └──┬──┘         ┌─────────────┐
  │       │         │            │             │
  │       │         │            ├─────────────┤
  └───────x─────────┼───────────►│xxxxxxxxxxxxx│
                    │            │x L0       xx│
                    │  PPN1,     │x          xx│
                    x───────◄────┤xxxxxxxxxxxxx│
                    │            ├─────────────┤
                    │            │             │
                    │            │             │
                    │            │             │
                    │            ├─────────────┤
                    └───────────►│             │
                                 │  huge page  │
                                 │             │
                                 │             │
                                 │             │
                                 │             │
                                 │             │
                                 │             │
                                 ├─────────────┤
                                 └─────────────┘
编辑于 2022-03-05 00:41