跳到主要内容

Go运行时入口

一 运行时追踪

1.0 使用gdb追踪

使用gdb可以直观的追踪到程序运行信息,使用步骤如下:

# 随便编译一个有main函数的go文件
go build -o main

# 在gdb模式下开始追踪
info files # 会输出 Entry point: 0x44ae10
b *0x44ae10 # 断点 这个文件,此时会输入如下信息
Breakpoint 1 at 0x44ae10: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
info symbol 0x44ae10
_rt0_amd64_linux in section .text
b _rt0_amd64
Breakpoint 2 at 0x44ae10: file /usr/local/go/src/runtime/asm_amd64.s, line 15.
b runtime.rt0_go
Breakpoint 3 at 0x44ae10: file /usr/local/go/src/runtime/asm_amd64.s, line 89.

1.1 运行时入口

Go编译出来的程序包含2个部分:

  • 运行时:入口为runtime包下的asm_amd64.s文件,完成命令行参数、操作系统、调度器初始化工作,然后创建 main goroutine 运行 runtime.main函数。
  • 用户逻辑:以main函数为入口

贴士:asm_amd64.s文件只针对linux64平台,入口文件会依据平台不同而不同,该文件核心代码如下:

CALL    runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)

// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ $0 // arg size
CALL runtime·newproc(SB) // 用于创建 goroutine任务
POPQ AX
POPQ AX

// start this M
CALL runtime·mstart(SB) // 让线程进入任务调度模式

MOVL $0xf1, 0xf1 // crash
RET

从运行时创建main goroutine来看,go程序的整个进程从一开始就以并发模式运行了。

1.2 运行时大致流程

![] (runtime-09.png)

二 运行时初始化

运行时需要对命令行参数、操作系统、调度器初始化等进行初始化。其中最重要的是操作系统和调度器。

本章节只记录系统相关初始化操作,调度器相关位于后文中。

2.1 CPU数量

CPU处理器数量在并发编程中是最重要的指标之一,决定了并行策略、架构设计。

当然现在也有超线程技术(Hyper-Threading),在单个物理核心内虚拟出多个逻辑处理器,类似多线程,将等待时间挖掘出来执行其他任务,以提升整体性能。但是相应的,逻辑处理器之间需要共享一些资源(如缓存刷新),可能也因此拖慢执行效率。

那么Go中的runtime.NumCPU返回的是物理核数量还是包含超线程的结果呢? (答案是后者)

// runtime2.go中的源码
var ncpu int32

// os_linux.go中的源码:
func osinit() {
ncpu = getproccount() // 返回逻辑处理器数量
}

// debug.go中的源码:
func NumCPU() int {
return int(ncpu)
}

2.2 schedinit

我们看看初始化时候做了哪些操作:

// proc.go
func schedinit() {
sched.maxmcount = 10000 // M最大数量限制
stackinit() // 内存相关初始化
malloclinit() // 内存相关初始化
mcommoninit(_g_.m) // M相关初始化
goargs() // 存储命令行参数
goenvs() // 存储环境变量
parsedebugvars() // 解析GODEBUF参数
gcinit() // 初始化gc
sched.lastpoll = uint64(nanotime()) // 初始化poll时间

// 设置 GOMAXPROCS,新版golang默认设置为cpu核心数
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}

完成上述初始化操作后执行:runtime.main函数。

三 逻辑层初始化

上述初始化是针对运行时内核而进行的,并非逻辑层里的init函数,如runtime包里的init函数,标准库、第三方库中的init函数。

逻辑层初始化位于proc.go:

// The main goroutine.
func main() {

// 栈最大值:64位位1G
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}

// 启动后台监控
systemstack(func() {
newm(sysmon, nil)
})

// 执行runtime包内初始化函数
runtime_init()

// 启动时间、启动垃圾回收期
runtimeInitTime = nanotime()
gcenable()

// 执行用户、标准库、第三方库初始化函数
fn := main_init
fn()

// 如果是库,不还行用户入口函数
if isarchive || islibrary {
return
}

// 执行用户入口函数
fn = main_main
fn()

// 退出
exit(0)
}

由上看出,执行了runtime.init,main.init,main.main三个函数。

其中前二者是初始化函数,由编译器动态生成,职能分别为:

  • runtime.init:只负责runtime包的初始化
  • main.init:标准库、第三方库、用户自定义函数在这里初始化

如图所示: ![] (runtime-10.png)