glibc 中的暂存缓冲区是什么意思?

What scratch buffer means in glibc?

我发现如果我使用具有严酷模式的 tcmalloc 堆检查程序检查下面的代码会导致堆泄漏,但使用 LSan 未发现泄漏
(我假设 glibc 中的内部分配在 LSan 中被抑制)

#include <string.h>
#include <netdb.h>

int foo() {
    struct addrinfo hints, *res;
    memset(&hints, 0, sizeof hints);

    getaddrinfo("www.example.com", 0, &hints, &res);

    freeaddrinfo(res);
}

int main() {
    foo();
}

我检查了一下,发现 getaddrinfo() 在 glibc 内部使用了 scratch buffer
并怀疑那些暂存缓冲区会导致内存泄漏
(即使它无害)

但遗憾的是没有完整的解释
并且只说 "scratch buffer is variable-sized buffers with on-stack default allocation";;

暂存缓冲区到底有什么作用?

可以参考glibc/include/scratch_buffer.hhere

来自 google-perftools 的自述文件:

In order to catch all heap leaks, tcmalloc must be linked last into your executable. The heap checker may mischaracterize some memory accesses in libraries listed after it on the link line. For instance, it may report these libraries as leaking memory when they're not. (See the source code for more details.)

通常,libc 最后链接。

暂存缓冲区或暂存 space 是一个经常用于预分配内存的术语(因为启动时间通常比运行时性能更重要)用于各种东西。我不知道它在 glibc 中的确切用法,但我只是假设它们需要一个缓冲区来进行内部计算。他们不是即时分配,而是使用预分配的暂存缓冲区。

LSan 支持抑制某些泄漏,但您必须自行检查构建中是否激活了抑制以及哪些抑制处于活动状态。

至于严酷模式:我强烈怀疑暂存缓冲区是在您的 main 函数之前分配的,并在它之后被释放。在这种情况下,HeapChecker 会报告它。不用太担心。

在内部,所有 NSS 接口(其中 getaddrinfo 是一个)看起来像 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 看起来不同,但内部支持实现与 public gethostbyname_r 函数非常相似。

因为使用更大缓冲区重试的习惯用法在 NSS 代码中很常见,所以引入了 struct scratch_buffer。 (以前,有相当折衷的固定缓冲区大小组合,allocaallocamalloc 回退等等。)struct scratch_buffer 结合了固定大小的 on-用于第一个 NSS 调用的堆栈缓冲区。如果 ERANGE 失败,则调用 scratch_buffer_grow,切换到堆缓冲区,并在后续调用中分配更大的堆缓冲区。 scratch_buffer_free 释放堆缓冲区(如果有的话)。

在您的示例中,tcmalloc 报告的泄漏与暂存缓冲区无关。 (我们在 getaddrinfo 中肯定有这样的错误,特别是在模糊的错误路径上,但当前代码应该基本上没问题。)Link 顺序也不是问题,因为显然 tcmalloc 是激活,否则您不会收到任何泄漏报告。

你看到 tcmalloc 泄漏的原因(而不是 valgrind 等其他工具)是因为 tcmalloc 没有调用魔法 __libc_freeres 函数,它是专门添加的用于堆检查器。通常,当进程终止时,glibc 不会释放所有内部分配,因为内核无论如何都会释放该内存。大多数子系统以某种方式在 __libc_freeres 中注册分配。在 getaddrinfo 示例中,我看到以下仍分配的资源:

  • 解析结果/etc/resolv.conf(系统DNS配置)。
  • 解析结果/etc/nsswitch.conf(NSS配置)。
  • 内部 dlopen 调用产生的各种动态加载程序数据结构(用于加载 NSS 服务模块。
  • 录制系统 IPv4/IPv6 支持 getaddrinfo 的缓存。

如果你在 valgrind 下 运行 你的例子,你可以很容易地看到这些分配,使用像这样的命令:

valgrind --leak-check=full --show-reachable=yes --run-libc-freeres=no

关键部分是 --run-libc-freeres=no,它指示 valgrind 而不是 调用 __libc_freeres,它默认调用 __libc_freeres。如果省略此参数,valgrind 将不会报告任何内存泄漏。