操作系统内存管理
内存管理
-
虚拟地址寻址
运行多个程序可能出现问题:进程1给某个物理地址比如0x70写入数据,现在切换到进程2,也给这个相同的物理地址写入数据,就发生覆盖了,这样的后果很严重,可能造成系统崩溃等。于是「虚拟地址」就出现了,大家都有自己的「虚拟地址」,可以利用它访问到自己的数据而且不会冲突,这是因为操作系统会提供一种机制,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。 操作系统是如何管理虚拟地址与物理地址之间的关系?主要有两种方式,分别是内存分段和内存分页。
-
内存分段
程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。虚拟地址由两部分组成,段选择子和段内偏移量,就可以找到真实物理地址。段选择子就保存在段寄存器里面,段选择子里面最重要的是段号,用作段表的索引,段号存在段表中,包括段基地址+段界限+特权等级(用于权限判断,比如内核级和用户级)。 内存分段后,段界限代表内存中这一段的总长度,段基地址代表着是哪一段起始地址,再加上段内偏移量,就可以找到这个虚拟地址的位置。
但分段也有缺点:
-
第一个就是内存碎片的问题。
-
外部内存碎片。也就是产生了多个不连续的小物理内存,导致剩下的连续空闲区可能对于新申请的大小来说太小了。 解决办法就是就是内存交换。 比如Linux 的Swap 空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换。交换过程就是把内存中的内容拷到硬盘,再拷回来的时候邻接着其他程序的内存区域,避免有空隙造成碎片。
以及程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能并不是很常使用,这也会导致内存的浪费;
-
-
第二个就是内存交换的效率低的问题。
如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。
为了解决这些问题,于是就出现了内存分页:
-
-
内存分页。为什么要分页?
总体而言可以认为分段内存的管理粒度太粗了,所以 intel 80386 就出来了个分页管理,一个更加精细化的内存管理方式。简单地说就是把内存等分成一页一页,每页 4KB 大小,按页为单位来管理内存。你看按一页一页来管理这样就不用把一段程序都加载进内存,只需要将用到的页加载进内存。这样内存的利用率就更高了,能同时运行的程序就更多了。并且由于一页就 4KB, 所以内存交换的性能问题得以缓解,毕竟只要换一定的页,而不需要整个段都换到磁盘中。
在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理块号,这个块号与页内偏移的组合就形成了物理内存地址
为什么分页有效?
-
时间局部性:一条指令的一次执行和下次执行,一个数据的一次访问和下次访问都集中在一个较短时间内;
-
空间局部性:当前指令和邻近的几条指令,当前访问的数据和邻近的几个数据都集中在一个较小区域内。
分页缺点?
- 内部内存碎片,内部碎片就是已经被分配出去却不能被利用的内存空间。比如一页4K,但这一页装不满(用了3K),于是形成了内部碎片。
计算题:十进制逻辑(虚拟)地址9612,一页8K,页表中,对应页号为1,物理块号为3,则实际物理地址: 每页8K(十六进制)=8192(十进制),9612(十进制)=8192(十进制)+1420(十进制)。 从地址映射表可以看到页1对应物理块3,因此地址9612(十进制)的物理存储位置=8192(十进制)× 3 + 1420(十进制)= 25996。
-
-
多级页表
在32位系统,一个进程可能访问整个4G内存,则至少要有 4MB 的内存来(4G/4K≈2^20=4MB)存储页表。看起来不大,但要是有100个进程,那就要400MB的页表,而实际上操作系统有上千个进程都很正常,那这MMU岂不是要比内存还大了?更别说64位系统了。所以提出了多级页表。
-
二级分页
对这4MB数量再分,分成1024(1级页表)*1024(2级页表),1级页表里面存1级页号+2级页表的地址,2级页表存2级页号+物理页号,就相当于1级页表去引用对应的2级页表,组合起来寻址, 这样好处是充分利用了局部性原理,大部分的1级页表其实都不会用到,假如只有%20用到了,只需要把20%对应的2级页表加进来就足够了,4KB(一级页表) + 20% * 4MB(二级页表)=
0.804MB
,这就省下了大量的空间。 -
多级分页
对于64位系统,两级分页肯定不够了,就变成了四级目录:
全局页目录项 PGD(Page Global Directory);上层页目录项 PUD(Page Upper Directory);中间页目录项 PMD(Page Middle Directory);页表项 PTE(Page Table Entry);
-
-
TLB(快表)
TLB(Translation Lookaside Buffer)通常称为页表缓存、转址旁路缓存、快表等。利用程序的局部性,把最常访问的几个页表项存储到访问速度更快的硬件,即TLB,专门做地址转换的工作,放在CPU中。CPU进行虚拟地址寻址时查TLB,若未命中出现缺页中断,才继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
-
段页式内存管理
将分页和分段机制结合起来。先将程序划分为多个有逻辑意义的段,接着再把每个段划分为多个页。每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。
访问时:第一次访问段表,得到页表起始地址;第二次访问页表,得到物理页号;第三次将物理页号与页内位移组合,得到物理地址。
-
Linux 内存管理
Linux 内存主要采用的是页式内存管理。早期 Intel X86 CPU 一律对程序中使用的地址先进行段式映射,然后才能进行页式映射,因此Linux将每个段起始地址设为0,所有程序都一样,这样做相当于逻辑上屏蔽了段式内存的概念。
-
缺页中断
缺页中断就是要访问的页不在主存中,需要操作系统通过将页调入主存后再进行访问,此时会暂时停止指令的执行,产生一个页不存在的异常,对应的异常处理程序就会从选择一页调入到内存,调入内存后之前的异常指令就可以继续执行。
页面置换算法?
当需要调入新的页面到内存中而内存已满时,需要用算法淘汰一些旧页面。常用的算法有:LFU、LRU、FIFO、最优页面置换算法(预测哪页下次访问的到来最久,不现实但可以作为评价依据)、第二次机会页面置换算法(是FIFO改进版,为了避免移走常用的,多给一次机会,设置一个修改位R,R为0代表没有2次用过,直接淘汰;R为1则说明2次访问过,R置为0页面放到链尾)、时钟页面置换算法(二次机会的改进版,变成环形链表,像时钟一样,R为0直接将新表插到此处;否则R为1,那么R置为0,移动指针到下一位,一直移动指针找到R为0)。