菜单

Go 运行时

相关源文件

Go Runtime 是为 Go 程序提供基本服务的底层系统。它包括 goroutine 调度、内存管理、垃圾回收、堆栈管理以及其他运行时服务。本页面重点介绍这些核心运行时组件的实现细节,这些组件支持 Go 的并发模型、内存安全和高效执行。

有关 Go 编译器信息,请参阅 Go Compiler

概述

Go Runtime 主要在 src/runtime 包中实现,部分功能由汇编代码提供。与许多作为独立进程运行的语言运行时不同,Go Runtime 直接编译到每个 Go 程序中,使得每个 Go 二进制文件都是自包含的。

Runtime 承担着几项核心职责:

  • 管理 goroutine 及其到 OS 线程的调度
  • 分配和回收内存
  • 垃圾回收
  • 管理堆栈
  • 处理通道等并发原语
  • 运行时诊断和性能分析

运行时架构

来源:src/runtime/proc.go22-115 src/runtime/runtime2.go16-108 src/runtime/mgc.go5-83

Goroutine 调度器

Go 调度器使用三部分模型(称为 G-P-M 模型)将 goroutine 多路复用到操作系统线程。

G-P-M 模型

来源:src/runtime/proc.go22-115 src/runtime/runtime2.go395-507 src/runtime/runtime2.go527-622 src/runtime/runtime2.go636-757

