我有一个用 Golang 编写的小守护进程,它在循环中工作并做一些事情。我发现,在使用 CGO_ENABLE=1 或 CGO_ENABLED=0 编译时,守护进程的行为会有所不同。例如,如果 CGO_ENABLE=1(默认值),程序的 VSZ 在短时间内(一小时内)膨胀到 1-2GB。当 CGO_ENABLED=0 时,VSZ 在很长一段时间内(数天)是相同的。看看下面的数字:

CGO_ENABLED=1(守护进程已经运行了 5 分钟)

$ grep -E 'VmSize|VmRSS' /proc/14916/status
VmSize:    1084052 kB
VmRSS:       12524 kB

CGO_ENABLED=0(守护进程已经工作了大约 30 个小时)
$ grep -E 'VmSize|VmRSS' /proc/15160/status
VmSize:    110232 kB
VmRSS:       9756 kB

守护进程不使用依赖于 CGO 的包或函数。其他 Go 编写的程序表现出相同的行为。我知道 VSZ 和 RSS 之间的区别,我很感兴趣这种行为的本质是什么?为什么用 CGO_ENABLED=1 编译的程序要求内核提供这么多内存?

我更喜欢不是“别担心,VSZ 只是一个虚拟内存,实际上它并没有被进程使用”形式的答案。

最佳答案

我可以做出有根据的猜测。

您可能知道,默认情况下,“引用”Go 实现(历史上称为“gc”;可从 the main site 下载)的编译器生成静态链接的二进制文件。这意味着,此类二进制文件仅依赖于操作系统内核提供的所谓“系统调用”,而不依赖于操作系统(或第 3 方)提供的任何共享库。

在基于 Linux 的平台上,这并不完全正确:在默认设置(在 Linux 上为 Linux 构建,即不交叉编译)中,生成的二进制文件实际上与 libc 链接
libpthread (间接地,通过 libc )。

这种“扭曲”来自 Go 标准库与操作系统交互的两个需求:

  • DNS解析,net包需要。
  • 用户和组查找,这是 os 包所需要的。

  • 这里的问题有两个:
  • Linux 本身(即内核,而不是整个操作系统)不提供任何方法来执行这些任务。
  • 任何典型的类 UNIX 系统,因为永远,使用称为“NSS”的特殊工具提供这两个任务,
    这是“名称服务开关”¹。

    NSS 提供了可插拔模块,可以服务于
    作为提供特定类型查询的数据库:DNS、用户/组数据库等(例如“服务”等的知名名称)。一个相当普遍的例子
    用户/组数据库的非标准提供程序是本地的
    联系 LDAP 服务器的服务。

  • 在典型的基于 GNU/Linux 的操作系统上,NSS 由libc(在不太典型的系统上,它可能由
    单独的共享库,但这并没有太大变化)。

    因为 - 再次,通常, - libc 是一个相当稳定的
    就其 API 而言的库(它甚至提供版本化符号
    为了面向 future ),Go 作者正确地决定链接 libc 以导入最小的符号子集(主要是 getaddrinfogetnameinfogetpwnam_r 等)是可以的
    默认情况下完成,因为它在 99% 的情况下都是安全的,
    如果不是,那些必须处理这些案件的人通常
    无论如何都知道该怎么做。

    因此,默认情况下 cgo 已启用并用于使用 NSS 实现这些查找。

    如果 cgo 被禁用,Go 编译器会改为链接自己的
    回退实现,试图模仿什么的一个子集
    完整的 NSS 实现(即解析 /etc/resolv.conf 并使用其中的信息直接查询此处列出的 DNS 服务器;解析 /etc/passwd/etc/group 以服务于用户/组数据库查询)。

    如您所见,在默认情况下,
  • libc 被映射,
  • 被初始化并使用一些内存来满足自己的需要——
    例如明显缓存 NSS 调用返回的数据。

  • 相反,在禁用 cgo 的情况下,则不会发生上述两种情况。您有更多静态链接的 stdlib 代码,但看起来默认情况仅在总体累积 RSS 使用方面胜过后者。

    考虑研究输出
    this query
    为了额外的乐趣;-)

    ¹ 不要与 Mozilla 的 libnss 混淆。

    10-07 17:03