计算机内存那些事

内存的访问

  1. 通过地址总线将地址送到内存,然后数据总线会把数据送到微处理器供给cpu使用。
  2. 32位总线可寻找范围为[0,2^32 - 1]相当于[0,4294967296],即4GB。

物理地址到虚拟地址

早期计算机是使用物理地址的,但是使用物理地址会存在以下几个问题:

  1. 地址空间不隔离

    所有的程序都是使用物理地址访问,程序所使用的内存空间是不隔离的。程序可以被其他程序访问,对恶意访问或修改没有防护措施。程序的稳定性难以保证。

  2. 内存使用效率低

    直接使用物理地址,必须是连续的。如果剩余的物理内存无法满足程序的空间分配,那么就需要把当前运行的某些程序写入磁盘,释放内存空间以供给需要运行的程序。当写入磁盘的程序被唤醒,系统会以同样的策略给该程序分配内存,内存条件满足后把程序读入内存。这里面存在很多的磁盘读写操作,导致内存使用效率低。原因1,使用物理地址,必须是连续的,不能充分的利用内存碎片,颗粒很大。原因2,磁盘读写的操作比较耗时,内存分配必须等待该操作的完成,这样相当于把内存使用的效率变低了。

  3. 程序运行地址不确定

    系统给程序分配的空间不固定与程序编写时固定地址的数据访问和指令跳转产生冲突。这个冲突增加了编程难度。

虚拟地址

为了解决使用物理地址存的问题,加入中间层,通过使用虚拟地址的方式访问程序。然后通过地址映射转换成成物理地址。由于每个程序只能访问自己的虚拟地址,只要妥善的控制这个映射关系,确保程序间的地址不重叠,就能实现程序间的内存地址相互隔离。

三、内存的存储管理方式

  1. 分段存储管理方式

    • 地址的整块映射。物理地址分配也是连续的。

    • 解决了地址空间不隔离、程序运行地址不确定的问题。但由于是连续地址,分配空间不足的情况下,需要对某些程序进行磁盘写入以释放空间,导致内存使用效率低的问题。

  2. 分页存储管理方式

    • 解决了内存使用效率低的问题。对于每个程序来说,在某个时间段内,只会频繁的用到某部分数据。那么可以把那些不常用的数据存储于磁盘中并释放内存空间。通过分页的存储方式实现更细粒度的控制。内存的分页需要硬件支持,同时在硬件支持的基础上通过系统选择使用的方式。例如硬件支持4k和8k每页,系统选择4k。但是在运行过程中是不能切换分页方式的,也即是说,任意时刻分页方式都是确定的。目前几乎所有pc系统都使用4k大小的页。

    • 内存共享:通过两个程序中的某些地址指向同一个物理页的方式实现。

    • 写入磁盘的数据唤醒:当程序通过虚拟地址对写入磁盘的数据发起调用的时候,内存中并不存在并会发出页错误(pageFault),然后系统接管线程并把磁盘中的页数据读入内存,并恢复正常访问。

    • 页的部分映射表会存储在一个叫TLB(Translation Look Aside Buffer)的寄存器,以提高内存的使用效率。因为把所有的映射表存储在内存中,cpu读取的时候需要两次访问内存。第一次访问页表,第二次才访问目标内存。这样会使访问效率变低。如果通过TLB保存常用的映射表,读取寄存器获取物理地址,能大大提高内存使用效率。TLB的容量有效,如果表满了会淘汰最老的不使用的页映射条目,这里应该有某类的淘汰算法来实现。

    • 一般来说,只有当内存不足的时候才会把数据读入磁盘,也即是虚拟内存。

  3. 段页存储管理方式

    • 分页和分段两种方式的结合,同时具备两者的优点。是应用最广泛的存储管理方式。

    • 物理地址与虚拟地址直接的转换时通过硬件实现的。但几乎都是通过MMU(Memory Management Unit)这样的硬件实现的。MMU一般集成于cpu内部。

程序的内存布局

  1. 内核空间。

    • 内存的部分空间分配给内核使用,这部分空间程序是无法访问的。例如,内存总量为4G,分配1~2G作内核使用。
    • 栈的作用:用于维护函数调用的上下文,离开栈函数的调用就无法执行。
    • 栈的地址增长方向:跟cpu和os的实现方式有关,Linux是向下增长的,但在程序中显示的都是虚拟地址,不能用于判断其增长方向。

    内存分布图:


    linux程序内存分布.png

    栈保存了函数调用所需要维护的信息:

    1. 函数返回地址和参数
    2. 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
    3. 保存上下文:包括函数调用前后要保持不变的寄存器。
    4. i386中使用ebp和esp两个寄存器来为函数活动记录划分范围。esp始终指向栈的顶部,也即当前函数活动记录的顶部。而ebp则会指向函数的栈帧底部。
      当一个新的函数调用时压栈过程大概如下:
      • 它的参数会先压栈(push params)
      • 然后对其函数执行函数调用(call func),并把返回值地址填入栈。
      • 保存之前的函数ebp,push ebp。
      • 设置当前函数的栈帧底部mov esp, ebp。当前位置的栈顶esp就是当前函数的栈帧底部ebp。
        -然后执行新的栈帧,当栈帧执行完之后会用ret 返回参数。此时通过读取ebp的值,ebp就能返回到之前函数的栈帧底部,同时esp划过函数参数恢复到之前函数的栈顶。
    5. 另外还有一个eip寄存器,记录cpu下一次执行指令的地址。
stackframe-cdecl.gif
    1. 堆的作用:用来容纳应用程序动态分配内存。
    2. 堆内存的分配可以通过程序控制(malloc()),当内存不足的时候会交由系统分配(brk()、mmap())。
    3. 堆分配算法:
      • 空闲链表

        每个节点为包含起始地址和空闲大小。查找空闲空间的时候,通过遍历空闲链表查找合适空间。返回地址和大小,用于内存分配和释放。
        存在缺点:链表被破坏,或者返回数据被破坏,那么会导致程序错误、甚至整个堆无法工作。因为空闲链表只记录空闲的数据,而不是全堆信息。如果大小字段错误,确实会导致整个堆无法工作,因为这种情况是无法判断出来的,无法估量整个堆的内存使用情况。

      • 位图

        把空闲内存划分为多个块,使用2个字节存储。假设每个块大小为x。堆空间大小为Y,那么数组的元素个数为y/x。每个块的地址也能通过公式计算出。那么我们返回数据的时候,会把第一个元素标记为头块(返回头快和后面跟随的块个数),后面的元素标记为主体块。那么块是有3种状态,空闲/头/主体,用用2bit表示。

        优点:

        1. 只要分配块的大小适中,那么cache命中率会很高,速度快。
        2. 稳定性好,避免用户越界读写破败位图,作简单备份。即使部分数据受到破坏,也不会导致整个堆无法工作。因为位图是记录了整个堆的内存使用情况,就算部分数据受到破坏,使用保守策略不使用破坏的部分,而不会导致整个堆不可用。

        缺点:

        1. 容易产生碎片。
        2. 块的大小太小,会导致cache命中率变低。可使用多级位图解决。
      • 对象池

        某些场合被分配的对象大小为固定的几个值。那么就按照这个大小分配块。可用位图或空闲链表实现。

    而实际中,堆分配算法往往是采用多种算法复合而成的。


ps:部分图片来源于网络,如有侵权,请联系马上删除。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容