目录

内存管理

内存管理需要解决的问题

  • 进程在内存中怎么放(或说进程在内存中的布局)
  • 限制进程对另一个进程的内存读写(或说对内存的保护)
  • 内存空间利用率最大化

基本解决思路(不可行)

以下序号分别对应上述问题:

  1. 可以一块一块的放,每个进程各占内存连续的一块
  2. 使用两个寄存器base和limit,base为进程的基址,limit为空间大小
  3. 动态加载(dynamic loading):动态链接库(dynamic link library)
    • 程序从开始到结束并不一定将所有代码都运行一遍,故可以不需要将所有的代码都加载到内存。所以就有了dll文件,dll也是实现一个功能的程序代码,只有当你使用这个功能的时候,这部分代码才会加载到内存中,提高了内存的利用率;还有一个好处就是多个相同的功能可以共用这一个文件,当其加载到内存中时也只有一份,如果其他程序用它,就直接用就行,不必重新加载
  4. 还有一种内存管理技术是swap(交换技术)
    • 不太常用的进程暂时放到外存中,使用时再读到内存中

还有个问题就是指令和数据在内存中的地址绑定问题。 因为每个进程每次运行在内存中的地址是不一定相同的。 绑定有两种方式:

  1. 在编译时绑定 比如boot,编译时就确定开始运行的地址是0X0700
  2. 在加载时绑定 在将进程加载到内存时,又加载程序将进程中的地址与真实物理地址进行绑定 (不是一个很好的策略)
  3. 执行时处理 进程在内存中可以从一个段移动到另一个段(比较不错的策略)
  4. 硬件支持

编译时绑定和加载时绑定的时候物理地址和虚拟地址时一样的

执行时绑定的物理地址和虚拟地址是不同的

MMU(内存管理单元)

作用:将虚拟地址映射成物理地址

一种简单策略是将虚拟地址加上base基址即使物理地址 采用这种管理方式的进程,都有一个relocation register(重定位寄存器),存放着程序的真实的起始地址,而逻辑地址是从0开始的,所以真实的物理地址是:relocation register + 逻辑地址、

提高内存空间利用率

一个程序往往有很多功能,而在运行的过程中,有很多功能往往用不到,如果此时将整个程序都加载到内存中,那么就造成浪费了。而如果将程序分成几个部分,用到的时候再加载,那么可以有效的提高内存利用率,于是dynamic link library(动态链接库)出现了。

进程在内存中的存放方式

contiguous allocation(连续分配) (现在已基本不用了) 进程之间相互独立,单个进程的地址是连续的。 /images/MemoryManage/2018-01-20_152111.jpg

几种填补内存空洞的策略: /images/MemoryManage/2018-01-20_152137.jpg

首次适应:选取第一个可以容纳的空洞

下次适应:轮流选取可以容纳进程的空洞

最佳适应:每次选取最适合(可以容纳并且最小)的空洞

最差适应:每次选取最大的空洞

用的最多的是首次适应

外部碎片与内部碎片

外部碎片:内存太小以至于不足以分配给其他进程,从而造成浪费,外部是指内存在进程空间之外

内部碎片:进程申请的内存大于自身需要的,从而有多余的一部分不能够利用,造成浪费,内部是指内存在进程空间之内、

页式内存管理

一个进程所拥有的内存在物理上是不连续的 物理内存的一页是2的整数次幂,在512byte和64M之间 逻辑页面与物理页面保持相同大小

页表:由逻辑页面映射物理页面 /images/MemoryManage/2018-01-22_140846.jpg

页面越小,内部碎片越少,而页表越大;页面大,内部碎片多,页表小

通常选择4K或8K大小的页面(当然也可以使用4M和8M大小的页面,页面的大小非常依赖于CPU的架构)

页表的内容与基本原理

  • 页表是存储在内存中的
  • 在内存中找到页表需要两个寄存器:页表基址寄存器、页表长度寄存器
  • 每次对内存中数据和指令的访问,都需要访问两次内存:一次是查找页表得到数据或指令的地址,一次是对数据或指令进行读写
  • 所以为了加快访问内存的速度,引入了TLB(translation look-aside table),相当于硬件的一个cache,可以快速的查找访问页表
  • TLB主要存储了两个部分:虚页号(page)和物理页号(frame)
  • 根据局部性原理(在一小段时间里,计算机会较多的访问当前内存区域),将当前访问比较多的页号存到TLB中
  • 要在页表中快速找到想找的页号,仅仅依靠软件(线性查找或排序号折半查找)是不够的,所以在硬件上引入了并行搜索 /images/MemoryManage/2018-01-25_171749.jpg

页表的实现与管理

  • 若页表采用固定大小分配实现 对于32位的计算机,一个进程的逻辑空间有2^32 byte大小,所以系统需要为一个进程维护一个大小为2^32/2^12=2^20=1M个页项大小的内存(暂时以32位系统和内存页为4K大小的计算机讨论) 而一个页表项的大小一般为计算机字长大小即4个字节 所以一个进程需要一个大小为4M的内存空间,而这个开销是巨大的

由此可知,页表不可以固定分配大小

  • 若页表采用动态增长维护

动态扩展首先想到的是链表实现,但如果用链表的话,在查找页号的时候只能遍历,这种查找效率是极低的。

通常使用两级页表结构,将页表分成外层页表和内层页表 /images/MemoryManage/2018-01-27_091321.jpg

外层页表只保存内层页表的索引,并不包含真正的物理页号,计算机首先根据逻辑地址运算得到外层页表的位置,然后再得出内层页表的位置,然后由内层页表得到真正的物理地址 /images/MemoryManage/2018-01-26_180354.jpg

前十位是外层页面号,中间十位是内层页面号,后十二位是页内偏移 /images/MemoryManage/2018-01-26_180407.jpg

页表的查找方式相当于散列查找(根据逻辑地址运算出页号)

这种页表管理方式所占的内存为:外层页表 2^10=1K 1K*4=4K,内层页表也为4K大小,总共大小为8K

所以4K的页表总共可以管理2^10*2^12 byte = 4M大小的内存,如果还需要4M,那么再新建出一个内层页表 页表总共需要12K

64位系统页表的实现

  • 64位系统如果也采用两级页表的管理方法的话是行不通的 外层 内层 页内偏移 : 42 10 12 故外层页表的大小为2^42*4=16T

  • 采用三级页表 一层 二层 三层 页内偏移 : 32 10 10 12

  • 采用散列表的方式 /images/MemoryManage/2018-01-26_181140.jpg

由逻辑地址的页面号通过哈希函数运算,再页表中找到对应的项,有冲突则通过链表连接在一起 页表也比较大

Linux内核4级页表的演进 操作系统页表管理

段式内存管理

/images/MemoryManage/2018-01-27_102922.jpg

现在一般使用段页式内存管理

操作系统内存管理–简单、页式、段式、段页式 存储管理之页式、段式、段页式存储