在内部,所有 NSS 接口(@987654322@ 是其中之一)看起来像gethostbyname_r:
int gethostbyname_r(const char *name,
struct hostent *ret, char *buf, size_t buflen,
struct hostent **result, int *h_errnop);
调用者通过buf 为结果数据提供了一个buflen 字节的缓冲区。如果事实证明此缓冲区的大小不足,则函数将失败并出现ERANGE 错误。调用者应该增长缓冲区(以某种方式重新分配它)并调用函数,其他参数相同。这会重复,直到缓冲区足够大并且函数成功(或函数由于其他原因而失败)。我们如何最终得到这个奇怪的界面是一个较长的故事,但它是我们今天拥有的界面。 getaddrinfo 看起来不同,但内部支持实现与公共 gethostbyname_r 函数非常相似。
因为 retry-with-a-larger-buffer 习惯用法在整个 NSS 代码中如此普遍,所以引入了 struct scratch_buffer。 (以前,固定缓冲区大小的组合相当不拘一格,alloca、alloca 和 malloc 回退等等。)struct scratch_buffer 组合了一个固定大小的堆栈缓冲区,用于第一个NSS 调用。如果ERANGE 失败,则调用scratch_buffer_grow,切换到堆缓冲区,并在后续调用中分配更大的堆缓冲区。 scratch_buffer_free 释放堆缓冲区(如果有)。
在您的示例中,tcmalloc 报告的泄漏与暂存缓冲区无关。 (我们在getaddrinfo 中肯定有这样的错误,特别是在晦涩的错误路径上,但当前代码应该基本没问题。)链接顺序也不是问题,因为显然tcmalloc 是活动的,否则你不会得到任何泄漏报告。
您看到tcmalloc 泄漏的原因(但没有使用其他工具,例如valgrind)是tcmalloc 没有调用魔术__libc_freeres 函数,该函数是专门为堆检查器添加的。通常,当进程终止时,glibc 不会释放所有内部分配,因为内核无论如何都会释放该内存。大多数子系统以某种方式使用__libc_freeres 在那里注册分配。在getaddrinfo 示例中,我看到以下仍然分配的资源:
- 解析
/etc/resolv.conf的结果(系统DNS配置)。
-
/etc/nsswitch.conf 解析结果(NSS 配置)。
- 由内部
dlopen 调用产生的各种动态加载器数据结构(用于加载 NSS 服务模块。
-
getaddrinfo 中记录系统 IPv4/IPv6 支持的缓存。
如果你在 valgrind 下运行你的例子,你可以很容易地看到这些分配,使用如下命令:
valgrind --leak-check=full --show-reachable=yes --run-libc-freeres=no
关键部分是--run-libc-freeres=no,它指示valgrind不调用__libc_freeres,默认情况下它会这样做。如果省略此参数,valgrind 将不会报告任何内存泄漏。