首页 > 解决方案 > 为什么 CGO_ENABLE 会对虚拟内存产生如此大的影响?

问题描述

我有一个用 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

解决方案


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

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

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

这种“扭曲”源于 Go 标准库必须与操作系统交互的两个需求:

  1. net软件包需要的 DNS 解析。
  2. 包需要的用户和组查找os

这里的问题有两个:

  • Linux本身(即内核,而不是整个操作系统)不提供任何方法来执行这些任务。

  • 任何典型的类 UNIX 系统,自古以来,都使用称为“NSS”的特殊工具来提供这两项任务,即“名称服务开关”¹。

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

在典型的基于 GNU/Linux 的操作系统上,NSS 是由以下方式实现的 libc(在不太典型的系统上,它可能由单独的共享库提供,但这并没有太大变化)。

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

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

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

如您所见,在默认情况下,

  • libc映射进来,并且
  • 它被初始化并使用一些内存来满足自己的需要——例如明显缓存 NSS 调用返回的数据。

反之,当cgo被禁用的情况下,以上两件事都不会发生。您有更多静态链接的 stdlib 代码,但看起来默认情况仅在整体累积 RSS 使用方面胜过后一种情况。

考虑研究 此查询的输出以 获得更多乐趣;-)


¹ 不要与 Mozilla 的libnss.


推荐阅读