Think OS 翻译程序员

操作系统思考 第六章 内存管理

2016-07-07  本文已影响118人  布客飞龙

第六章 内存管理

作者:Allen B. Downey

原文:Chapter 6 Memory management

译者:飞龙

协议:CC BY-NC-SA 4.0

C提供了4种用于动态内存分配的函数:

这套API是出了名的易错和苛刻。内存管理是设计大型系统中,最具有挑战性的一部分,它正是许多现代语言提供高阶内存管理特性,例如垃圾回收的原因。

6.1 内存错误

C的内存管理API有点像Jasper Beardly,动画片《辛普森一家》中的一个配角,他是一个严厉的代课老师,喜欢体罚别人,并使用戒尺惩罚任何违规行为。

下面是一些应受到惩罚的程序行为:

这些规则听起来好像不难遵循,但是在一个大型程序中,一块内存可能由程序一部分分配,在另一个部分中使用,之后在其他部分中释放。所以一部分中的变化也需要其它部分跟着变化。

同时,同一个内存块在程序的不同部分中,也可能有许多别名或者引用。这些内存块在所有引用不再使用时,才应该被释放。正确处理这件事情通常需要细心的分析程序的所有部分,这非常困难,并且与良好的软件工程的基本原则相违背。

理论上,每个分配内存的函数都应包含内存如何释放的信息,作为接口文档的一部分。成熟的库通常做得很好,但是实际上,软件工程的实践通常不是这样理想化的。

内存错误非常难以发现,因为这些症状是不可预测的,这使得事情更加糟糕,例如:

你应该从中总结出一条规律,就是安全的内存管理需要设计和规范。如果你编写了一个分配内存的库或模块,你应该同时提供释放它的接口,并且内存管理从开始就应该作为API设计的一部分。

如果你使用了分配内存的库,你应该按照规范使用API。例如,如果库提供了分配和释放储存空间的函数,你应该一起使用或都不使用它们。例如,不要在不是malloc分配的内存块上调用free。你应该避免在程序的不同部分中持有相同内存块的多个引用。

通常在安全的内存管理和性能之间有个权衡。例如,内存错误的的最普遍来源是数组的越界写入。这一问题的最显然的解决方法就是边界检查。也就是说,每次对数组的访问都应该检查下标是否越界。提供数组结构的高阶库通常会进行边界检查。但是C风格数据和大多数底层库不会这样做。

6.2 内存泄漏

有一种可能会也可能不会受到惩罚的内存错误。如果你分配了一块内存,并且没有释放它,就会产生“内存泄漏”。

对于一些程序,内存泄露是OK的。如果你的程序分配内存,对其执行计算,之后退出,这可能就不需要释放内存。当程序退出时,所有分配的内存都会由操作系统释放。在退出前立即释放内存似乎很负责任,但是通常很浪费时间。

但是如果一个程序运行了很长时间,并且泄露内存的话,它的内存总量会无限增长。此时会发生一些事情:

如果malloc返回了NULL,但是你仍旧把它当成分配的内存块进行访问,你会得到段错误。因此,在使用之前检查malloc的结果是个很好的习惯。一种选择是在每个malloc调用之后添加一个条件判断,就像这样:

void *p = malloc(size);
if (p == NULL) {
    perror("malloc failed");
    exit(-1);
}

perrorstdio.h中声明,它会打印出关于最后发生的错误的错误信息和额外的信息。

exitstdlib.h中声明,会使进程终止。它的参数是一个表示进程如何终止的状态码。按照惯例,状态码0表示通常终止,-1表示错误情况。有时其它状态码用于表示不同的错误情况。

错误检查的代码十分讨厌,并且使程序难以阅读。但是你可以通过将库函数的调用和错误检查包装在你自己的函数中,来解决这个问题。例如,下面是检查返回值的malloc包装:

void *check_malloc(int size)
{
  void *p = malloc (size);
  if (p == NULL) {
    perror("malloc failed");
    exit(-1);
  }
  return p;
}

由于内存管理非常困难,多数大型程序,例如Web浏览器都会泄露内存。你可以使用Unix的pstop工具来查看系统上的哪个程序占用了最多的内存。

6.3 实现

当进程启动时,系统为text段、静态分配的数据、栈和堆分配空间,堆中含有动态分配的数据。

并不是所有程序都动态分配数据,所以堆的大小可能很小,或者为0。最开始堆只含有一个空闲块。

malloc调用时,它会检查这个空闲块是否足够大。如果不是,它会向系统请求更多内存。做这件事的函数叫做sbrk,它设置“程序中断点”(program break),你可以将其看做一个指向堆底部的指针。

译者注:sbrk是Linux上的系统API,Windows上使用HeapAllocHeapFree来管理堆区。

sbrk调用时,它分配的新的物理内存页,更新进程的页表,并设置程序中断点。

理论上,程序应该直接调用sbrk(而不是通过malloc),并且自己管理堆区。但是malloc易于使用,并且对于大多数内存使用模式,它运行速度快并且高效利用内存。

为了实现内存管理API,多数Linux系统都使用ptmalloc,它基于dlmalloc,由Doug Lea编写。一篇描述这个实现要素的论文可在http://gee.cs.oswego.edu/dl/html/malloc.html访问。

对于程序员来说,需要注意的最重要的要素是:

上一篇 下一篇

猜你喜欢

热点阅读