跳转至

分析 Linux 上 Go 1.27 程序的内存映射

文章背景与核心概要

本文基于 Ubuntu 26.04 LTS(64位 x86 架构)与 Go 1.27rc2 环境,深入探讨了一个极简 Go 程序的 Linux 操作系统内存使用情况。得益于 Go 1.25 中引入的 decoratemappings 特性,开发者现在可以通过查看 /proc/<pid>/maps 文件清晰识别和分类 Go 进程的虚拟内存空间,包括机器代码段、Go 随机化堆、内部元数据、页面分配器以及内核映射区。


概要

Summary

本文探讨了运行在 Go 1.27rc2(64位 x86 Ubuntu 26.04 LTS 服务器)上的极简 Go 程序的 Linux 操作系统内存使用情况。利用现代 Go 运行时特性——特别是在 Go 1.25 中引入的 decoratemappings 功能——我们可以查看 /proc/<pid>/maps,清晰地对虚拟内存分配进行分类,范围涵盖机器代码、随机化 Go 堆、内部元数据、页面分配器以及内核映射。

This article explores the Linux OS memory usage of a minimalist Go program running on Go 1.27rc2 (on a 64-bit x86 Ubuntu 26.04 LTS server). Utilizing modern Go runtime features—specifically the decoratemappings feature introduced in Go 1.25—we can inspect /proc/<pid>/maps to clearly categorize virtual memory allocations, ranging from machine code and the randomized Go heap to internal metadata, page allocators, and kernel mappings.


1. 引言

1. Introduction

多年前,有人曾发表过关于 简单 Go 1.11 程序的 Linux 操作系统内存使用情况 的深入分析。在 Go 1.27rc2 中重新探讨这一主题时,Go 现在能够向我们展示关于其内存使用的更多信息。

Many years ago, a deep dive was published regarding the Linux OS memory use of a simple Go 1.11 program. Returning to this topic in Go 1.27rc2, Go can now tell us much more information about its memory usage.

使用下面这个非常简单的程序:

Using this very simple program:

package main

func main() {
    var i uint64
    for {
        i++
    }
}

请确保在 go.mod 文件中设置了 go 1.25 或更高版本。在合适的 Linux 发行版上运行此程序会生成一个信息丰富的 /proc/<pid>/maps 文件,可以将其按区域划分并重新格式化,以显示具体大小而非起止地址范围。

Ensure you set go 1.25 or higher in your go.mod file. Running this on a suitable Linux distribution yields an informative /proc/<pid>/maps file, which can be broken into sections and reformatted to show sizes instead of start–end ranges.


2. 程序代码与静态数据

2. Program Code and Static Data

首先,我们检查程序的机器代码、只读数据、已初始化的变量以及零初始化数据 (bss) 空间:

First, we examine the program's machine code, read-only data, initialized variables, and zeroed data (bss) space:

00400000          504K r-xp   memdemo
0047e000          656K r--p   memdemo
00522000           32K rw-p   memdemo
0052a000          212K rw-p 

该区块是连续的,覆盖从 0x004000000x0055f000 的范围。 * r, w, x:读取、写入和代码执行权限。 * p:写时复制(私有)映射。

This block is contiguous, covering 0x00400000 to 0x0055f000. * r, w, x: Read, write, and code execution permissions. * p: Copy-on-write (private) mapping.

(与 2018 年的 Go 1.11 版本 相比,程序本身变大了,这可能是由于各种隐式拉入的运行时包有所扩展。)

(Comparing this to the Go 1.11 version from 2018, the program itself has grown, likely due to expansions in various implicitly pulled-in runtime packages.)


3. Go 堆内存

3. The Go Heap

接下来是主要的 Go 堆及其初始 64 MByte arena。该程序实际上只需要其中的 4 MByte 块进行实际分配,其余部分保持为保留空间:

Next is the main Go heap with its initial 64 MByte arena. This program only required a 4 MByte chunk out of it for actual allocation, keeping the rest as reserved space:

d75e4000000     12288K ---p  [anon: Go: heap reservation]
d75e4c00000      4096K rw-p  [anon: Go: heap]
d75e5000000     49152K ---p  [anon: Go: heap reservation]

该连续区块从 0xd75e4000000 延伸至 0xd75e8000000

This contiguous block runs from 0xd75e4000000 through 0xd75e8000000.

Go 堆的位置过去是固定的,但自 Go 1.26 以来,在 64 位平台上随机化堆基地址已成为默认行为。这仍然是一个持续中的 Go 实验特性来源),在 internal/goexperiment 中被追踪为 RandomizedHeapBase64

The Go heap's location used to be fixed, but since Go 1.26, randomizing the heap base address is the default on 64-bit platforms. This remains a lingering Go experiment (via) tracked as RandomizedHeapBase64 in internal/goexperiment.


4. Go 内部内存分配

4. Go Internal Allocations

接下来是 Go 内部分配的各种组合,连续堆叠在一起:

Next are an assortment of Go internal allocations, stacked consecutively:

