开篇
今天,计算机系统结构的真正挑战不在于内存的容量,而是内存的速度。如果你的软件实际上受到磁盘和内存的等待时间(访问时间)的限制,那么就是再好的
芯片也无济于事。在内存和cpu之间存在着一道很深的鸿沟,而且是越来越深。在过去,每隔一两年,cpu的处理速度就会提升一倍,在相同的时间内,内存的容
量倒是扩大了一倍,但它的访问时间提升却没有那么明显。。
所以我的理解是:内存主要受限于容量和速度。
容量问题除了集成工艺的发展扩大物理内存容量,还可用用虚拟内存的办法解决。访问速度则可通过Cache技术的发展和程序设计的优化。
内存管理的前世今生
最初的计算机使用的内存直接对物理内存进行访问。这样的方式很快就被淘汰,原因有二:1)直接访问内存不利于程序间、程序和操作系统之间的保护。比如说某个进程可能读或者写操作系统的数据,这样做是很危险的。2)这对程序大小有很大的限制。要运行的程序都要首先加载进入内存中,如果程序过大,超过内存的大小,这样的程序要运行是不可能的。
下面先介绍一下之前的内存管理模式,然后在介绍现在使用的虚拟内存技术。
Intel 80x86内存模型及工作原理:
(也就是之前计算机的工作原理)
80x86内存模型的基本形式是段。它是一块64kb的内存区域,有一个段寄存器所指向。内存地址的形成过程是:取得段寄存器的值,向左移动4位(相当于乘上16)
然后加上偏移地址的值。注意,不同的段地址加上偏移地址可能指向同一个内存地址。这里的访问地址就是物理地址,也就是说在没有虚拟内存之前的计算机对内存的访问是直接访问物理地址的。
MS-DOS 640k的限制由来:
在MS-Dos下运行的应用程序都有一个严格的内存限制,就是可用内存只有640KB.这个限制源于Intel 8086这个最初的DOS机器的最大地址返回。8086支持20位的地址
,总共是1M的内存。之所以可用内存只有640KB是因为某些段必须保留供系统使用。
如:
F0000到FFFF 64KB,永久性的ROM区域BIOS、诊断信息等
D0000到EFFF 128KB,用于ROM存储区域。。。
后面的就不一一列举。
下面介绍虚拟内存技术~
虚拟内存
虚拟,也就是不存在的意思,也就是说这里的内存实际上是不存在的。这种技术给每个进程提供了一个完整的地址空间,即好像每个进程都拥有整个内存(如果是32位的话每个进程就有4GB的地址空间,也就是说每个进程可使用的内存大小是4GB)那么这是如何实现的呢?
其实物理内存还是只有一个,每个进程虽然有4GB的空间,但是用进程运行的时候那4GB的空间中只有用得到的那部分映射到了物理地址上,而用不到的则存放在磁盘中,等要用的时候再把它装入物理内存。其实这就是虚拟内存的大体思想。
和MS-DOS一样,让程序受限于机器的物理内存数量是相当不方便的。很早的时候,就有了虚拟内存的概念。它的基本思路是用廉价但缓慢的磁盘来扩充内存。
在任一时刻,程序实际需要使用的虚拟内存区段的内容就被载入到实际的物理内存中。当物理内存中的数据有一段时间未被使用,就可能被转移到硬盘中,节省下
来的空间用于载入其他数据。现在的很多计算机系统都使用了虚拟内存的技术。
虚拟内存应用例子:
SunOS中每个程序都运行于32位的地址空间中,每个程序都以为自己有对整个地址空间的访问权,这正是通过虚拟内存实现的。所有进程共享机器的物理内存,当内存用完
时就用磁盘保存数据。在运行时,数据在磁盘和内存之间来回移动,内存管理器把虚拟地址翻译为物理内存地址,并让一个进程始终运行于真正的物理内存中。这些都是由操作
系统完成,应用程序员只看到虚地址 ,而不知道具体的切换情况。
虚拟内存通过“页”的形式组织。也就是操作系统在磁盘和内存之间来回移动进行保护的单位,一般为级K字节。
交换区:
从潜在的可能性上说,以进程有关的所有内存都将被系统调用,但是如果该进程不会马上运行(可能由于优先级等原因或者是处于睡眠状态),操作系统可能暂时取回给它 分配的内存,把该进程相关的信息备份到磁盘上,这样这个进程就被换出。在磁盘中有一个特殊的交换区用于保存从内存中换出的进程。交换区的大小一般是内存的几倍。
进程只能操作位于物理内存中的页面。当一个进程引用一个不存在物理内存中的页面时,MMU就会产生一个错误。内核对此作出反应,并判断是否有效,无效,内核向进程发出一个“Segmentation violation(段违规)”的信号。如果有效,内核从磁盘中取回该页,换入到内存中。一旦页面进入内存,进程便被解锁,可以重新运行。进程并不知道它曾因为页面换入时间等待了一会。
Cache存储器:
首先,Cache存储器是对多层存储概念的扩展,可以认为Cache是为解决内存和cpu数据交换速度过慢而产出的。
它的特点是容量小,价格高,速度快,它位于内存和cpu之间,有两种组织方式,有的是位于cpu一侧,有的则是位于内存一侧。
catch的操作速度和系统周期时间相同,所以一个50MHz的处理器,他的cache的存取周期为20ns,典型情况下,内存的速度仅为cache 的四分之一!速度确实很客观哈~
简答的介绍一下cache的工作原理:cache有一个地址列表及他们的内容,随着处理器不断引用新的内存地址,地址列表的内容也一直在变化中。当cpu要从内存中取出数据时,先看地址是否存在于cahe地址列表中,如果存在直接读取,不存在则从内存中读取。内存中读取数据以行为单位,在读取的同时也装入cache中。
有关高速缓存可参见博文:
内存泄漏
”内存泄漏“其实我是不想用这种比较专业的说法来说问题的,好像会给人感觉比较远。。。但是仔细想下这个翻译其实还是有道理的。“泄漏”,泄漏的结果是怎样?泄漏了会变少,内存泄漏就是可用的内存少了。为什么会变少呢?。如果你申请的内存(比如说地址A)在结束时没有释放,内存管理程序就会认为地址A的数据是有用的。但实际上使用地址A的进程已经结束,但他申请的内存还没释放。如果没有其他程序显示的释放的话,这块地址就会一直被认为是有用的(其他进程用不了)。这就是内存泄漏了。
有些程序并不需要管理他们动态内存的使用,当需要内存时,他们简单的通过分配内存获得,而不必担心如何回收它。当然,如果有回收机制的语言也不必担心如何释放分配的内存。
对于需要管理动态内存分配和回收的情况,比如c语言通常不适用垃圾回器(自动回收不用的内存块)那么这些程序在释放和分配内存时,就需要认真对待。
堆经常会出现两种类型的问题:
释放或改写仍在使用的内存(称为内存损坏)
未释放不在使用的内存(称为内存泄漏)
这是最难被调试发现的问题之一。如果每次已分配的内存块不再使用了却不是放,进程就会分配越来越多的内存,导致可用内存越来越少。
避免内存泄漏
每次分配内存时,注意在不用的时候释放内存。如果释放和先前分配的不对应,那很可能就会造成内存泄漏!
如果是c语言,一个更简单的办法就是用alloca函数分配内存,当离开调用alloca函数时,他分配的内存就会被自动释放。显然,这不适用于那些创建比函数身命周期长的变量的函数。有些热不赞成使用alloca,因为它不并是一种可移植的方法。如果处理器在硬件上不支持堆栈,alloca()就很难高效的实现。
如何检测内存泄漏:
1、如果是unix或者linux,首先用swap命令观察还有多少可用的交换空间。在短时间内连续键入该命令两三下次,看可用的交换区是否在减少。还可以使用其它的一些如netstat、vmstat的工具。如果发现不断有内存分配且从不释放,就可能出现内存泄漏。
2、确定可以进程:用ps-lu命令来显示所有进程的大小。重复这个命令,如果一个进程看上去不断增长而从不缩小,那么基本可以确定发生泄漏的进程了。
参考资料:C专家编程