文章目录
一、内存如何用起来
1.1 为进程绑定地址
内存使用的方法就是先把程序加载到内存中,程序的每条指令和数据在内存中都有相应的位置,位置可以在如下三种时机进行绑定:
编译时:在编译期间就知道进程将在内存中的驻留位置,那么就可以生成绝对代码。如果将来开始地址发生变化,那么必须重新编译。这种方法的优点是性能高,缺点是不够灵活,常用于嵌入式系统中。
载入时:在编译期间生成可重定位代码,在加载时绑定位置,如果将来开始位置发生变化,重新载入即可。优先是相较于编译时绑定更为灵活,缺点就是速度会变慢,灵活性还是不够,因为在内存中经常有进程的换入换出,那么载入时生成的位置就不能用了。
运行时:程序在运行期间确定位置,这种方法灵活性最高。
1.2 逻辑地址和物理地址
在程序编译期间会为该进程生成可重定位地址,然后在进程进入内存的时候会为其分配内存空间,该进程所占内存的最小地址称为基址,基址 + 可重定位地址 = 物理地址,这一步骤是由内存管理单元(MMU)完成的。
逻辑地址:CPU 所生成的地址,逻辑地址是内部和编程使用的、并不唯一。在实际的编程中就是上述提到的可重定位地址,每个进程都有自己的逻辑地址。
物理地址:内存单元看到的地址。
二、分段
2.1 分段的引入
在一个程序中,可能会有代码、变量、数组、堆栈等,我们在内存中不能把它们放在一起,原因有以下两点: ① 这些元素的属性是不一样,比如说代码一般是只读的,而变量是可写的,所以不能把放在一起,否则会导致内存溢出的情况下将只读变量修改
② 而且堆栈是动态增长的,而代码是不变的,如果放到一起,会因为堆栈的增长,为整个进程重新分配内存空间,这是没有必要的。
所以在内存中要将一个程序中的不同内容分开存储,这些分类就是分段,分段是在用户视角的内存管理方案。
2.2 如何使用分段
为了支持分段,操作系统引入了段表的概念,段表中记录了段号,段的基址、段的长度及保护,每个进程都有其对应的段表,当访问一个地址的数据时是通过段号和段内偏移来访问的,<segment-number,offset>。
三、内存分区与内存分配算法
3.1 内存分区
知道了进程是分段的之后,就要考虑内存是怎么分割的了。
固定分区:操作系统在初始化的时候,将内存等分成 k 个分区,可能导致的问题:大的进程可能放不进去,小的放进去可能会导致内部碎片。
可变分区:在可变分区方案里,操作系统有一个表,用于记录哪些内存可用和哪些内存已占用。当有进程需要内存时,操作系统根据内存分配算法为进程分配内存。
3.2 内存分配算法
首次适应:从头开始查找可用的内存表,找到第一个足够大的内存空间为其分配,速度快,但会产生太多的外部碎片。
最佳适应:分配最小的足够大的内存。必须查找整个表,或者将列表按大小排序,速度慢,会产生外部碎片。
最差适应:分配最大的足够大的内存。必须查找整个表,或者将列表按大小排序,速度慢,会将大的内存空间变小,浪费空间。
四、分页
4.1 分页的引入
上面提到了外部碎片,外部碎片就是可用内存之间的间隙,这些间隙一般都很小,没有内存可以使用,如果间隙太多,则会浪费大量的内存空间,为了解决这一问题,可以使用紧缩的方式来解决,但是这种方式非常非常浪费性能, 会给用户造成死机的错觉,所以引入了下一个解决方案——分页。
分页借鉴了固定分区的机制,将内存分成等大的页,然后将一个段离散的分布在不同的页中。
实现分页的基本方法是将物理内存分为固定大小的块,称为**帧或页框 (frame) ;而将逻辑内存也分为相同大小的块,称为页 (page) **。
通过分页访问内存的方式是通过页表。页表记录了页号和对应的页框,页号是按照从小到大排列的,通过 CPU 的逻辑地址与页表作映射就可以得到对应的物理地址。
4.2 页表映射的例子:
比如有如下页表,页面的尺寸为 4 K:
页号 | 页框号 |
---|---|
0 | 5 |
1 | 1 |
2 | 3 |
3 | 6 |
逻辑地址为 0x2240(十六进制),那么该逻辑地址的映射方法如下:
用逻辑地址除以页面尺寸,及 0x2240 除以 4k (0x3),即 0x2240 右移 3 位,得到 0x2,所以页号就是 2,页偏移位 0x240,所以最后的映射结果就是 0x3240。
通过上述映射过程,可以将逻辑地址进行一个总结:如果逻辑地址空间为 2^m ,且页大小为 2^n,那么高 m - n 位就表示页号,低 n 位表示页偏移
五、多级页表
5.1 多级页表的引入
页表解决了外部碎片的问题,但是也造成了空间浪费,它需要为整个内存空间所有的帧都建立页表,如果每个页面大小位 4K,地址空间为 32 位 (即 4 G),那么 2^20 个 这样的页面,即 2^20 个页表项,每项需要 4 B来存储,那么一个页表所占的内存就是 4MB,那么十个进程就占用 40MB,这是绝对不能容忍的,而由于程序的空间局部性,每个进程可能只会访问一部分内存空间,所以很容易想到,在页表中只存储用到的页,如下图:
该方法解决了空间上的问题,但是同时又带来了时间上的消耗,因为此时的页表不再是连续的了,所以我们如果搜索某一个页的时候最快的方法就是使用二分查找,时间复杂度位 O(log n),相比于之前的 O(1),此时的时间复杂度又太高,所以我们需要引入新的机制,即能保证空间负责度低,又能保证时间复杂度低——多级页表。
这里以二级页表作示例:
使用二级页表,第一次页表存储用到的页所在的页表,第二级存储实际的页。最经典的比喻就是单级页表相当于是没有大标题的目录,如果我们想要看某个小章节,就必须把所有章节都存起来;而二级页表就相当于是有大标题的目录,如果我们想看某个小的章节,只需要把该部分对应的大的章节存起来即可。
5.2 二级页表节省空间的示例
如下图,在 32 位地址空间中,每个页面大小为 4KB,页表中每个条目大小为 4B,计算下述页表所占用的空间。
计算:每个页表中存储 2^10 个页表项,每项大小为 4B,所以每个页表大小为 4KB,这里用到 4 个页表,所以占用内存为 16K。
六、快表
6.1 快表的引入
为了加快逻辑地址到物理地址的映射,计算机提供了硬件支持——TLB,这是一个类似于高速缓冲器的硬件,访问 TLB 要比访问内存快的多。
6.2 如何使用快表
-
根据页号访问快表(存储在快表中的页号不需要连续,使用硬件支持可以一次命中);
-
如果页在快表中,直接从快表中读取相应的物理地址;
-
如果也不再快表中,就访问内存中的页表,再从页表中读取物理地址,同时将页表中的项添加到快表中;
6.3 快表很小,为什么命中率依然高
因为我们所写的程序大多是顺序结构和循环结构,这就导致程序的地址访问上存在空间局部性,我们可能经常访问某几个区域的内存,而不访问其他区域的内存,这就是快表的命中率提高了。