作者:Allen B. Downey
原文:Chapter 6 Memory management
译者:飞龙
协议:CC BY-NC-SA 4.0
C提供了4种用于动态内存分配的函数:
malloc
NULL
calloc
free
realloc
这套API是出了名的易错和苛刻。内存管理是设计大型系统中,最具有挑战性的一部分,它正是许多现代语言提供高阶内存管理特性,例如垃圾回收的原因。
C的内存管理API有点像Jasper Beardly,动画片《辛普森一家》中的一个配角,他是一个严厉的代课老师,喜欢体罚别人,并使用戒尺惩罚任何违规行为。
下面是一些应受到惩罚的程序行为:
这些规则听起来好像不难遵循,但是在一个大型程序中,一块内存可能由程序一部分分配,在另一个部分中使用,之后在其他部分中释放。所以一部分中的变化也需要其它部分跟着变化。
同时,同一个内存块在程序的不同部分中,也可能有许多别名或者引用。这些内存块在所有引用不再使用时,才应该被释放。正确处理这件事情通常需要细心的分析程序的所有部分,这非常困难,并且与良好的软件工程的基本原则相违背。
理论上,每个分配内存的函数都应包含内存如何释放的信息,作为接口文档的一部分。成熟的库通常做得很好,但是实际上,软件工程的实践通常不是这样理想化的。
内存错误非常难以发现,因为这些症状是不可预测的,这使得事情更加糟糕,例如:
你应该从中总结出一条规律,就是安全的内存管理需要设计和规范。如果你编写了一个分配内存的库或模块,你应该同时提供释放它的接口,并且内存管理从开始就应该作为API设计的一部分。
如果你使用了分配内存的库,你应该按照规范使用API。例如,如果库提供了分配和释放储存空间的函数,你应该一起使用或都不使用它们。例如,不要在不是malloc分配的内存块上调用free。你应该避免在程序的不同部分中持有相同内存块的多个引用。
通常在安全的内存管理和性能之间有个权衡。例如,内存错误的的最普遍来源是数组的越界写入。这一问题的最显然的解决方法就是边界检查。也就是说,每次对数组的访问都应该检查下标是否越界。提供数组结构的高阶库通常会进行边界检查。但是C风格数据和大多数底层库不会这样做。
有一种可能会也可能不会受到惩罚的内存错误。如果你分配了一块内存,并且没有释放它,就会产生“内存泄漏”。
对于一些程序,内存泄露是OK的。如果你的程序分配内存,对其执行计算,之后退出,这可能就不需要释放内存。当程序退出时,所有分配的内存都会由操作系统释放。在退出前立即释放内存似乎很负责任,但是通常很浪费时间。
但是如果一个程序运行了很长时间,并且泄露内存的话,它的内存总量会无限增长。此时会发生一些事情:
如果malloc返回了NULL,但是你仍旧把它当成分配的内存块进行访问,你会得到段错误。因此,在使用之前检查malloc的结果是个很好的习惯。一种选择是在每个malloc调用之后添加一个条件判断,就像这样:
void *p = malloc(size); if (p == NULL) { perror("malloc failed"); exit(-1); }
perror在stdio.h中声明,它会打印出关于最后发生的错误的错误信息和额外的信息。
perror
stdio.h
exit在stdlib.h中声明,会使进程终止。它的参数是一个表示进程如何终止的状态码。按照惯例,状态码0表示通常终止,-1表示错误情况。有时其它状态码用于表示不同的错误情况。
exit
stdlib.h
错误检查的代码十分讨厌,并且使程序难以阅读。但是你可以通过将库函数的调用和错误检查包装在你自己的函数中,来解决这个问题。例如,下面是检查返回值的malloc包装:
void *check_malloc(int size) { void *p = malloc (size); if (p == NULL) { perror("malloc failed"); exit(-1); } return p; }
由于内存管理非常困难,多数大型程序,例如Web浏览器都会泄露内存。你可以使用Unix的ps和top工具来查看系统上的哪个程序占用了最多的内存。
ps
top
当进程启动时,系统为text段、静态分配的数据、栈和堆分配空间,堆中含有动态分配的数据。
text
并不是所有程序都动态分配数据,所以堆的大小可能很小,或者为0。最开始堆只含有一个空闲块。
malloc调用时,它会检查这个空闲块是否足够大。如果不是,它会向系统请求更多内存。做这件事的函数叫做sbrk,它设置“程序中断点”(program break),你可以将其看做一个指向堆底部的指针。
sbrk
译者注:sbrk是Linux上的系统API,Windows上使用HeapAlloc和HeapFree来管理堆区。
HeapAlloc
HeapFree
sbrk调用时,它分配的新的物理内存页,更新进程的页表,并设置程序中断点。
理论上,程序应该直接调用sbrk(而不是通过malloc),并且自己管理堆区。但是malloc易于使用,并且对于大多数内存使用模式,它运行速度快并且高效利用内存。
为了实现内存管理API,多数Linux系统都使用ptmalloc,它基于dlmalloc,由Doug Lea编写。一篇描述这个实现要素的论文可在http://gee.cs.oswego.edu/dl/html/malloc.html访问。
ptmalloc
dlmalloc
对于程序员来说,需要注意的最重要的要素是:
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8