上一篇介绍了内存分段,这一篇将介绍内存分页。
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。即划分逻辑地址空间至固定大小的页(Page),划分物理内存空间至固定大小的帧(Frame),并建立方案,转换逻辑地址为物理地址(pages to frames)。在 Linux 下,每一页的大小通常为 4KB
。
页是连续的虚拟内存,帧是非连续的物理内存,不是所有的页都有对应的帧。
虚拟地址与物理地址之间通过页表来映射,页表是存储在内存里的,CPU 种的内存管理单元 (MMU,Memory Management Unit)就做将虚拟地址转换成物理地址的工作。
由于页的大小是固定的,就不会产生外部内存碎片问题,只有少量的内部碎片。
如果内存空间不够,操作系统会把其他正在运行的进程中的最近没被使用的内存页面给释放掉,也就是暂时写在硬盘上,一旦需要,再加载进来。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
在分页机制下,虚拟地址分为两部分,页号和页内偏移。
由于每一页的 长度L 固定,所以可以根据 逻辑地址A 计算得到 页号P 和 段内偏移量W。
页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址。
操作系统要为每个进程建立一张页表,页表通常在 PCB(进程控制块,Process Control Block) 中。在 32 位页表项中,高 20 位来存放页面的物理基地址,因此剩下的 12 位可用于存放属性信息。
页表的索引代表逻辑地址的页号。
当虚拟页不在物理内存中时,即第 0 位为 0 时,会存在以下两种情况:
因为页表一定要覆盖全部虚拟地址空间,所以在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。每个进程都需要使用 4MB 的内存来存储页表,所以会占据比较大的内存空间。
因为二级页表项只有在需要的时候才会被创建。所以可以通过多级页表的方式解决空间问题,比如使用二级页表的方式,原本需要 100多万个页表项才能表示所有虚拟地址,现在就只需要 1024 个页表项了。
把二级分页推广到多级页表,就会发现页表占用的内存空间更少了。对于 64 位的系统,两级分页肯定不够了,就变成了四级目录。
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,带来了额外的时间开销。
但是可以利用程序的局部性原理,在 CPU 芯片中加入一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Look-aside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。
TLB 存在于 CPU 中,属于 MMU 的一部分,可以简单地理解为页表的 Cache,其内容为部分页表的副本。
程序的局部性原理(Principle of locality):程序在执行过程中的一个较短时期,所执行的指令地址和指令的操作数地址,分别局限于一定区域。
举个例子,比如你在玩飞机大战时,开始指令只在最开头执行一次,而移动飞机和发射炮弹进行射击这些指令,则在开始后一大段时间内持续地执行,这就体现了时间局部性。再比如一个数组是连续存储在一起的,所以顺序遍历时就具备空间局部性。
当访问的页面存在于 TLB 当中,则称为 TLB 命中,否则称为 TLB 失效。失效分为以下两种:
软失效(soft miss):物理页在内存中,但是不在 TLB 中,此时需要刷新 TLB,将物理页加入到 TLB 中。
硬失效(hard miss):物理页既不在内存中,也不在 TLB 中,则需要从磁盘中加载。此时会触发一个缺页中断,将原有的读取内存的线程阻塞,然后从磁盘中读取数据到内存中,再触发中断更新 TLB,并唤醒线程进入就绪队列等待执行。
如果 TLB 已经满了,则需要使用缓存置换算法选择一个已经存在的条目进行覆盖。常用的缓存置换算法为 LRU(Least Recently Used,最近最不常用算法)。
TLB 有三种常见的设计方式:
8 路组相联映射是现在常用的方式。
在逻辑地址到物理地址的转换过程中,如果因某种原因导致无法访问到最终的物理内存单元,CPU 会产生一次缺页异常(Page Fault),从而进入缺页异常处理程序。
分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再通过触发却也中断的方式加载到物理内存里面去。
在请求页面的过程中,如果访问的页面不在物理内存中,就会产生一次缺页中断,在外部存储器中找到所缺的页面并将其载入物理内存。具体过程如下:
再加上 TLB 和考虑内存是否已满,整个分页管理的过程如下:
其实这张图还不够完整,比如并没有考虑到 TLB 满了需要置换的情况等。
缺页中断是由硬件所产生的一种特殊的中断,与一般的中断存在区别:
内存分页是把物理内存和虚拟内存都分成大小固定且相等的页进行使用,从而解决了内存分段有外部碎片的问题。
可以通过多级页表的方式减小页表所占的内存空间,但是太多级页表会降低地址转换的效率。
由于程序存在局部性,所以在 CPU 的 MMU 中设置了 TLB,即页表的缓存来加速常用的页表项的转换。
另外,给进程分配虚拟内存空间时,并不会直接分配所有的物理内存,而是在进程第一次使用页的时候触发缺页中断,进行内存映射,并将数据和代码载入内存中。