【问题标题】:What scratch buffer means in glibc?glibc 中的暂存缓冲区是什么意思?
【发布时间】:2020-07-21 16:57:43
【问题描述】:

我发现如果我使用 tcmalloc 堆检查器以 draconian 模式检查以下代码会导致堆泄漏,但使用 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
并怀疑那些暂存缓冲区会导致内存泄漏
(尽管它没有害处)

但遗憾的是没有完整的解释
并且只说“暂存缓冲区是具有堆栈默认分配的可变大小缓冲区”;;

究竟什么是暂存缓冲区?

你可以参考glibc/include/scratch_buffer.hhere

【问题讨论】:

    标签: c glibc libc tcmalloc leak-sanitizer


    【解决方案1】:

    来自 google-perftools 的自述文件:

    为了捕获所有堆泄漏,tcmalloc 必须链接到 last 你的可执行文件。堆检查器可能会错误地描述某些内存 访问链接行后面列出的库。例如, 当它们不是时,它可能会将这些库报告为内存泄漏。 (看 更多细节请查看源代码。)

    通常,libc 是最后链接的。

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

    LSan 支持抑制一些泄漏,但您必须检查自己是否以及哪些抑制在您的构建中处于活动状态。

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

    【讨论】:

      【解决方案2】:

      在内部,所有 NSS 接口(@98​​7654322@ 是其中之一)看起来像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。 (以前,固定缓冲区大小的组合相当不拘一格,allocaallocamalloc 回退等等。)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 将不会报告任何内存泄漏。

      【讨论】:

      • 感谢您的评论,我学到了很多!但遗憾的是我看不出 --run-libc-freeres=no 和 --run-libc-freeres=no 之间有什么区别,所以我只是鲁莽地开始检查 tcmalloc 和 glibc 源代码......如果你还有更多建议请告诉我,因为我担心需要多少时间才能理解它是如何工作的
      • 不确定你在那里尝试了什么。对我来说,valgrind 如果我通过--run-libc-freeres=no,则报告基本上相同的泄漏。主要优点是它可以自动解析调试符号,产生很好的回溯。
      • 哦,很抱歉让您感到困惑,我的意思是使用 valgrind 检查泄漏,无论是否使用“-run-libc-freeres=no”选项对我来说都没有区别。不过还是非常感谢你,我不想占用你的时间,我会自己进一步检查
      猜你喜欢
      • 2010-10-13
      • 1970-01-01
      • 2013-02-09
      • 2019-01-30
      • 1970-01-01
      • 2021-11-19
      • 2014-06-11
      • 1970-01-01
      • 2014-03-27
      相关资源
      最近更新 更多