这份文档是为描述 Subversion 1.1 而编写的。如果您正在运行较新版本的 Subversion,我们强烈建议您访问 https://svnbooks.subversion.org.cn/ 并查阅适合您 Subversion 版本的版本。
几乎每个使用过 C 编程语言的开发者都在某个时刻对管理内存使用感到头疼。分配足够的内存供使用,跟踪这些分配,在不再需要时释放内存——这些任务可能相当复杂。当然,如果不能正确执行这些操作,会导致程序崩溃,甚至更糟,导致计算机崩溃。幸运的是,Subversion 依赖于 APR 库以实现可移植性,该库提供了 apr_pool_t 类型,它代表一个池,应用程序可以从中分配内存。
内存池是程序使用的一块内存的抽象表示。与其使用标准的malloc()及其相关函数直接向操作系统请求内存,链接到 APR 的程序只需请求创建一个内存池(使用apr_pool_create()函数)。APR 将从操作系统分配一块中等大小的内存,这块内存将立即可供程序使用。任何时候程序需要一些池内存,它都会使用 APR 池 API 函数之一,例如apr_palloc(),它从池中返回一个通用的内存位置。程序可以不断地从池中请求内存片段,APR 将继续授予这些请求。池将自动增长以适应程序,这些程序请求的内存比原始池包含的内存更多,当然,前提是系统上没有更多可用的内存。
现在,如果这是池故事的结束,它几乎不值得特别关注。幸运的是,事实并非如此。池不仅可以创建,还可以使用以下方法清除和销毁:apr_pool_clear()和apr_pool_destroy()分别。这使开发人员能够从池中分配多个(或数千个)东西,然后使用单个函数调用清理所有这些内存!此外,池具有层次结构。您可以为任何先前创建的池创建“子池”。当您清除一个池时,它的所有子池都会被销毁;如果您销毁一个池,它及其子池都会被销毁。
在我们继续之前,开发人员应该意识到,他们可能不会在 Subversion 源代码中找到对我们刚刚提到的 APR 池函数的太多调用。APR 池提供了一些可扩展机制,例如能够将自定义“用户数据”附加到池,以及用于注册在销毁池时调用的清理函数的机制。Subversion 以一种不太平凡的方式利用了这些扩展。因此,Subversion 提供了(并且大部分代码使用)包装函数svn_pool_create(), svn_pool_clear()和svn_pool_destroy().
虽然池对于基本的内存管理很有帮助,但池结构在循环和递归场景中真正大放异彩。由于循环在其迭代次数上通常是无界的,而递归在其深度上也是无界的,因此这些代码区域的内存消耗可能会变得不可预测。幸运的是,使用嵌套内存池可以成为一种很好的方法,可以轻松地管理这些潜在的棘手情况。以下示例演示了在相当常见的场景中使用嵌套池的基本方法——递归遍历目录树,对树中的每个东西执行一些任务。
示例 8.5 有效的池使用
/* Recursively crawl over DIRECTORY, adding the paths of all its file children to the FILES array, and doing some task to each path encountered. Use POOL for the all temporary allocations, and store the hash paths in the same pool as the hash itself is allocated in. */ static apr_status_t crawl_dir (apr_array_header_t *files, const char *directory, apr_pool_t *pool) { apr_pool_t *hash_pool = files->pool; /* array pool */ apr_pool_t *subpool = svn_pool_create (pool); /* iteration pool */ apr_dir_t *dir; apr_finfo_t finfo; apr_status_t apr_err; apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME; apr_err = apr_dir_open (&dir, directory, pool); if (apr_err) return apr_err; /* Loop over the directory entries, clearing the subpool at the top of each iteration. */ for (apr_err = apr_dir_read (&finfo, flags, dir); apr_err == APR_SUCCESS; apr_err = apr_dir_read (&finfo, flags, dir)) { const char *child_path; /* Clear the per-iteration SUBPOOL. */ svn_pool_clear (subpool); /* Skip entries for "this dir" ('.') and its parent ('..'). */ if (finfo.filetype == APR_DIR) { if (finfo.name[0] == '.' && (finfo.name[1] == '\0' || (finfo.name[1] == '.' && finfo.name[2] == '\0'))) continue; } /* Build CHILD_PATH from DIRECTORY and FINFO.name. */ child_path = svn_path_join (directory, finfo.name, subpool); /* Do some task to this encountered path. */ do_some_task (child_path, subpool); /* Handle subdirectories by recursing into them, passing SUBPOOL as the pool for temporary allocations. */ if (finfo.filetype == APR_DIR) { apr_err = crawl_dir (files, child_path, subpool); if (apr_err) return apr_err; } /* Handle files by adding their paths to the FILES array. */ else if (finfo.filetype == APR_REG) { /* Copy the file's path into the FILES array's pool. */ child_path = apr_pstrdup (hash_pool, child_path); /* Add the path to the array. */ (*((const char **) apr_array_push (files))) = child_path; } } /* Destroy SUBPOOL. */ svn_pool_destroy (subpool); /* Check that the loop exited cleanly. */ if (apr_err) return apr_err; /* Yes, it exited cleanly, so close the dir. */ apr_err = apr_dir_close (dir); if (apr_err) return apr_err; return APR_SUCCESS; }
前面的示例演示了在 循环 和递归情况中有效地使用池。每次递归都通过创建传递给该函数的池的子池来开始。该子池用于循环区域,并在每次迭代时被清除。结果是内存使用大致与递归深度成正比,而不是与作为顶级目录子级的文件和目录总数成正比。当对这个递归函数的第一次调用最终完成时,实际上在传递给它的池中存储的数据很少。现在想象一下,如果这个函数必须alloc()和free()每一个使用的数据片段!
池可能并不适合所有应用程序,但它们在 Subversion 中非常有用。作为 Subversion 开发人员,您需要习惯池以及如何正确使用它们。无论 API 如何,内存使用错误和膨胀都可能难以诊断和修复,但 APR 提供的池结构已被证明是一种非常方便、节省时间的实用功能。