(四)动态内存分配
内存的使用问题和并发问题似乎占据了程序员一半以上的困扰!所以在现代一些语言中,尽量避开程序员自己直接控制内存的使用(主要是动态内存分配的使用).在若干年前的dos时代储存器基本上是公开访问的,现在操作系统给程序员加入各种屏障! 但在嵌入式领域,为了追求性能,操作系统去掉了虚拟存储器的概念,变量指向了实际的内存地址!
虚拟存储器是一个抽象的概念,当你的程序引用一个”内存”变量,你不知道他的实际存贮在什么地方,是物理内存,是磁盘虚拟内存,还是高速缓存中等等!
下面是一个存储器的层次金字塔图形
对于计算机上的存储是一个层次结构,如图越向上,容量越小,单位容量价格越高,但存储速度越快.对于操作系统而言,速度慢的存储器扮演着速度快存储器的缓存(一般来说最快的是CPU),所以一级catch可以说是寄存器的缓存,二级catch是一级catch的缓存,主内存是二级catch的缓存,本地磁盘是主内存的缓存…
如果对程序性能要求很高,就用充分理解这种缓存机制!这方面的文章也很多.
下面说说动态内存分配的问题!
从前面讨论可以知道,在c语言中由#define定义的常量是在预编译做了宏替换,而在程序中的这些字面量有的被编译到代码中(ELF文件的.text段),有的在只读数据段(比如字符串,另外双精度double占用8个字节,运算时需要FPU 的ST寄存器或者叫浅栈,不容易编译到代码中),剩下的有初值的外部变量(包括static限定的变量)在数据段(.data),为赋初值的外部变量在.bss段中(ELF文件没有给分配空间,加载到内存映像时分配),还有一种是局部变量(自动变量)在用户栈中(参看前面的内存映像图).前面我们讲过变量生存期的问题,局部变量决定于调用栈,生存周期在函数调用过程中,外部变量(包括static限定的变量)在整个进程运行期中,还有一种情况就是通过malloc函数调用动态生存期的变量,他们的生存期是程序员自己控制的(其实,还要取决于malloc等函数的实现,不过先这么认为吧)!理解动态内存分配先要有下面的前提概念:
首先,内存分配(包括内核加载,用户进程加载,动态库加载等等)都是建立在操作系统的虚拟内存分配之上的!关于虚拟内存分配可以参考任何OS的书籍(Unix,Win32都可以,大同小异).其基本含义是: (1)进程使用的内存地址是虚拟的(每个进程感觉自己拥有所有的内存资源),需要进过各种地址翻译最终指向实际的物理地址(2)主内存和磁盘采用页交换的方式加载进程和相关数据,也就是说数据何时加载到主内存,何时缓存到磁盘是OS调度的,对应用程序是透明的!(3)虚拟存储器给用户程序提供了一个基于字节的内存序列(可以认为是一个大的字节数组),在32位系统中,用户可以得到假象的4G(内核要使用1G或2G等内存地址)字节的内存序列.
其次,不同的OS提供的内存访问方式是有区别的.进程映像也是有区别的!(虽然虚拟存储器的概念是一致的)!
最后,不是所有计算机系统都有虚拟存储器的概念,比如嵌入式领域大多数是没有虚拟存储器的,程序使用的都是实际物理地址!
既然我们可以得到很大的内存字节序列,而我们程序使用的数据类型是不同种类的(比如记录类型),还有,OS提供的获得内存的API(系统调用)是有一定的约束的,比如有最小内存块,还有内存对齐等等,另外申请内存和释放内存都涉及到内核调用,频繁的使用会影响性能!直接使用OS提供的API会有什么问题呢?首先如果我们需要比最小内存块还小的数据怎么办?
频繁的内核调用牺牲性能怎么办? 基于这样的各种问题,产生了一个新的概念 内存管理器.
C标准库的Malloc/Free函数就是内存管理器.Win32下OS本身也提供了内存管理器.
在Win32下实现动态内存分配有三种方法(1)调用VirtualAlloc的虚拟存储器分配! (2)调用HeapAlloc系列函数实现的一种堆内存的内存管理器!(3)内存映射文件.
在Linux下提供了两种系统调用实现动态内存分配(1)调用brk实现进程内堆内存分配(参看前面的进程内存映像图)(2)使用mmap的内存映像.
关于操作系统的内存分配可以参考各自的资料(Linux方面的专著较少,但网上资料丰富,Win32方面微软出版的windows的书籍不少).
需要知道的是,c库中的malloc/free函数是通过操作系统的API(系统调用)实现的内存管理器.也就是说,你可以使用第三方的内存管理器,或者自己实现malloc/free.在K&R的著作里有简单的malloc/free实现!怎样使用虚拟内存提供的字节序列实现复杂数据格式属于一种技术,当然还要考虑内存分配回收的效率,使用的简单等许多因素!在c/c++等编译语言中,由内存引发的编程问题非常之多(另外一个就是并发了,以及并发和内存访问的”综合症”).如果你具备了各种基本的知识(包括前面的变量的概念,进程内存映像等等),遇到的问题就会少很多,或者可以很快找到问题之所在!但不是所有程序员对底层的实现都有很好的理解,所以现代许多语言比如Java就不允许程序员管理内存,设置不允许程序员对变量进行内存引用!
手动管理内存有其优缺点!尤其在复杂的程序结构中比如有异常流的执行中,还有比如Windows的消息机制下,手动分配和释放内存有很大的复杂度.
有没有更好的自动内存管理的方式呢?
介绍两种方法:
(1) 使用引用技术实现生存期自管理类型.
Borland Delphi的内存管理器提供了类似c语言的malloc/free/realloc 函数(分别是getmem/freemem/reallocmem),也提供了类似c++的 new/delete(分别是new/dispose).在这个内存管理器上面使用引用计数实现了ansistring(字符串),interface(接口),array(动态数组),variant(变体类型)几种类型的身存期自管理.
比如我前面提到的ansistring在这种类型数据的负偏移保留了内存变量对他的引用计数,如果引用计数为0则调用内存管理器函数释放内存,为了完成这种机制,编译器嵌入了不少用于实现这种机制的隐含代码(一般程序员是看不到的).如果是基于应用的快速开发,大可放心的使用这种机制减少手动内存分配.但如果涉及比较底层的开发,就需要了解这种机制,否则会付出很大的代价.
(Borland提供delphi实现的绝大多少源代码,大家可以自己看)
(2)垃圾回收机制
Java采用的是垃圾回收的完全自动的内存管理模式(垃圾回收有很多中算法,有兴趣可以阅读这方面的文章).需要知道的是,使用垃圾回收,程序员无法知道何时释放内存(更无法干涉内存分配释放的行为),还有就是垃圾回收相对来说管理内存的性能比较差!
采用什么样的内存管理方式,取决于程序的应用!c语言中完全可以各种方式的库实现内存管理,当然你也可以自己编写内存管理器用于自己的数据管理(不同操作系统上方法不同).
动态内存分配(包括各种内存管理器)会遇到什么问题呢?(c语言关于内存方面常见的编程问题可以在百度google中查找,多看几遍会有好处).其实也就两方面的问题,空间和时间
空间问题有一个比较典型的问题就是,如果一个程序中使用多个内存管理器实例,它们会有冲突,解决办法就是使用同一个内存管理器!时间的问题就是并发,动态内存管理天生的并发问题(比如多线程).
前一篇:重新学习 c 语言(4)- 库和宿主实现(三) 程序级异常