7b3b11460000      256K rw-p  [anon: Go: immortal metadata]
7b3b114a0000     1408K rw-p  [anon: Go: profiler hash buckets]
7b3b11600000    32768K rw-p  [anon: Go: heap index]
7b3b13600000   289708K ---p  [anon: Go: scavenge index]
7b3b250eb000        4K rw-p  [anon: Go: scavenge index]
7b3b250ec000   234576K ---p  [anon: Go: scavenge index]
7b3b33600000   289708K ---p  [anon: Go: page summary]
7b3b450eb000        4K rw-p  [anon: Go: page alloc]
7b3b450ec000   270788K ---p  [anon: Go: page summary]
7b3b5595d000        4K rw-p  [anon: Go: page alloc]
7b3b5595e000    33844K ---p  [anon: Go: page summary]
7b3b57a6b000        4K rw-p  [anon: Go: page alloc]
7b3b57a6c000     3664K ---p  [anon: Go: page summary]
7b3b57e1c000      512K rw-p  [anon: Go: immortal metadata]
7b3b57e9c000       64K rw-p  [anon: Go: gc bits]
7b3b57eac000       64K rw-p  [anon: Go: allspans array]
7b3b57ebc000     1024K rw-p  [anon: Go: page alloc index]
7b3b57fbc000       72K rw-p  [anon: Go: immortal metadata]
7b3b57fce000      564K ---p  [anon: Go: page summary]
7b3b5805b000        4K rw-p  [anon: Go: page alloc]
7b3b5805c000      456K ---p  [anon: Go: page summary]
7b3b580ce000      128K rw-p  [anon: Go: page alloc]
7b3b580ee000      256K rw-p  [anon: Go: immortal metadata]

该区域从 0x7b3b11460000 延伸至 0x7b3b5812e000,包含略多于 1 GB 的地址空间(约 1,159,992 KB),不过最大的部分是无法访问的(带有 ---p 标记,没有任何读、写或执行权限)。

This region runs from 0x7b3b11460000 through 0x7b3b5812e000, encompassing just over 1 GB of address space (~1,159,992 KB), though the largest portions are inaccessible (---p with no read, write, or execution permissions).

虽然这比 2018 年的 Go 1.11 保留了多得多的地址空间,但实际分配的可访问内存仅约 36,572 KB。在 某些场景下,保留地址空间与物理内存分配之间的这种区别至关重要。

While this reserves significantly more address space than Go 1.11 in 2018, only about 36,572 KB is actually allocated as accessible memory. This distinction between reserved address space and physical memory allocation is critical in certain scenarios.


5. 内核、栈与传统映射

5. Kernel, Stack, and Legacy Mappings

在 Go 特定的段之后,我们能找到系统层面的分配:

Following the Go-specific segments, we find system-level allocations:

VDSO 支持

VDSO Support

由内核映射到每个地址空间中(从 0x7b3b5812e0000x7b3b58136000):

Mapped by the kernel into every address space (from 0x7b3b5812e000 to 0x7b3b58136000):

7b3b5812e000       16K r--p  [vvar]
7b3b58132000        8K r--p  [vvar_vclock]
7b3b58134000        8K r-xp  [vdso]

(参见 vdso(7)Kernel Internals VDSO

(See vdso(7) and Kernel Internals VDSO)

进程栈

Process Stack

在 Go 运行时初始化之前,由内核在 exec() 运行进程时分配:

Allocated by the kernel when exec() runs the process, prior to Go runtime initialization:

7ffc60b8f000      136K rw-p  [stack]

Vsyscall 区域

Vsyscall Area

一种传统的机制(LWN 文章Kernel Internals):

A legacy mechanism (LWN Article, Kernel Internals):

ffffffffff600000    4K --xp  [vsyscall]

6. 启用映射装饰器

6. Enabling Mapping Decorators

在支持的 Linux 系统上,Go 通过 runtime/set_vma_name_linux.go 命名这些内存区域。此行为由 $GODEBUG 中的 decoratemappings 选项控制,该选项在 Go 1.25 及更高版本中默认启用。

On supported Linux systems, Go names these memory regions via runtime/set_vma_name_linux.go. This behavior is controlled by the decoratemappings option in $GODEBUG, which defaults to enabled in Go 1.25 and later.

映射名称的要求:

Requirements for Mapping Names:

  1. Go 版本: 使用 Go 1.25 或更高版本构建,且在 go.mod 中指定了 go 1.25(或更高)。如果指定了旧版本,Go 默认会使用 decoratemappings=0(你也可以在运行时显式传递 GODEBUG=decoratemappings=1)。
  1. Go Version: Built with Go 1.25 or later, and go 1.25 (or higher) specified in go.mod. If an older version is specified, Go defaults to decoratemappings=0. (You can also explicitly pass GODEBUG=decoratemappings=1 at runtime).
  1. Linux 内核支持: 内核必须启用了 CONFIG_ANON_VMA_NAME 配置选项。
  2. Ubuntu: 至少自 22.04 LTS 起已默认启用。
  3. Fedora: 默认禁用。
  1. Linux Kernel Support: The kernel must have the CONFIG_ANON_VMA_NAME configuration option enabled.
  2. Ubuntu: Enabled since at least 22.04 LTS.
  3. Fedora: Disabled by default.

(注意:禁用映射名称允许内核将共享权限但具有不同名称的相邻虚拟内存区域进行合并——例如,将 0x7b3b11460000 处的前两个内存区域合并为一个单区块)。

(Note: Disabling mapping names allows the kernel to merge adjacent virtual memory areas that share permissions but have different names—for instance, collapsing the first two memory regions at 0x7b3b11460000 into a single block).