上一篇文章中我们解释了虚拟内存的基本实现原理,但是也存在一级页表过多占用内存的问题,并且每次在从内存中访问页表也会增加额外的读取操作。这篇文章将详细解释为什么多级页表可以节省内存空间,以及TLB如何减少地址映射带来的开销。我们将首先从多级页表开始谈起。
多级页表
为了方便理解,我们先从二级页表开始图解。假设虚拟内存和物理内存都是4G, 地址32位,页面大小4K。虚拟地址的表示如下:
VPN0 是第一级页表项的索引, 第一级页表的页表项(PTE)共有1024 个页表项,每个页表项指向第二级页表的起始地址。VPN1是第二级页表项的索引,每个二级页表也有 1024 个 页表项,二级页表中页表项存储物理页号,即物理页对应的起始地址。如下图所示。
此时我们来分析每级页表的内存占用,每个二级页表占用 1024 4B= 4KB 的存储空间,二级页表则需要1024 * 4KB = 4MB; 一级页表 10244B=4KB. 总的需要 4M+4KB,这似乎比只有一级页表(4MB)的占用还要多一些,怎么能节省空间呢?
但是通常情况下,一个进程可能只占用整个虚拟内存一部分。如果此时某个第一级页表项为空的话,操作系统将不会存储对应的二级页表。举个极端的例子,假如第一级页表只有前两个页表项(PTE0和PTE1)不为空,那么操作系统将只会存储两个二级页表项,此时的内存占用便只有 4KB*2 + 4KB =12KB. 是不是极大的节省了内存空间。
在理解了二级页表之后,多级页表只不过是二级页表的一个扩展。假设有 k 级页表,则第 k 级页表的页表项为 PPN,其余 j (1<= j <= k-1)级 页表项存储的都是第 j+1 级页表的起始地址。如果第 j 级页表项的某个页表项为空,那么 其对应的下一级页表也将不需要存储。这样也就节省了存储空间。
虽然多级页表节省了内存空间,但是对于 k 级页表而言,一次地址映射将会额外增加 k 次内存访问,这无疑是不可接受的。其实,在处理器中,还有一个专门的硬件单元 MMU 来负责内存地址映射。
MMU
MMU 是处理器中集成的一个专门用于地址管理的硬件。它不仅可以提供访存保护,还会极大的提高地址映射的速度。根据程序的局部性原理,MMU 中添加了叫做翻译后备缓冲区(TLB)的缓存单元,它可以将常用的页表项缓存,减少访存次数。我们可以通过一个例子Core i7 来展示MMU与处理器的关系。从如下图,我们可以看到,TLB 与 L1/L2缓存的结构非常相似,只不过容量相对比较下,每一级TLB缓存也只能存储最多上百个条目。此外,在这个多核系统中,每个核心上都会有自己独立的 TLB 缓存。这也为我们以后理解多线程提供了基础。比如,如果我们我们在一个核上跑多个线程,那么在线程切换时就可能会重新刷新TLB和L1/L2缓存区,这无疑也是需要权衡的开销。目前我在做深度学习优化时,通常采用一个物理核绑定一个线程的配置处理。
总结及实际应用
结合上篇文章,我们需要理解以下几个问题:
1)虚拟内存把物理内存当作缓存使用,并通过页面置换的方式满足内存不足时也能运行程序。
2)缺页指的是从虚拟地址映射到物理地址时,页表中没有相应的映射条目。缺页异常处理程序需要重新建立页表项。缺页可能会导致从磁盘读取数据,称为 major fault; 也可能不需要从磁盘读取数据,称为 minor fault. 根据缺页处理程序的特征,缺页是一个很耗时的操作,我们的应用程序在管理内存时应该尽量减少缺页的产生。
3)TLB 缓存利用程序的局部性原理,可以极大的减少地址映射的时间。在应用开发时,我们需要关注内存的访问顺序,以尽可能减少TLB 不命中,比如在C语言中,二维数组采用行优先进行访问,假如我们有一个 的数组:
float[5120][1024] 如果页大小为 4KB, float占用4个字节, 那么每一行刚好占用一页。如果我们按照行进行访问数组,那么每行将最多只可能产生一次TLB不命中;但是如果我们按页进行访问,那么每两次相邻的访问的地址映射都是不同页表项,因此可能会产生很多的 TLB 不命中。这也是作为一名程序员需要了解操作系统原理的原因所在!
讲到这里,相信你也会明白为什么我们在内存优化系列文章(1)AI性能加速1.3x倍就靠一个环境变量!中分析缺页异常和TLB这两个指标了。但是关于虚拟内存的页面置换算法,我们并没有涉及。
下集预告
Linux 虚拟内存管理中,每个进程都有一块独立的虚拟内存空间,通常分为代码段,数据段,堆栈等等。操作系统提供了 brk 和 mmap 为应用程序分配虚拟内存?这个和我们的动态申请内存有什么关系?动态内存分配如何减少缺页异常的产生?我们将在后续的内存优化系列中逐一解读!欢迎继续关注!
推荐资料:
学堂在线《操作系统》视频课程
《深入理解计算机系统》
《现代操作系统》
