go程序启动过程
go的启动入口函数
对go有开发经验的朋友都知道,main函数不是真正的启动入口,只是go暴露给用户编写的业务的接口。
这点上基本所有的语言都是类似,在main函数调用前,go需要做一系列的准备工作。
go的启动在 runtime/rto XXX.s, xxx是因为平台的差异。不同系统不同芯片都有自己的启动方法。
go runtime包中,给不同平台的定义的启动函数。.s 是汇编的文件与.asm一样。
以Linux为例,探究启动阶段都做了什么
#include "textflag.h"
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
JMP _rt0_amd64_lib(SB)
// 上边入口函数 调用的函数定义
TEXT _rt0_amd64(SB),NOSPLIT,$-8
// 两个参数存入了寄存器
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
// 跳转到了新的方法
JMP runtime·rt0_go(SB)
关键代码:
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
// Copy arguments forward on an even stack.
// Users of this function jump to it, they don't call it.
MOVL 0(SP), AX
MOVL 4(SP), BX
SUBL $128, SP // plenty of scratch
ANDL $~15, SP
MOVL AX, 120(SP) // save argc, argv away
MOVL BX, 124(SP)
// set default stack bounds.
// _cgo_init may update stackguard.
// 初始化 g0协程
MOVL $runtime·g0(SB), BP
LEAL (-64*1024+104)(SP), BX
MOVL BX, g_stackguard0(BP)
MOVL BX, g_stackguard1(BP)
MOVL BX, (g_stack+stack_lo)(BP)
MOVL SP, (g_stack+stack_hi)(BP)
// 中间删除了一些源码 太多判断
// save m->g0 = g0
MOVL DX, m_g0(AX)
// save g0->m = m0
MOVL AX, g_m(DX)
CALL runtime·emptyfunc(SB) // fault if stack check is wrong
// convention is D is always cleared
CLD
// 运行时检测
CALL runtime·check(SB)
// saved argc, argv
MOVL 120(SP), AX
MOVL AX, 0(SP)
MOVL 124(SP), AX
MOVL AX, 4(SP)
// 拷贝参数到 go程序中
CALL runtime·args(SB)
// 初始化系统相关参数,如cpu是几核
CALL runtime·osinit(SB)
// 初始化调度器
CALL runtime·schedinit(SB)
// 新建一个 goroutine,该 goroutine 绑定 runtime.main,放在 P 的本地队列,等待调度
// create a new goroutine to start program
PUSHL $runtime·mainPC(SB) // entry
// go 创建协程底层都是采用newproc 函数
CALL runtime·newproc(SB)
POPL AX
// 创建一个 M 这里开始真正调用 runtime·main
// start this M
CALL runtime·mstart(SB)
CALL runtime·abort(SB)
RET
DATA runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
到此,系统中有了两个协程goroutine ,一个g0和一个主协程。
往下走看
runtime.main
跳到 proc.go 目录
// 删除了一些代码
// The main goroutine.
func main() {
mp := getg().m
// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
lockOSThread()
// 一些初始化的工作
doInit(runtime_inittasks) // Must be before defer.
// 启动垃圾回收器
gcenable()
// 删除了一些代码,有判断是否为 cgo
/ / Run the initializing tasks. Depending on build mode this
// list can arrive a few different ways, but it will always
// contain the init tasks computed by the linker for all the
// packages in the program (excluding those added at runtime
// by package plugin).
// 用户依赖包的init方法,在这个时候执行了。
for _, m := range activeModules() {
doInit(m.inittasks)
}
// 链接到 main包的main函数,开始调用。
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
if raceenabled {
runExitHooks(0) // run hooks now, since racefini does not return
racefini()
}
// Make racy client program work: if panicking on
// another goroutine at the same time as main returns,
// let the other goroutine finish printing the panic trace.
// Once it does, it will exit. See issues 3934 and 20018.
// 进行panic trace的配置
if runningPanicDefers.Load() != 0 {
// Running deferred functions should not take long.
for c := 0; c < 1000; c++ {
if runningPanicDefers.Load() == 0 {
break
}
Gosched()
}
}
if panicking.Load() != 0 {
gopark(nil, nil, waitReasonPanicWait, traceBlockForever, 1)
}
runExitHooks(0)
}
编译阶段回去link到main函数。
//go:linkname main_main main.main
func main_main()
总结:
前面为了不影响阅读,有些分支的代码,未贴出来,这里来统一总结下:
1. g0是每个go程序的第一个协程,目的是为了调度其他协程
2. 运行时的检测 主要检测内容
// 运行时检测
CALL runtime·check(SB)
•检查各种类型的长度
•检查结构体字段的偏移量
•检查 CAS 操作
•检查指针操作
•检查 atomic 原子操作
•检查栈大小是否是 2的幂次
3.调度器的初始化
// 初始化调度器
CALL runtime·schedinit(SB)
全局栈空间内存分配
堆内存空间的初始化
初始化当前系统线程
加载命令行参数到 os.Args
加载操作系统环境变量
垃圾回收器的参数初始化
算法初始化 (map、hash等)
设置 process 数量
4. 创建了一个主协程
用于执行 `runtime.main`
5. 初始化了一个 M,用来调度主协程
6.主协程在执行主函数包含了这几个工作
执行runtime 包中的init 方法
启动GC 垃圾收集器
执行用户包依赖的 init 方法
执行用户主函数 main.main0
配置了panic trace的收集