文将对 Linux™ 程序员可以使用的 内存 管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文 将为您提供如何管理 内存 的细节,然后将进一步展示如何手工管理 内存 ,如何使用引用计数或者 内存 池来半手工地管理 内存 ,以及如何使用垃圾收集自动管理 内存 。 为什么必须管理 内存 内存 管理是计算机编程最为基本的领域之一。在很多脚本语言 ,您不必担心 内存 是如何管理的,这并不能使得 内存 管理的重要性有一点点降低。对实际编程来说,理解您的 内存 管理器的能力与局限性至关重要。在大部分系统语言 ,比如 C 和 C++,您必须进行 内存 管理。本文将介绍手工的、半手工的以及自动的 内存 管理实践的基本概念。 追溯到在 Apple II 上进行汇编语言编程的时代,那时 内存 管理还不是个大问题。您实际上在运行整个系统。系统有多少 内存 ,您就有多少 内存 。您甚至不必费心思去弄明白它有多少 内存 ,因为每一台机器的 内存 数量都相同。所以,如果 内存 需要非常固定,那么您只需要选择一个 内存 范围并使用它即可。 不过,即使是在这样一个简单的计算机 ,您也 有问题,尤其是当您不知道程序的每个部分将需要多少 内存 时。如果您的空间有限,而 内存 需求是变化的,那么您需要一些方法来满足这些需求: 确定您 是否 有足够的 内存 来处理数据。 从可用的 内存 获取一部分 内存 。 向可用 内存 池(pool) 返回部分 内存 ,以使其可以由程序的其他部分或者其他程序使用。 实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收 内存 。程序的动态性越强, 内存 管理就越重要,您的 内存 分配程序的选择也就更重要。让我们来了解可用于 内存 管理的不同方法,它们的好处与不足,以及它们最适用的情形。 malloc :该 函数 分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用 内存 ,那么它返回一个空指针。 free:该 函数 获得指向由 malloc 分配的 内存 片段的指针,并将其 释放 ,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将 内存 归还给程序,而无法将 内存 归还给操作系统)。 物理 内存 和虚拟 内存 要理解 内存 在程序 是如何分配的,首先需要理解如何将 内存 从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理 内存 。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部 内存 。实际上,这些进程使用的是 虚拟 内存 。 只是作为一个例 ,让我们假定您的程序正在访问地址为 629 的 内存 。不过,虚拟 内存 系统不需要将其存储在位置为 629 的 RAM 。实际上,它甚至可以不在 RAM —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映 内存 所在的物理位置,所以它们被称为虚拟 内存 。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM ,那么操作系统将暂时停止您的进程,将其他 内存 转存到硬盘 ,从硬盘上加载被请求的 内存 ,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的 内存 更多的 内存 。 在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存 。现在,大部分人的系统上并没有 4 GB 内存 ,即使您将 swap 也算上, 每个进程所使用的 内存 也肯定少于 4 GB。因此,当加载一个进程时,它 得到一个取决于某个称为 系统 断点(system break)的特定地址的初始 内存 分配。该地址之后是未被映射的 内存 —— 用于在 RAM 或者硬盘 没有分配相应物理位置的 内存 。因此,如果一个进程运行超出了它初始分配的 内存 ,那么它必须请求操作系统“映射进来(map in)”更多的 内存 。(映射是一个表示一一对应关系的数学术语 —— 当 内存 的虚拟地址有一个对应的物理地址来存储 内存 内容时,该 内存 将被映射。) 基于 UNIX 的系统有两个可映射到附加 内存 的基本系统调用: brk: brk() 是一个非常简单的系统调用。还记得系统 断点吗?该位置是进程映射的 内存 边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加 内存 或者从进程取走 内存 。 mmap: mmap(),或者说是“ 内存 映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的 内存 ,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写 内存 将对文件 的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的 内存 的能力。 munmap() 所做的事情与 mmap() 相反。 如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额 的虚拟 内存 。在我们的例 将使用 brk(),因为它更简单,更通用。 实现一个简单的分配程序 如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc () 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统 是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理 内存 时都涉及到了哪些事情。 要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc .c 的文件 。接下来,我将一次一个部分地对该清单进行解释。 在大部分操作系统 内存 分配由以下两个简单的 函数 来处理: void * malloc (long numbytes):该 函数 负责分配 numbytes 大小的 内存 ,并返回指向第一个字节的指针。 void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该 函数 将分配的空间归还给进程的“空闲空间”。 malloc _init 将是初始化 内存 分配程序的 函数 。它要完成以下三件事:将分配程序标识为已经初始化,找到系统 最后一个有效 内存 地址,然后建立起指向我们管理的 内存 的指针。这三个变量都是全局变量: 清单 1. 我们的简单分配程序的全局变量 int has_initialized = 0; void *managed_memory_start; void *last_valid_address; 如前所述,被映射的 内存 的边界(最后一个有效地址)常被称为系统 断点或者 当前 断点。在很多 UNIX® 系统 ,为了指出当前系统 断点,必须使用 sbrk(0) 函数 。 sbrk 根据参数 给出的字节数移动当前系统 断点,然后返回新的系统 断点。使用参数 0 只是返回当前 断点。这里是我们的 malloc 初始化代码,它将找到当前 断点并初始化我们的变量: 清单 2. 分配程序初始化 函数 /* Include the sbrk function */ #include void malloc _init() /* grab the last valid address from the OS */ last_valid_address = sbrk(0); /* we don't have any memory to manage yet, so *just set the beginning to be last_valid_address managed_memory_start = last_valid_address; /* Okay, we're initialized and ready to go */ has_initialized = 1; 现在,为了完全地管理 内存 ,我们需要能够追踪要分配和回收哪些 内存 。在对 内存 块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的 内存 块。因此, malloc 返回的每块 内存 的起始处首先要有这个结构: 清单 3. 内存 控制块结构定义 struct mem_control_block { int is_available; int size; 现在,您可能 认为当程序调用 malloc 时这 引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们 将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的 内存 。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的 内存 。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个 内存 字节就可以再次找到这个结构。 在讨论分配 内存 之前,我们将先讨论 释放 ,因为它更简单。为了 释放 内存 ,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码: 清单 4. 解除分配 函数 void free(void *firstbyte) { struct mem_control_block *mcb; /* Backup from the given pointer to find the * mem_control_block mcb = firstbyte - sizeof(struct mem_control_block); /* Mark the block as being available */ mcb->is_available = 1; /* That's It! We're done. */ return; 如您所见,在这个分配程序 内存 释放 使用了一个非常简单的机制,在固定时间内完成 内存 释放 。分配 内存 稍微困难一些。以下是该算法的略述: 清单 5. 主分配程序的伪代码 1. If our allocator has not been initialized, initialize it. 2. Add sizeof(struct mem_control_block) to the size requested. 3. start at managed_memory_start. 4. Are we at last_valid address? 5. If we are: A. We didn't find any existing space that was large enough -- ask the operating system for more and return that. 6. Otherwise: A. Is the current space available (check is_available from the mem_control_block)? B. If it is: i) Is it large enough (check "size" from the mem_control_block)? ii) If so: a. Mark it as unavailable b. Move past mem_control_block and return the pointer iii) Otherwise: a. Move forward "size" bytes b. Go back go step 4 C. Otherwise: i) Move forward "size" bytes ii) Go back to step 4 我们主要使用连接的指针遍历 内存 来寻找开放的 内存 块。这里是代码: 清单 6. 主分配程序 void * malloc (long numbytes) { /* Holds where we are looking in memory */ void *current_location; /* This is the same as current_location, but cast to a * memory_control_block struct mem_control_block *current_location_mcb; /* This is the memory location we will return. It will * be set to 0 until we find something suitable void *memory_location; /* Initialize if we haven't already done so */ if(! has_initialized) { malloc _init(); /* The memory we search for has to include the memory * control block, but the users of malloc don't need * to know this, so we'll just add it in for them. numbytes = numbytes + sizeof(struct mem_control_block); /* Set memory_location to 0 until we find a suitable * location memory_location = 0; /* Begin searching at the start of managed memory */ current_location = managed_memory_start; /* Keep going until we have searched all allocated space */ while(current_location != last_valid_address) /* current_location and current_location_mcb point * to the same address. However, current_location_mcb * is of the correct type, so we can use it as a struct. * current_location is a void pointer so we can use it * to calculate addresses. current_location_mcb = (struct mem_control_block *)current_location; if(current_location_mcb->is_available) if(current_location_mcb->size >= numbytes) /* Woohoo! We've found an open, * appropriately-size location. /* It is no longer available */ current_location_mcb->is_available = 0; /* We own it */ memory_location = current_location; /* Leave the loop */ break; /* If we made it here, it's because the Current memory * block not suitable; move to the next one current_location = current_location + current_location_mcb->size; /* If we still don't have a valid location, we'll * have to ask the operating system for more memory if(! memory_location) /* Move the program break numbytes further */ sbrk(numbytes); /* The new memory will be where the last valid * address left off memory_location = last_valid_address; /* We'll move the last valid address forward * numbytes last_valid_address = last_valid_address + numbytes; /* We need to initialize the mem_control_block */ current_location_mcb = memory_location; current_location_mcb->is_available = 0; current_location_mcb->size = numbytes; /* Now, no matter what (well, except for error conditions), * memory_location has the address of the memory, including * the mem_control_block /* Move the pointer past the mem_control_block */ memory_location = memory_location + sizeof(struct mem_control_block); /* Return the pointer */ return memory_location; 这就是我们的 内存 管理器。现在,我们只需要构建它,并在程序 使用它即可。 运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些 函数 ,不过, malloc () 和 free() 才是最主要的 函数 ): 清单 7. 编译分配程序 gcc -shared -fpic malloc .c -o malloc .so 该程序将生成一个名为 malloc .so 的文件,它是一个包含有我们的代码的共享库。 在 UNIX 系统 ,现在您可以用您的分配程序来取代系统的 malloc (),做法如下: 清单 8. 替换您的标准的 malloc LD_PRELOAD=/path/to/ malloc .so export LD_PRELOAD LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库 的符号赋予优先权。因此,从现在起,该 的任何应用程序都将使用我们的 malloc (),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc (),不过它们是例 。其他使用 realloc() 等其他 内存 管理 函数 的应用程序,或者错误地假定 malloc () 内部行为的那些应用程序,很可能 崩溃。ash shell 似乎可以使用我们的新 malloc () 很好地工作。 如果您想确保 malloc () 正在被使用,那么您应该通过向 函数 的入口点添加 write() 调用来进行测试。 我们的 内存 管理器在很多方面都还存在欠缺,但它可以有效地展示 内存 管理需要做什么事情。它的某些缺点包括: 由于它对系统 断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。 当分配 内存 时,在最坏的情形下,它将不得不遍历 全部进程 内存 ;其 可能包括位于硬盘上的很多 内存 ,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘 移出数据。 没有很好的 内存 不足处理方案( malloc 只假定 内存 分配是成功的)。 它没有实现很多其他的 内存 函数 ,比如 realloc()。 由于 sbrk() 可能 交回比我们请求的更多的 内存 ,所以在堆(heap)的末端 遗漏一些 内存 。 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。 分配程序不是线程安全的。 分配程序不能将空闲空间拼合为更大的 内存 块。 分配程序的过于简单的匹配算法 导致产生很多潜在的 内存 碎片。 我确信还有很多其他问题。这就是为什么它只是一个例 ! 其他 malloc 实现 malloc () 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其 包括: 分配的速度。 回收的速度。 有线程的环境的行为。 内存 将要被用光时的行为。 局部缓存。 簿记(Bookkeeping) 内存 开销。 虚拟 内存 环境 的行为。 小的或者大的对象。 实时保证。 每一个实现都有其自身的优缺点集合。在我们的简单的分配程序 ,分配非常慢,而回收非常快。另 ,由于它在使用虚拟 内存 系统方面较差,所以它最适于处理大的对象。 还有其他许多分配程序可以使用。其 包括: Doug Lea Malloc :Doug Lea Malloc 实际上是完整的一组分配程序,其 包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 pt malloc 。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近 释放 内存 。 pt malloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分 ,有一篇描述 Doug Lea 的 Malloc 实现的文章。 BSD Malloc :BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之 ,这个分配程序可以从预先确实大小的对象构成的池 分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能 浪费 内存 。在 参考资料部分 ,有一篇描述该实现的文章。 Hoard:编写 Hoard 的目标是使 内存 分配在多线程环境 进行得非常快。因此,它的构造以锁的使用为 心,从而使所有进程不必等待分配 内存 。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分 ,有一篇描述该实现的文章。 众多可用的分配程序 最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序 内存 分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常 带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料 的链接)。它有点过时,因为它没有考虑虚拟 内存 环境,不过大部分算法都是基于前面给出的 函数 。 在 C++ ,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”) ,描述了一个小对象分配程序(请参阅 参考资料 的链接)。 基于 malloc () 的 内存 管理的缺点 不只是我们的 内存 管理器有缺点,基于 malloc () 的 内存 管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc () 来管理 内存 可能 非常令人失望。如果您有大量的不固定的 内存 引用,经常难以知道它们何时被 释放 。生存期局限于当前 函数 内存 非常容易管理,但是对于生存期超出该范围的 内存 来说,管理 内存 则困难得多。而且,关于 内存 管理是由进行调用的程序还是由被调用的 函数 来负责这一问题,很多 API 都不是很明确。 因为管理 内存 的问题,很多程序倾向于使用它们自己的 内存 管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理 内存 分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究 内存 管理的其他选择。 引用计数是一种 半自动(semi-automated)的 内存 管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成 内存 管理任务。 在引用计数 ,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序 将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就 将引用计数减少 1。结束这个动作之后,它还 检查计数 是否 已经减到零。如果是,那么它将 释放 内存 。 这样做的好处是,您不必追踪程序 某个给定的数据结构可能 遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时 释放 该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数 函数 。另 ,内置 函数 和第三方的库不 知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。 要实现引用计数,您只需要两个 函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时 释放 内存 。 一个示例引用计数 函数 集可能看起来如下所示: 清单 9. 基本的引用计数 函数 /* Structure Definitions*/ /* Base structure that holds a refcount */ struct refcountedstruct int refcount; /* All refcounted structures must mirror struct * refcountedstruct for their first variables /* Refcount maintenance functions */ /* Increase reference count */ void REF(void *data) struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount++; /* Decrease reference count */ void UNREF(void *data) struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount--; /* Free the structure if there are no more users */ if(rstruct->refcount == 0) free(rstruct); REF 和 UNREF 可能 更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在 释放 内存 之前要调用的 函数 的指针(类似于面向对象语言 的析构 函数 —— 如果您的结构 包含这些指针,那么这是 必需的)。 当使用 REF 和 UNREF 时,您需要遵守这些指针的分配规则: UNREF 分配前左端指针(left-hand-side pointer)指向的值。 REF 分配后左端指针(left-hand-side pointer)指向的值。 在传递使用引用计数的结构的 函数 函数 需要遵循以下这些规则: 在 函数 的起始处 REF 每一个指针。 在 函数 的结束处 UNREF 第一个指针。 以下是一个使用引用计数的生动的代码示例: 清单 10. 使用引用计数的示例 /* EXAMPLES OF USAGE */ /* Data type to be refcounted */ struct mydata int refcount; /* same as refcountedstruct */ int datafield1; /* Fields specific to this struct */ int datafield2; /* other declarations would go here as appropriate */ /* Use the functions in code */ void dosomething(struct mydata *data) REF(data); /* Process data */ /* when we are through */ UNREF(data); struct mydata *globalvar1; /* Note that in this one, we don't decrease the * refcount since we are maintaining the reference * past the end of the function call through the * global variable void storesomething(struct mydata *data) REF(data); /* passed as a parameter */ globalvar1 = data; REF(data); /* ref because of Assignment */ UNREF(data); /* Function finished */ 由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 malloc 和 free 等低层的分配程序来实际地分配和 释放 它们的 内存 。 在 Perl 等高级语言 ,进行 内存 管理时使用引用计数非常广泛。在这些语言 ,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这 对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处: 实现简单。 易于使用。 由于引用是数据结构的一部分,所以它有一个好的缓存位置。 不过,它也有其不足之处: 要求您永远不要忘记调用引用计数 函数 。 无法 释放 作为循环数据结构的一部分的结构。 减缓几乎每一个指针的分配。 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 try 或 setjmp()/ longjmp())时,您必须采取其他方法。 需要额 内存 来处理引用。 引用计数占用了结构 的第一个位置,在大部分机器 最快可以访问到的就是这个位置。 在多线程环境 更慢也更难以使用。 C++ 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书 的“Smart Pointers”那一章。 内存 池是另一种半自动 内存 管理方法。 内存 池帮助某些程序进行自动 内存 管理,这些程序 经历一些特定的阶段,而且每个阶段 都有分配给进程的特定阶段的 内存 。例如,很多网络服务器进程都 分配很多针对每个连接的 内存 —— 内存 的最大生存期限为当前连接的存在期。Apache 使用了池式 内存 (pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的 内存 池。在结束每个阶段时, 一次 释放 所有 内存 。 在池式 内存 管理 ,每次 内存 分配都 指定 内存 池,从 分配 内存 。每个 内存 池都有不同的生存期限。在 Apache ,有一个持续时间为服务器存在期的 内存 池,还有一个持续时间为连接的存在期的 内存 池,以及一个持续时间为请求的存在期的池,另 还有其他一些 内存 池。因此,如果我的一系列 函数 生成比连接持续时间更长的数据,那么我就可以完全从连接池 分配 内存 ,并知道在连接结束时,这些 内存 被自动 释放 。另 ,有一些实现允许注册 清除 函数 (cleanup functions),在清除 内存 池之前,恰好可以调用它,来完成在 内存 被清理前需要完成的其他所有任务(类似于面向对象 的析构 函数 )。 要在自己的程序 使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本 默认 包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式 内存 实现,请参阅 参考资料部分 指向这些实现的文档的链接。 下面的假想代码列表展示了如何使用 obstack: 清单 11. obstack 的示例代码 #include #include /* Example code listing for using obstacks */ /* Used for obstack macros (x malloc is a malloc function that exits if memory is exhausted */ #define obstack_chunk_alloc x malloc #define obstack_chunk_free free /* Pools */ /* Only permanent allocations should go in this pool */ struct obstack *global_pool; /* This pool is for per-connection data */ struct obstack *connection_pool; /* This pool is for per-request data */ struct obstack *request_pool; void allocation_failed() exit(1); int main() /* Initialize Pools */ global_pool = (struct obstack *) x malloc (sizeof (struct obstack)); obstack_init(global_pool); connection_pool = (struct obstack *) x malloc (sizeof (struct obstack)); obstack_init(connection_pool); request_pool = (struct obstack *) x malloc (sizeof (struct obstack)); obstack_init(request_pool); /* Set the error handling function */ obstack_alloc_failed_handler = &allocation_failed; /* Server main loop */ while(1) wait_for_connection(); /* We are in a connection */ while(more_requests_available()) /* Handle request */ handle_request(); /* Free all of the memory allocated * in the request pool obstack_free(request_pool, NULL); /* We're finished with the connection, time * to free that pool obstack_free(connection_pool, NULL); int handle_request() /* Be sure that all object allocations are allocated * from the request pool int bytes_i_need = 400; void *data1 = obstack_alloc(request_pool, bytes_i_need); /* Do stuff to process the request */ /* return */ return 0; 基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 释放 。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的 内存 ,那么它也可以使用更长期限的 obstack,比如连接或者全局 内存 。传递给 obstack_free() 的 NULL 指出它应该 释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。 使用池式 内存 分配的益处如下所示: 应用程序可以简单地管理 内存 内存 分配和回收更快,因为每次都是在一个池 完成的。分配可以在 O(1) 时间内完成, 释放 内存 池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下 除以一个大的因数,使其变成 O(1))。 可以预先分配错误处理池(Error-handling pools),以便程序在常规 内存 被耗尽时仍可以恢复。 有非常易于使用的标准实现。 池式 内存 的缺点是: 内存 池只适用于操作可以分阶段的程序。 内存 池通常不能与第三方库很好地合作。 如果程序的结构发生变化,则不得不修改 内存 池,这可能 导致 内存 管理系统的重新设计。 您必须记住需要从哪个池进行分配。另 ,如果在这里出错,就很难捕获该 内存 池。 垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常 在当可用 内存 减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理 内存 ,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。 收集器的类型 复制(copying): 这些收集器将 内存 存储器分为两部分,只允许数据驻留在其 一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。 内存 新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另 ,当进行这项复制操作时,所有指针都必须被更新为指向每个 内存 条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。 标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到 内存 时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配 内存 重新使用它们。 增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有 内存 引发问题。增量收集器避免了这些问题。 保守的(Conservative):保守的垃圾收集器在管理 内存 时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的 内存 的指针,那么收集器就将其标记为正在被引用。有时没有被引用的 内存 被收集,这样 引发问题,例如,如果一个整数域 包含一个值,该值是已分配 内存 的地址。不过,这种情况极少发生,而且它只 浪费少量 内存 。保守的收集器的优势是,它们可以与任何编程语言相集成。 Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect- malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc / free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序 所使用的相同的 LD_PRELOAD 技巧,在系统上的几乎任何程序 启用垃圾收集。如果您怀疑某个程序正在泄漏 内存 ,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏 内存 时,很多人在其 使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。 垃圾收集的一些优点: 您永远不必担心 内存 的双重 释放 或者对象的生命周期。 使用某些收集器,您可以使用与常规分配相同的 API。 其缺点包括: 使用大部分收集器时,您都无法干涉何时 释放 内存 。 在多数情况下,垃圾收集比其他形式的 内存 管理更慢。 垃圾收集错误引发的缺陷难于调试。 如果您忘记将不再使用的指针设置为 null,那么仍然 内存 泄漏。 一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其 的一些。为了满足项目的要求,有很多 内存 管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将 有帮助。下表对比了本文 涉及的 内存 管理策略。 表 1. 内存 分配策略的对比 策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 SMP 线程友好 定制分配程序 取决于实现 取决于实现 取决于实现 很难 无 取决于实现 取决于实现 简单分配程序 内存 使用少时较快 很快 差 容易 高 否 否 GNU malloc 容易 高 否 Hoard 容易 高 否 是 引用计数 N/A N/A 非常好 是(取决于 malloc 实现) 取决于实现 池 非常快 极好 是(取决于 malloc 实现) 取决于实现 垃圾收集 (进行收集时慢) 否 几乎不 增量垃圾收集 否 几乎不 增量保守垃圾收集 容易 高 否 几乎不 Hahns Boehm Conservative Garbage Collector 是最流行的开源垃圾收集器,它可以用于常规的 C/C++ 程序。 关于现代操作系统 的虚拟 内存 的文章 Marshall Kirk McKusick 和 Michael J. Karels 合著的 A New Virtual Memory Implementation for Berkeley UNIX 讨论了 BSD 的 VM 系统。 Mel Gorman's Linux VM Documentation 讨论了 Linux VM 系统。 关于 malloc 的文章 Poul-Henning Kamp 撰写的 Malloc in Modern Virtual Memory Environments 讨论的是 malloc 以及它如何与 BSD 虚拟 内存 交互。 Berger、McKinley、Blumofe 和 Wilson 合著的 Hoard -- a Scalable Memory Allocator for Multithreaded Environments 讨论了 Hoard 分配程序的实现。 Marshall Kirk McKusick 和 Michael J. Karels 合著的 Design of a General Purpose Memory Allocator for the 4.3BSD UNIX Kernel 讨论了内核级的分配程序。 Doug Lea 撰写的 A Memory Allocator 给出了一个关于设计和实现分配程序的概述,其 包括设计选择与折衷。 Emery D. Berger 撰写的 Memory Management for High-Performance Applications 讨论的是定制 内存 管理以及它如何影响高性能应用程序。 关于定制分配程序的文章 Doug Lea 撰写的 Some Storage Management Techniques for Container Classes 描述的是为 C++ 类编写定制分配程序。 Berger、Zorn 和 McKinley 合著的 Composing High-Performance Memory Allocators 讨论了如何编写定制分配程序来加快具体工作的速度。 Berger、Zorn 和 McKinley 合著的 Reconsidering Custom Memory Allocation 再次提及了定制分配的主题,看 是否 真正值得为其费心。 关于垃圾收集的文章 Paul R. Wilson 撰写的 Uniprocessor Garbage Collection Techniques 给出了垃圾收集的一个基本概述。 Benjamin Zorn 撰写的 The Measured Cost of Garbage Collection 给出了关于垃圾收集和性能的硬数据(hard data)。 Hans-Juergen Boehm 撰写的 Memory Allocation Myths and Half-Truths 给出了关于垃圾收集的神话(myths)。 Hans-Juergen Boehm 撰写的 Space Efficient Conservative Garbage Collection 是一篇描述他的用于 C/C++ 的垃圾收集器的文章。 Web 上的通用参考资料 内存 管理参考 有很多关于 内存 管理参考资料和技术文章的链接。 关于 内存 管理和 内存 层级的 OOPS Group Papers 是非常好的一组关于此主题的技术文章。 C++ 内存 管理讨论的是为 C++ 编写定制的分配程序。 Programming Alternatives: Memory Management 讨论了程序员进行 内存 管理时的一些选择。 垃圾收集 FAQ 讨论了关于垃圾收集您需要了解的所有内容。 Richard Jones 的 Garbage Collection Bibliography 有指向任何您想要的关于垃圾收集的文章的链接。 Michael Daconta 撰写的 C++ Pointers and Dynamic Memory Management 介绍了关于 内存 管理的很多技术。 Frantisek Franek 撰写的 Memory as a Programming Concept in C and C++ 讨论了有效使用 内存 的技术与工具,并给出了在计算机编程 应当引起注意的 内存 相关错误的角色。 Richard Jones 和 Rafael Lins 合著的 Garbage Collection: Algorithms for Automatic Dynamic Memory Management 描述了当前使用的最常见的垃圾收集算法。 在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.5 节“Dynamic Storage Allocation” ,描述了实现基本的分配程序的一些技术。 在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.3.5 节“Lists and Garbage Collection” ,讨论了用于列表的垃圾收集算法。 Andrei Alexandrescu 撰写的 Modern C++ Design 第 4 章“Small Object Allocation”描述了一个比 C++ 标准分配程序效率高得多的一个高速小对象分配程序。 Andrei Alexandrescu 撰写的 Modern C++ Design 第 7 章“Smart Pointers”描述了在 C++ 智能指针的实现。 Jonathan 撰写的 Programming from the Ground Up 第 8 章“Intermediate Memory Topics” 有本文使用的简单分配程序的一个汇编语言版本。 来自 developerWorks 自我管理数据缓冲区 内存 (developerWorks,2004 年 1 月)略述了一个用于管理 内存 的自管理的抽象数据缓存器的伪 C (pseudo-C)实现。 A framework for the user defined malloc replacement feature (developerWorks,2002 年 2 月)展示了如何利用 AIX 的一个工具,使用自己设计的 内存 系统取代原有的 内存 系统。 掌握 Linux 调试技术 (developerWorks,2002 年 8 月)描述了可以使用调试方法的 4 种不同情形:段错误、 内存 溢出、 内存 泄漏和挂起。 在 处理 Java 程序 内存 漏洞 (developerWorks,2001 年 2 月) ,了解导致 Java 内存 泄漏的原因,以及何时需要考虑它们。 在 developerWorks Linux 专区 ,可以找到更多为 Linux 开发人员准备的参考资料。 从 developerWorks 的 Speed-start your Linux app 专区 ,可以下载运行于 Linux 之上的 IBM 间件产品的免费测试版本,其 包括 WebSphere® Studio Application Developer、WebSphere Application Server、DB2® Universal Database、Tivoli® Access Manager 和 Tivoli Directory Server,查找 how-to 文章和技术支持。 通过参与 developerWorks blogs 加入到 developerWorks 社区。 可以在 Developer Bookstore Linux 专栏 定购 打折出售的 Linux 书籍。 1. 函数 malloc 分配 内存 为了增强程序可读性,有时 函数 malloc 分配 内存 。测试了如下三种方法,容易想到的是第一种。事实证明这种也是错误的! #include &lt;stdio.h&gt; #include &lt;string.h&gt; #include &lt;stdlib.h&gt; typedef struct _dataStc int bu... 2 头文件的作用是什么? 答:一、通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件 的接口声明来调用库功能,而不必关心接口怎么实现的。编译器 从库 提取相应的代码。 二、头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件 的声明不一致,编译器就 指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。 3 内存 的分配方式的分配方式有几种? 答:一、从静态存储区域分配。 内存 在程序编译的时候就已经分配好,这块 内存 在程序的整个运行期间都存在。例如全局变量。 二、在栈上创建。在执行 函数 时, 函数 内局部变量的存储单元都可以在栈上创建, 函数 执行结束时这些存储单元自动被 释放 。栈 内存 分配运算内置于处理器的指令集 ,效率很高,但是分配的 内存 容量有限。 三、从堆上分配,亦称动态 内存 分配。程序在运行的时候用 malloc 或new申请任意多少的 内存 ,程序员自己负责在何时用free或delete 释放 内存 。动态 内存 的生存期由我们决定,使用非常灵活,但问题也最多。 这些因为需要申请 内存 的时候不知道大小,所以只能申请成指针形式的,在uci_getvalue 申请的 内存 ,在 函数 内部没有 释放 ,所以在web_wifi_mode_set调用的时候去 释放 。申请过的 内存 一定要 释放 。用calloc在 函数 内部申请的指针return到 函数 ,然后在另 一个 函数 释放 ,这种操作是可行的。与 malloc 唯一的区别是在于calloc在返回地址之前将申请的空间全部初始化为0。1、用calloc申请的 内存 如果作为返回值的话,可以在 释放 。2、 内存 的申请 malloc 和calloc。........ 动态申请 内存 创建链表,读入一组数据,判断 释放 与不 释放 前后的指针所指空间的数据 是否 一致,一致就是没有 释放 , 不一致表示已被 释放 ,变为垃圾数据。 代码:没有 释放 内存 时 //头文件区 #include<stdio.h> #include<string.h> #include<stdlib.h> #include"func.h" typedef struct ListNode float val; struct ListNode* next; } Node; 1、 函数 原型及说明: void * malloc (long NumBytes):该 函数 分配了NumBytes个字节,并返回了指向这块 内存 的指针。如果分配失败,则返回一个空指针(NULL)。 关于分配失败的原因,应该有多种,比如说空间不足就是一种。 void free(void *FirstByte): 该 函数 是将之前用 malloc 分配的空间还给程序或者是操作系统,也就是 释放 了这块 内存 ,让它重新 malloc 分配的空间,先创建先分配, 内存 地址往后延续 ,free连续清除多个分配空间的时候从 内存 地址大的先清除,连续清除,如果遇到下一个地址比当前清除地址大的话,就跳过去往后判断继续清除。... 1、 函数 原型及说明: void * malloc (long NumBytes):该 函数 分配了NumBytes个字节,并返回了指向这块 内存 的指针。如果分配失败,则返回一个空指针(NULL)。 关于分配失败的原因,应该有多种,比如说空间不足就是一种。 void free(void *FirstByte): 该 函数 是将之前用 malloc 分配的空间还给程序或者是操作系统,也就是 释放 了这块 内存 ,让它重新得到自由。 2、 函数 的用法: 其实这两个 函数 用 1. 函数 malloc 分配 内存 为了增强程序可读性,有时 函数 malloc 分配 内存 。测试了如下三种方法,容易想到的是第一种。事实证明这种也是错误的! #include <stdio.h> #include <string.h> #include <stdlib.h> typedef struct _dataStc int buf[100]; int size; }dataStc; void func1(dataStc* p) p = (dat