在这段插曲中,我们讨论了UNIX系统中的内存分配接口。所提供的接口相当简单,因此本章是简短的。我们要解决的主要问题是这个。
关键:如何分配和管理内存。在UNIX/C程序中,理解如何分配和管理内存对于构建健壮可靠的软件至关重要。常用的接口是什么?应该避免哪些错误!
14.1类型的内存
在运行C程序时,分配了两种类型的内存。第一个被称为堆栈内存,它的分配和分配是由编译器为您(程序员)隐式管理的。出于这个原因,它有时被称为自动内存。在C的堆栈上声明内存是很容易的。例如,假设您需要为一个名为x的整数函数func()中的某个空间,要声明这样的内存,您只需这样做。
编译器完成其余的工作,当您调用func()时,确保在堆栈上留出空间。当您从函数返回时,编译器将为您分配内存;因此,如果您想要一些信息在调用之外生存,您最好不要将该信息留在堆栈上。正是这种对长期记忆的需要,才使我们进入了第二种记忆。称为堆内存。正是这种对长寿命内存的需求使我们进入了第二种类型的内存,称为堆内存所有的分配和回收明确由你,程序员。这无疑是一个沉重的责任!当然还有很多错误的原因。但是如果你小心谨慎,注意的话,你会正确地使用这些接口,并且不会有太多的麻烦。下面是一个如何在堆上分配整数的示例。
关于这个小代码片段的一些注释。首先,您可能注意到堆栈和堆分配都发生在这一行上:首先,编译器知道在看到您声明的指针(int *x)时,为指向整数的指针预留空间;随后,当程序调用malloc()时,它请求堆上一个整数的空间;例程返回这样一个整数的地址(在成功时,或在失败时为NULL),然后将其存储在堆栈中以供程序使用。由于它的显式特性,并且由于它的使用更加多样化,堆内存对用户和系统都带来了更多的挑战。因此,它是我们讨论的其余部分的重点。
14.2 malloc()调用
malloc()调用非常简单:您传递一个大小请求堆上的某个空间,它要么成功,并返回一个指向新分配的空间的指针,要么失败,返回NULL2。手册页显示了使用malloc需要做什么;在命令行键入man malloc,您将看到。
从这个信息中,您可以看到,您所需要做的就是包含头文件stdlib。使用malloc h。实际上,您实际上并不需要这样做,因为C库(在默认情况下是所有C程序链接的)都有malloc()的代码;添加标题只是让编译器检查您是否正确地调用malloc()(例如,传递正确类型的参数的正确数量)。
malloc()的唯一参数是类型大小的t,它简单地描述了您需要多少字节。但是,大多数程序员不会直接在这里输入数字(比如10);事实上,这样做将被认为是一种糟糕的形式。相反,使用了各种例程和宏。例如,要为双精度浮点值分配空间,只需这样做。
小贴士:当有疑问时,试一试。
如果你不确定你使用的是怎样的程序或操作,没有什么可以替代简单的尝试,并确保它的行为符合你的期望。虽然阅读手册页或其他文档是有用的,但在实践中如何工作才是重要的。编写一些代码并测试它!这无疑是确保代码符合您的要求的最佳方式。事实上,这就是我们所做的检查我们所说的关于sizeof()的事情是正确的。哇,那可真是一大笔钱啊!malloc()调用使用sizeof()操作符请求适当的空间量。在C中,这通常被认为是编译时的操作符,这意味着实际大小在编译时是已知的,因此一个数字(在本例中是8,对于一个double)被替换为malloc()的参数。
在C中,这通常被认为是编译时的操作符,这意味着实际大小在编译时是已知的,因此一个数字(在本例中是8,对于一个double)被替换为malloc()的参数。由于这个原因,sizeof()被正确地认为是一个操作符而不是一个函数调用(函数调用在运行时发生)。您还可以将变量的名称(不仅仅是类型)传递给sizeof(),但是在某些情况下,您可能得不到预期的结果,所以要小心。例如,让我们看看下面的代码片段。
在第一行中,我们为一个10个整数的数组声明了空间,这是很好的和dandy。但是,当我们在下一行使用sizeof()时,它会返回一个很小的值,比如4(在32位机器上)或8(在64位机器上)。原因是,在这种情况下,sizeof()认为我们只是在询问一个指向整数的指针有多大,而不是我们动态分配了多少内存。然而,有时sizeof()会像您期望的那样工作。
在这种情况下,有足够的静态信息让编译器知道已经分配了40个字节。另一个需要注意的地方是字符串。当为一个字符串声明空间时,使用下面的用法:malloc(strlen(s) + 1),它使用函数strlen()获取字符串的长度,并将1添加到它,以便为字符串结束字符留出空间。使用sizeof()可能会带来麻烦。您可能还会注意到malloc()返回一个指向void类型的指针。这样做只是在C中传递一个地址,并让程序员决定如何处理它。程序员通过使用所谓的cast进一步帮助解决问题;在上面的示例中,程序员将malloc()的返回类型转换为指向double的指针。除了告诉编译器之外,Casting并没有真正完成任何事情。其他程序员可能会阅读你的代码。是的,我知道我在做什么。通过使用malloc()的结果,程序员只是给出了一些保证;对于正确性来说,不需要cast。
14.3 The free() Call
事实证明,分配内存是等式中最简单的部分;知道什么时候,如何,甚至是自由记忆是最困难的部分。为了释放不再使用的堆内存,程序员只需调用free。
例程采用一个参数,一个由malloc()返回的指针。因此,您可能会注意到,已分配区域的大小并没有被用户传递,并且必须由内存分配库本身跟踪。
14.4常见的错误
在使用malloc()和free()时出现了许多常见错误。这是我们在本科教学系统课程中反复看到的一些例子。所有这些示例都从编译器中编译并运行了一个peep;编译C程序是构
建一个正确的C程序所必需的,但这远远不够,因为你将会学习到(通常是困难的)正确的内存管理一直是一个问题,事实上,许多更新的语言都支持自动内存管理。在这种
语言中,当您调用类似于malloc()的东西来分配内存(通常是新的或类似于分配新对象的东西)时,您永远不需要调用任何东西来释放空间;相反,垃圾收集器运行并计算出您
不再引用的内存,并将其释放给您。
忘记分配内存
许多例程期望在调用它们之前分配内存。例如,常规strcpy(dst, src)从源指针复制一个字符串到目标指针。然而,如果你不小心,你可能会这样做。
当你运行这段代码时,它很可能会导致一个分割错误,这是一个很花哨的术语,因为你做错了内存,你这个愚蠢的程序员,我很生气。
在这种情况下,正确的代码应该是这样的。
另外,您可以使用strdup(),使您的生活更轻松。阅读strdup手册页了解更多信息。
提示:它编译或运行它不等于是正确的。
仅仅因为一个程序编译(!)甚至一次运行一次或多次正确并不意味着程序是正确的。许多事件可能会使你达到你认为它有效的程度,但随后发生了一些变化并停止了。
一个常见的学生反应是说(或大喊)但它以前工作过!然后责怪编译器,操作系统,硬件,甚至(我们敢说)教授。但问题通常是在你认为它会在你的代码中。在责怪其他
组件之前,先开始工作并调试它。
没有足够的内存分配
一个相关的错误是没有分配足够的内存,有时称为缓冲区溢出。在上面的示例中,常见的错误是为目标缓冲区提供足够的空间。
奇怪的是,这取决于malloc是如何实现的以及许多其他细节,这个程序通常看起来是正确的。在某些情况下,当字符串复制执行时,
它会在已分配的空间的末尾写入一个字节,但在某些情况下,这是无害的,可能改写了不再使用的变量。在某些情况下,这些溢出可能
是非常有害的,而且实际上是系统中许多安全漏洞的根源[W06]。在其他情况下,malloc库无论如何都分配了一些额外的空间,因此您的程序
实际上不会在某些地方乱写。
忘记初始化已分配的内存。
有了这个错误,您可以正确地调用malloc(),但是忘记在新分配的数据类型中填充一些值。不要这样做!如果您忘记了,您的程序最终会遇到未初始化的read。
它从堆中读取一些数据的未知值。谁知道可能在那里?如果你幸运的话,一些值,程序仍然工作(例如,0)。如果你不幸运,一些随机的和有害的东西。
忘记释放内存
另一个常见错误是内存泄漏,它发生在您忘记释放内存的时候。在长时间运行的应用程序或系统(例如OS本身)中,这是一个巨大的问题,因为缓慢的内存泄漏
最终导致内存耗尽,因此需要重新启动。因此,一般来说,当您完成了一段内存时,您应该确保将其释放。注意,使用垃圾收集的语言在这里没有帮助:如果您
仍然有一些内存的引用,没有垃圾收集器将会释放它,因此内存泄漏仍然是一个问题,即使在更多的m中也是如此。
在某些情况下,似乎不调用free()是合理的。例如,您的程序是短暂的,并且将很快退出;在这种情况下,当进程结束时,操作系统将清理其分配的所有页面,因
此不会发生内存泄漏。虽然这肯定能奏效(请参阅第7页的内容),但这可能是一个坏习惯,所以要谨慎选择这样的策略。从长远来看,作为一名程序员,你的目标
之一就是养成良好的习惯;其中一个习惯就是理解你如何管理内存,以及(在C语言中),释放你hav的模块。
在使用它之前释放内存。
有时一个程序在使用它之前会释放内存;这样的错误被称为悬浮指针,你可以猜到,它也是一件坏事。随后的使用可能会破坏程序,或者改写有效的内存(例如,
您调用free(),然后再调用malloc()来分配其他的东西,然后再回收错误释放的内存。
反复释放内存
程序有时也会释放内存不止一次;这就是所谓的双重自由。这样做的结果是没有定义的。你可以想象,内存分配库可能会被混淆,会做各种奇怪的事情;崩溃是一个常见的结果。
调用free()不正确
我们讨论的最后一个问题是不正确地调用free()。毕竟,free()只希望将您从malloc()早期接收到的指针之一传递给它。当你传递一些其他的价值时,坏事就会发生。因此,这种
无效的释放是危险的,当然也应该避免。
旁白:一旦您的进程退出,为什么没有内存泄漏。
当您编写一个短期程序时,您可能会使用malloc()来分配一些空间。程序运行并即将完成:在退出之前是否需要调用free()一段时间?虽然这似乎是错误的,但没有任何真正意义
上的记忆丧失。原因很简单:系统中实际上有两个级别的内存管理。第一级内存管理由操作系统执行,操作系统在运行时将内存分发给进程,并在进程退出(或其他die)时将其
收回。第二个层次的管理是在每个流程中,例如当您调用malloc()和free()时在堆内。即使您没有调用free()(也因此在堆中泄漏内存),操作系统将回收进程的所有内存(包括代码
、堆栈和堆),当程序结束运行时。无论您在地址空间中的堆的状态是什么,当进程结束时,操作系统会收回所有这些页面,从而确保没有内存丢失,尽管您没有释放它。
因此,对于短时间的程序,泄漏内存常常不会导致任何操作问题(尽管它可能被认为是糟糕的表单)。当您编写一个长时间运行的服务器(如web服务器或数据库管理系统,它永远
不会退出)时,泄漏的内存是一个更大的问题,当应用程序耗尽内存时,它最终会导致崩溃。当然,内存泄漏在一个特定的程序中是一个更大的问题:操作系统本身。再次向我们
展示:那些编写内核代码的人的工作是最艰巨的…
正如你所看到的,有很多方法可以滥用内存。由于频繁的内存错误,一个完整的工具生态圈已经开发出来帮助您在代码中发现这些问题。检查purify [HJ92]和valgrind [SN05];
- 摘要
它们都非常擅长帮助您定位与内存相关的问题。一旦你习惯了使用这些强大的工具,你就会怀疑没有它们你是如何生存的
底层操作系统支持
您可能已经注意到,在讨论malloc()和free()时,我们没有讨论系统调用。原因很简单:它们不是系统调用,而是库调用。因此,malloc库管理虚拟地址空间中的空间,但它本身
构建在一些系统调用之上,这些系统调用调用操作系统以请求更多内存或释放一些回给系统。
一个这样的系统调用叫做brk,它用来改变程序的位置:堆的末端位置。它需要一个参数(新断点的地址),因此根据新的break是否大于或小于当前中断,可以增加或减小堆的大小。
一个额外的调用sbrk通过了一个增量,但是其他的服务也有类似的目的。
注意,您不应该直接调用brk或sbrk。它们被内存分配库使用;如果你试着去使用它们,你很可能会犯错误。使用malloc()和free()代替。
最后,您还可以通过mmap()调用从操作系统获得内存。通过传入正确的参数,mmap()可以在您的程序中创建一个匿名内存区域,该区域不与任何特定文件相关联,而是与交换
空间有关,我们稍后将在虚拟内存中详细讨论这个问题。这段内存也可以像堆一样处理,并进行管理。请阅读mmap的手册页了解更多细节
14.6 Other Calls-其他调用
还有一些内存分配库支持的调用。例如,calloc()分配内存,并在返回之前将其归零;这可以防止一些错误,在这些错误中,您假设内存是零,并且忘记自己初始化内存(参见上面
未初始化的段落)。常规realloc()也可以是有用的,当您为某个东西(比如一个数组)分配了空间,然后需要向它添加一些东西:realloc()创建一个新的更大的内存区域,将旧区域复
制到它,并返回指向新区域的指针。
14.7总结
我们介绍了一些处理内存分配的api。 和往常一样,我们已经介绍了基本知识;更多细节可 其他地方。阅读C - book [KR88]和Stevens [SR05](第7章)。 更多的信息。这是一篇关于
如何检测和纠正的很酷的现代论文。 许多这些问题会自动出现,见Novark等人[N+07];这 论文还包含了一些常见问题和一些简洁的总结。 如何找到并修复它们的想法。