这份文档是为了描述 Subversion 1.2 而编写的。如果您运行的是更新版本的 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 提供的池结构已被证明是一种极其方便、节省时间的功能。