0 回顾
- 将物理内存打成一片一片固定大小的页,根据页来分配内存
- 有什么优点呢?
- 空间利用率高,没有内存碎片
- 单独的分页机制,问题就需要用到今天的多级页表和快表来解决
- 分页机制是纸上看到的内容,在纸上没有问题,在实际系统上会出现问题,需要用到今天的多级页表和快表来解决
- 所以说,多级页表和快表和前面的分页机制合在一起就形成了一个比较完善的在实际中可以比较高效工作的完整机制
1 分页
- 分页引出什么问题?
- 页表的问题,主要的原因就是页表大了
- 为了提高内存空间利用率,页应该小(比如说是4K),但是页小了页表项就会增加,就大了
每个进程只有最后一页才会有内部碎片,所以页小了,那么内部碎片就小了,因此就能提高内存利用率,比如说每一页是4MB,所以浪费的可能就是4MB,每一页是4KB,浪费的可能就是4KB,所以页要小。但也不能太小,太小了,页表项(逻辑页号)(比如说4K,4K就是 2 20 2^{20} 220次方个页表项~)就多了,那么页表占用的内存就变大了,所以页的大小要折中。自己的一点理解。
- 逻辑地址 / 页大小,得到逻辑页,通过查询页表【逻辑页到物理页的映射】,得到物理页的地址, 然后逻辑地址 % 页大小得到偏移
综上:分页的问题,为了提高内存空间利用率,页的单位大小应该尽可能小,但是页小了页表就大了,维护和查询页表的开销就大了,页面尺寸通常为4K,地址是32位的,那么就会有 2 20 2^{20} 220个页面,意思是每个进程都要维护 2 20 2^{20} 220个表项的页表吗?需要1M个页表项,每个页表项一般需要4个字节,所以页表的总大小是4M,所以每个进程是4M内存,10个就是40M
- 我的理解是一个进程有时不仅要加载代码本身,还会加载数据,那么为了利用所有内存,所以页表必须包含所有内存
- 因为每次取出来地址都要查页表找到对应的物理地址
- 能不能把4M的空间降下来,总不可能一个
Hello World
都要4M吧
1.1 降低页表的大小
- 为了降低页表的大小,第一种尝试,页表中只放使用了的逻辑页号
- 疑问:每个进程的逻辑地址空间是有限的,根本不会占到全部2^20个页表项吧?正常的代码,也不会在低地址空间放一些数据,同时在高地址空间放一些数据,中间空一大段吧?而是应该集中的低地址空间吧?那么高地址空间的表项根本没必要建立啊?
- 下面的例子中,0、1、2、3逻辑页号中,2没有用到,所以将2这个表项从页表中删掉了,删掉了这个空的,就少了1M的页表项,反正也是损耗,不使用
1.2 尝试只存放用到的页
- 明确表的目的,根据逻辑页找到物理页
- 现在不连续了,怎么完成从逻辑页到物理页的映射
- 只能去挨个比较,查找
-
这样处理后,只有用到的逻辑页才有页表项,页表中的页号就不连续了
- 本来用硬件,只需要对号入座,就可以很快查到对应的表项,现在不连续了,就不能这么做了,必须顺序查找或二分查找
- 每碰到一个地址需要翻译,就需要查找页面,即使用二分查找优化,也要最多20次的查询,每条指令可能会有多个地址需要翻译,那么每条指令的执行时间就被拉长了好多,这样肯定是不行的
- 而且如果程序本身占用的页就很多,这种优化也无济于事,页表仍然很大
- 听到这里大概明白了
- 我们无法假设用户程序编译(汇编)后的代码使用的逻辑地址空间是很守规矩的使用低地址空间的,很可能是低地址空间用一点,高地址空间用一点,是不连续的,必须考虑这种可能性
- 页表的逻辑页号如果是连续的,不管有没有用到,都有这么一个表项,那么在查页表的时候,就有类似于数组查询的随机存取的效果,可以 O ( 1 ) O(1) O(1)的查到对应的表项,用硬件加速也很方便
- 页表的逻辑页号如果只保存用到的表项,那么就是不连续的,那么在查页表的时候,就必须使用查找算法,时间复杂度不再是 O ( 1 ) O(1) O(1)的,这样做显然是不合适的,指令的执行效率大打折扣
-
既要连续又要让页表占用内存少,怎么办?
-
引出多级页表
1.3 多级页表
- 多级页表,即页目录表(章)+页表(节)
- 多级页表,使得逻辑页号连续的同时,占用的内存少
- 先用书的章节目录做类比(比如1. bootsect,P37这样的;非常像页表,节的标题(bootsect)就是页号,页框号就是对应哪一个页面(P37),以上的那种目录就是单级目录,单级目录有什么缺点?实际当中是多级目录,第一章第二章之类的,先找章再看节,就不用一节一节看过去,而且仍然是连续的)
- 引出了多级页表的概念,内存中只需要放当前用到的那一部分页表
- 现在只需要拿到第一章第二章等的章标题,再拿到第五章的节标题,也就是说,只要拿到前四个页目录项,以及第五个目录项对应的页表1个,这样在内存当中占的就少,既连续了,也保证内存小了
- 之前说高20位是页号,现在多级页表中,高10位是页目录号,次高10位是页号,低12位是页内偏移
- 多级页表的核心思想就是,用页表来管理页表自身
- 开始讲解细节
- 之前说高20位是页号,现在多级页表中,高10位是页目录号,次高10位是页号,低12位是页内偏移
- 多级页表的核心思想就是,用页表来管理页表自身
- 顶级页号10位,则顶级页目录有2^10个表项,每个表项4字节,则顶级页目录共占用4K,正好是一页
- 每一个顶级页内部,包含2^10个二级页,每个表项4字节,则每个二级页目录也占用4K,正好是一页
- 顶级页目录中,指向的二级页目录即使没有用到,这个表项也会存在于顶级页目录中,这样造成的空间浪费是小于4K的
- 顶级页目录中有效的表项指向的二级页目录会被加载到内存中,二级页目录包括的页即使没有用到,这个表项也会存在于二级页目录中,这样造成的空间浪费也是小于4K的
- 如下图的例子中,顶级页目录中,共有3个表项是有效的,分别指向3个二级页目录,顶级页目录+3个二级页目录共占用 4 ∗ 4 K = 16 K 4*4K=16K 4∗4K=16K的空间,远远小于原先需要的4M;这3个二级页目录共管理了 3 ∗ 2 22 = 12 M 3*2^{22}=12M 3∗222=12M的逻辑地址空间,当然其中也有一些空间没有用到,细究的话真实使用的逻辑地址空间肯定没有12M
- 而且可以看到,顶级页目录的前两个表项是在低地址空间,后一个表项是在高地址空间,对于这种不连续的逻辑地址的使用也是完美处理的;也就是说,程序可以使用不连续的逻辑地址空间,但是多级页表仍然是连续的,查找的时间复杂度是 O ( 1 ) O(1) O(1)的
- 总结一下这个例子,内存中的页表占了16K,表示了12M的地址空间,这12M的地址空间分布在低地址空间和高地址空间两处
- 用递归的视角分析:把一段长的页表视为“一个进程的代码段”,将其分页,并用页表记录其分布,即实现了对页表的分页管理,而二级页表也就是“页表的页表”
- 引入多级页表前,需要放入内存,2的十次方个页表,每个页表大小为4KB,共4MB。引入多级页表后只需要一个页目录表和三个页表,共16KB
- 程序只需要查找指令所在的页目录以及页目录对应的页表,所以页目录索引表占用的空间就是 2 10 ∗ 4 = 4 k 2^{10}*4 = 4k 210∗4=4k,三条指令各自还需要存储一个页表,每个4K,所以一共需要16K
- 这个16k就是图中的样子,页目录表一项4字节,一共10项,虽然就用了3项但是位置要空出来(页目录表就是4k)
- 因为页目录表中有3项所以对应3个页表,同理每个页表大小也是4k
- 多级页表高效,类似章节方式存储,仍然是连续的不用去比较,这段不懂的记住这个
- 比如你要看第五章第1节,只需要记住章号以及节号
- 核心就是把大批的连续空页打包一个空页目录项,这么一个空目录就表示了为空还能满足复杂度 O ( 1 ) O(1) O(1)执行
1.4 快表(TLB)
- 快表非常快能找到逻辑页对应的物理页
- 分段->分页->多级页表->快表
- 比如说,还是之前看书的例子,第五章第一节,我直接记住这里的页号是多少就行了,到时候直接翻到页号,假如是300页,直接翻到300页,这就是快表
- 多级页表增加了访存的次数,每一级访问一次内存;每访问一次内存,需要额外访问页表级数次的内存
- 之前讲的是在32位情况下,页面大小为4K时,需要二级页表
- 那如果是64位情况下呢?肯定需要更多级的页表,那么访存次数也就更多
- 所以多级页表的解决方案仍然会有时间复杂度的增加
- 为了解决这个问题,引出了TLB,TLB是一组相联快速存储(快表),是寄存器,可以看成是页表的高速缓存
- 举例讲解TLB的工作过程
- TLB是硬件,所以可以存储少量的不连续的表项,查询速度很快
- TLB保存最近访问的逻辑页号和物理页框号的对应关系
- 之前讲的是在32位情况下,页面大小为4K时,需要二级页表
- 命中直接TLB(表越大,命中率越高)
- 未命中直接TLB之后再多级页表
1.5 时间局部性和空间局部性
- 局部性:刚访问的经常再访问,而且它旁边的也容易很快被访问
- 局部性原理,你访问一次的内存地址,你更有可能会再次访问
- 所以老使用老访问的页表项就放入快表中
- 记住了直接翻过去就行
- 实际上是快表+多级页表,体现了折中的思想
2 总结
分段->分页->多级页表->快表