三个主要组成部分是:

  1. G (Goroutine):代表一个 goroutine,这是一个轻量级的执行线程。 g 结构包含:

    • 堆栈边界(stackstackguard0stackguard1
    • 当前状态(atomicstatus)- running、runnable、waiting 等。
    • 调度器信息(sched)- PC、SP 等。
    • 通道等待信息(waiting
    • 等等
  2. P (Processor):代表一个逻辑处理器(执行 Go 代码所需的资源)。 p 结构包含:

    • 一个可运行 goroutine 的队列(runq
    • 内存分配器缓存(mcache
    • 调度器状态(statusschedtick
    • GC 状态和工作队列
  3. M (Machine):代表一个 OS 线程。 m 结构包含:

    • 当前运行的 goroutine 的引用(curg
    • 其分配的 P 的引用(p
    • 线程本地存储(tls
    • 系统调用状态

来源:src/runtime/runtime2.go395-507 src/runtime/runtime2.go527-622 src/runtime/runtime2.go636-757

调度器操作

调度器遵循以下关键原则:

  1. 工作窃取 (Work Stealing):当一个 P 用尽 goroutine 时,它会尝试从其他 P 窃取工作。
  2. 抢占 (Preemption):goroutine 可以在函数调用时或通过定期抢占检查被抢占,以确保公平性。
  3. Syscall 处理:当 goroutine 进行系统调用时,其 M 可能会与其 P 分离,以便其他 goroutine 可以运行。
  4. 无锁操作:许多调度器操作使用原子操作来避免锁定。

调度器在 schedinit() 中初始化,并且主调度循环发生在 schedule() 中,该函数负责选择下一个要运行的 goroutine。

常见的调度器状态转换

操作状态变更描述
创建⟶ _Grunnable创建新的 goroutine 并将其放入运行队列。
调度_Grunnable ⟶ _Grunning选择 goroutine 在线程上运行。
阻塞_Grunning ⟶ _Gwaitinggoroutine 被阻塞(例如,在通道或互斥锁上)。
解除阻塞_Gwaiting ⟶ _Grunnablegoroutine 被解除阻塞并放回运行队列。
系统调用_Grunning ⟶ _Gsyscallgoroutine 进入系统调用。
系统调用返回_Gsyscall ⟶ _Grunninggoroutine 从系统调用返回。

来源:src/runtime/proc.go820-921 src/runtime/runtime2.go16-108 src/runtime/proc.go1063-1083

内存管理

Go 的内存管理系统由多个协同工作的组件组成,以高效地分配和释放内存。

内存布局

来源:src/runtime/malloc.go5-101 src/runtime/mheap.go56-178 src/runtime/mcache.go src/runtime/mcentral.go

关键组件

  1. mheap:中央堆管理器,负责控制所有内存页。

    • 管理堆分配区域(64 位系统上为 64MB 块,32 位系统上为 4MB)
    • 跟踪 span 和 page
    • 管理内存映射和取消映射
  2. mcentral:每个大小类的中央空闲列表。

    • 维护带有空闲对象的 span 列表
    • 充当 mcache 和 mheap 之间的中介
  3. mcache:每个 P 的空闲对象缓存。

    • 为大多数对象提供快速、无锁的分配
    • 当缓存为空时,从 mcentral 补充
  4. mspan:代表一个连续的 page 块。

    • 包含 span 的元数据
    • 跟踪哪些对象已分配和空闲

来源:src/runtime/malloc.go5-101 src/runtime/mheap.go56-178 src/runtime/mcache.go src/runtime/mcentral.go

分配过程

分配路径遵循分层方法:

  1. 小对象分配(≤32KB)

    • 找到适合分配的大小类。
    • 检查本地 P 的 mcache 是否有空闲槽。
    • 如果 mcache 为空,则从 mcentral 获取新的 span。
    • 如果 mcentral 为空,则从 mheap 分配新的 pages。
  2. 大对象分配(>32KB)

    • 直接从 mheap 分配 span。
    • 向上取整到 page 大小。
  3. 微小分配(<16 字节,无指针)

    • 特殊的快速路径,将多个微小分配打包到单个内存块中。

来源:src/runtime/malloc.go5-101 src/runtime/malloc.go112-129

垃圾回收

Go 使用并发的三色标记清除垃圾收集器。

GC 阶段

来源: src/runtime/mgc.go5-83 src/runtime/mgcmark.go src/runtime/mgcsweep.go5-24

GC 周期包含以下主要阶段

  1. 清扫终止:

    • Stop-the-world (STW) 阶段
    • 完成上一周期剩余的清扫工作
  2. 标记阶段:

    • STW 以启用写屏障并为标记做准备
    • 恢复执行
    • 并发根标记(栈、全局变量等)
    • 并发对象标记
    • 写屏障确保不会遗漏对象
  3. 标记终止:

    • STW 以完成标记
    • 记账任务
  4. 清扫阶段:

    • 并发清扫未标记的内存
    • 按分配需求
    • 由后台清扫 goroutine 执行

来源: src/runtime/mgc.go5-83 src/runtime/mgcmark.go16-115 src/runtime/mgcsweep.go5-24

GC 触发和调度

当堆增长达到阈值时触发 GC,该阈值由以下因素控制:

  • GOGC 环境变量(默认为 100%)——触发下一次 GC 的存活堆的百分比。
  • 内存限制(如果通过 GOMEMLIMIT 设置)
  • 通过 runtime.GC() 强制 GC

GC 使用调度算法来平衡

  • CPU 利用率(通常后台标记约为 25%)
  • 内存开销
  • 分配性能

来源: src/runtime/mgc.go112-119 src/runtime/mgcpacer.go14-58

写屏障

在标记阶段,运行时会启用写屏障来捕获可能导致对象遗漏的指针更新。写屏障

  • 由编译器在指针写入时插入
  • 记录旧指针和新指针的值
  • 确保对象不会因并发更新而被遗漏

来源: src/runtime/mgc.go213-232

栈管理

Go 使用分段栈,这些栈可以按需增长和收缩。

栈实现

每个 goroutine 都有自己的栈,初始很小(例如 2KB),但可以增长到允许的最大值(64 位系统上为 1GB)。栈

  • 当栈空间不足时增长(栈溢出检查)
  • 当大部分未使用时可以收缩
  • 在垃圾回收期间会被扫描
  • 在内存中是连续的(不是链接的段)

来源: src/runtime/stack.go16-65 src/runtime/stack.go66-147

栈扫描

在垃圾回收期间,扫描栈以查找指向堆对象的引用

  • 使用编译器生成的栈映射来识别指针
  • 使用解旋信息遍历栈帧
  • 处理运行时中的栈增长/收缩
  • 系统调用和汇编代码的特殊处理

来源: src/runtime/mgcmark.go src/runtime/traceback.go16-37

并发原语

运行时提供了 Go 并发原语的实现。

通道

通道促进 goroutine 之间的安全通信

  • 实现在 src/runtime/chan.go
  • 支持阻塞和非阻塞操作
  • 使用等待的发送者/接收者队列
  • 使用 sudog 结构体来表示等待的 goroutine

来源: src/runtime/chan.go src/runtime/proc.go319-370

计时器

计时器系统负责调度未来的事件

  • 实现在 src/runtime/time.go
  • 每个 P 都有一个计时器堆
  • 网络轮询器用于高效的计时器等待
  • 支持睡眠的 goroutine 和 time.Ticker/time.Timer

来源: src/runtime/time.go46-117 src/runtime/time.go185-207 src/time/sleep.go

同步

运行时实现了各种同步原语

  • 互斥锁和读写互斥锁
  • 信号量(内部使用)
  • Waitgroups
  • Once 对象

这些原语依赖于原子操作和调度器来高效地管理 goroutine 的阻塞和唤醒。

来源: src/runtime/proc.go437-481 src/runtime/proc.go460-481

运行时诊断和跟踪

Go 运行时包含广泛的诊断功能

运行时跟踪

执行跟踪器捕获详细的运行时事件

  • Goroutine 创建/阻塞/解阻塞
  • 系统调用
  • GC 事件
  • 处理器状态
  • 网络事件

跟踪器实现在 src/runtime/trace.go 中,可以通过 runtime/trace 包或使用 go tool trace 命令访问。

来源: src/runtime/trace.go5-18 src/runtime/trace.go28-140

栈跟踪和分析

运行时可以

  • 生成栈跟踪 (runtime.Stack())
  • 支持分析(CPU、内存、阻塞、互斥锁分析)
  • 提供详细的 GC 统计信息
  • 公开运行时指标

来源: src/runtime/traceback.go16-76 src/runtime/export_test.go

初始化和引导

当 Go 程序启动时,运行时会自行引导

  1. runtime.schedinit() 中进行运行时初始化
  2. 调度器初始化和 goroutine 设置
  3. 主 goroutine 的创建
  4. GC 初始化
  5. 后台 goroutine 的启动(GC、清扫器、扫描器)

proc.go 中的 main 函数是入口点,它在调用用户的 main.main() 之前设置运行时。

来源: src/runtime/proc.go146-330 src/runtime/proc.go820-921

与操作系统的交互

运行时通过以下方式与操作系统进行交互:

  • 用于 I/O、内存管理等的系统调用
  • 信号处理
  • 线程管理
  • 网络轮询
  • 特定于操作系统的代码(位于特定平台的文件中)

特定平台的实现保存在单独的文件中,例如 os_linux.goos_windows.go 等。

来源: src/runtime/proc.go332-345 src/runtime/os_linux.go

结论

Go 运行时是一个复杂的系统,为 Go 的并发模型、内存安全和性能提供了基础。理解其架构和组件有助于诊断问题、优化程序,并欣赏语言的设计决策。

有关特定组件的更多详细信息,请参阅相关的维基页面