Golang 的底层引导流程/启动顺序

在Golang中,程序的执行入口为 main() 函数,那么底层又是如何工作的呢? 这个问题的答案我们可以在runtime源码找到。对它的解释主要在 src/runtime/proc.go 文件,下面我们看一下它是如何一步一步开始执行的。go version 1.15.6

在文件头部有一段对 Goroutine scheduler 的介绍,我们先了解一下。

调度器的工作是分发goroutines到工作线程让其运行。一句话指明了调度器的存在意义,就是指挥协调GPM干活。

主要包含三部分
G 指的是 goroutine
M 工作线程,也叫machine
P 处理器(逻辑CPU),执行 Go code 的一种资源。这里的Go code 其实就是 goroutine里的代码。

M必须被指派给P去执行 Go code, 但可以被阻塞或通过P进行系统调用。

设计文档 https://golang.org/s/go11sched

再往下会发现一段注释说明

// src/runtime/proc.go

// The bootstrap sequence is:
//
//	call osinit
//	call schedinit
//	make & queue new G
//	call runtime·mstart
//
// The new G calls runtime·main.

不错,这个说的就是我们今天的重点。共分四大阶段, 在第三阶段创建新G是通过调用 runtime.main() 函数实现的,主要用来创建一个main goroutine,然后再运行 runtime.mstart 并启动m0。整个流程如图所示

runtime.main

M0 是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量 runtime.m0 中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 在之后M0就和其他的M一样了。

osinit

系统初始化, runtime.osinit() 函数,这个文件的位置根据操作系统架构的使用的文件不一样。如果为linux的话,则文件为os_linux.go, windows为 os_windows.go

以下是 os_linux.go 文件里的 osinit 函数原型

func osinit() {
	ncpu = getproccount()
	physHugePageSize = getHugePageSize()
	osArchInit()
}

主要是获取CPU数量页大小操作系统初始化工作。

schedinit

调度器初始化, 调用的函数为schedinit()

func schedinit() {
	lockInit(&sched.lock, lockRankSched)
	lockInit(&sched.sysmonlock, lockRankSysmon)
	lockInit(&sched.deferlock, lockRankDefer)
	lockInit(&sched.sudoglock, lockRankSudog)
	lockInit(&deadlock, lockRankDeadlock)
	lockInit(&paniclk, lockRankPanic)
	lockInit(&allglock, lockRankAllg)
	lockInit(&allpLock, lockRankAllp)
	lockInit(&reflectOffs.lock, lockRankReflectOffs)
	lockInit(&finlock, lockRankFin)
	lockInit(&trace.bufLock, lockRankTraceBuf)
	lockInit(&trace.stringsLock, lockRankTraceStrings)
	lockInit(&trace.lock, lockRankTrace)
	lockInit(&cpuprof.lock, lockRankCpuprof)
	lockInit(&trace.stackTab.lock, lockRankTraceStackTab)

	...
}

全是与锁有关的函数,前四个是和调度器相关,接着两个与panic和deadlock相关。

lockInit(&allglock, lockRankAllg) lockInit(&allpLock, lockRankAllp) 是和G、P有关。
lockInit(&cpuprof.lock, lockRankCpuprof) 与性能分析有关

func schedinit() {
	...

	// raceinit must be the first call to race detector.
	// In particular, it must be done before mallocinit below calls racemapshadow.
	_g_ := getg()
	if raceenabled {
		_g_.racectx, raceprocctx0 = raceinit()
	}

	sched.maxmcount = 10000

	...
}

调用函数 getg() 获取当前的一个G。
如果启用race的话,进行 raceinit() 调用。
设置 m 的最大数量,这里是固定的10000,有必要记住这一点,面试经常问的。

func schedinit() {
	...

	tracebackinit()
	moduledataverify()
	stackinit()
	mallocinit()
	fastrandinit() // must run before mcommoninit
	mcommoninit(_g_.m, -1)
	cpuinit()       // must run before alginit
	alginit()       // maps must not be used before this call
	modulesinit()   // provides activeModules
	typelinksinit() // uses maps, activeModules
	itabsinit()     // uses activeModules

	...
}

stackinit() 栈初始化
mallocinit()
mcommoninit() 对当前m进行初始化,有锁操作(重点了解)
cpuinit() 读取环境变量GODEBUG,并调用 internal/cpu.Initialize
alginit() map使用必须调用,算法相关
typelinksinit() map相关
modulesinit() 是与go module 相关
itabsinit() 与go module 相关

func schedinit() {
	...

	sigsave(&_g_.m.sigmask)
	initSigmask = _g_.m.sigmask

	goargs()
	goenvs()
	parsedebugvars()
	gcinit()

	...
}

gcinit() GC初始化

func schedinit() {
	...

	sched.lastpoll = uint64(nanotime())
	procs := ncpu
	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
		procs = n
	}
	if procresize(procs) != nil {
		throw("unknown runnable goroutine during bootstrap")
	}
	
	...
}

sched.lastpoll 设置调度器初始化轮训时间
procs: 设置当前cpu个数,在 osinit() 函数里已经获取到。如果环境变量设置了CPU个数,直使用设置个数。调用函数procresize() 函数调整cpu 数量,此操作会导致STW,和shched被锁。

func schedinit() {
	...

	// For cgocheck > 1, we turn on the write barrier at all times
	// and check all pointer writes. We can't do this until after
	// procresize because the write barrier needs a P.
	if debug.cgocheck > 1 {
		writeBarrier.cgo = true
		writeBarrier.enabled = true
		for _, p := range allp {
			p.wbBuf.reset()
		}
	}

	if buildVersion == "" {
		// Condition should never trigger. This code just serves
		// to ensure runtime·buildVersion is kept in the resulting binary.
		buildVersion = "unknown"
	}
	if len(modinfo) == 1 {
		// Condition should never trigger. This code just serves
		// to ensure runtime·modinfo is kept in the resulting binary.
		modinfo = ""
	}
}

cgocheck 与cgo 相关,可能会与 writeBarrier 相关,建议了解一下 writeBarrier

总结

这个函数是首个调用的函数,大部分与基本配置有关,如锁、M的最大数量为10000,CPU 个数,GC等等。

make && queue new G

调用函数 newproc ,创建一个新的G,函数原型:

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// 使用一个 siz 字节的参数创建一个 fn 的新 g,将它放在g队列里等待运行
// 编译器将 go 语句转换为对这个函数的调用
//
// The stack layout of this call is unusual: it assumes that the
// arguments to pass to fn are on the stack sequentially immediately
// after &fn. Hence, they are logically part of newproc's argument
// frame, even though they don't appear in its signature (and can't
// because their types differ between call sites).
//
// This must be nosplit because this stack layout means there are
// untyped arguments in newproc's argument frame. Stack copies won't
// be able to adjust them and stack splits won't be able to copy them.
//
//go:nosplit
func newproc(siz int32, fn *funcval) {}

mstart

调用 runtime.mstart() 函数。这个函数是M的入口。函数原型:

// mstart is the entry-point for new Ms.
//
// This must not split the stack because we may not even have stack
// bounds set up yet.
//
// May run during STW (because it doesn't have a P yet), so write
// barriers are not allowed.
//
//go:nosplit
//go:nowritebarrierrec
func mstart() {}

mstart 函数是一个新M的入口。

我们接着看一下函数体。栈是不能拆分的,因为有可能还未设置栈边界。
可运行在 STW 期间(还没有P),所以 写屏障 是不允许的。

//go:nosplit
//go:nowritebarrierrec
func mstart() {
	// 获取一个G(当前为g0)
	_g_ := getg()

	// 检查当前G的边界lo是否等于0,如果等于则初始化系统栈 
	osStack := _g_.stack.lo == 0
	if osStack {
		// Initialize stack bounds from system stack.
		// Cgo may have left stack size in stack.hi.
		// minit may update the stack bounds.
		// 从 system statck 中初始化 _g_.stack 边界
		size := _g_.stack.hi

		// Cgo
		if size == 0 {
			size = 8192 * sys.StackGuardMultiplier
		}

		// 初始化_g_.stack
		_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
		_g_.stack.lo = _g_.stack.hi - size + 1024
	}


	// Initialize stack guard so that we can start calling regular
	// Go code.
	// 初始化 _g_.stackguard0,以便可以运行 go code
	_g_.stackguard0 = _g_.stack.lo + _StackGuard
	// This is the g0, so we can also call go:systemstack
	// functions, which check stackguard1.
	// 这是g0,所以我们也可以调用go:systemstack 函数检查 stackguard1
	_g_.stackguard1 = _g_.stackguard0

	// 启动m
	mstart1()

	// Exit this thread.
	// 退出当前线程
	switch GOOS {
	case "windows", "solaris", "illumos", "plan9", "darwin", "aix":
		// Windows, Solaris, illumos, Darwin, AIX and Plan 9 always system-allocate
		// the stack, but put it in _g_.stack before mstart,
		// so the logic above hasn't set osStack yet.
		osStack = true
	}
	// 重要函数
	mexit(osStack)
}

函数 mstart() 主要工作就是先获取一个Gg0),然后进行一些栈和栈保护的初始化工作,然后再启动M,最后再退出M。这里关注重点是 mstart1()mexit() 函数。

func mstart1() {
	_g_ := getg()

	// 判断当前g是否为g0, 在 mstart() 函数里获取的就是g0,这里再判断一次
	// g0 是m创建的第一个goroutine,与后面创建的普通goroutine不同,g0主要用来实现对普通goroutine的调度
	if _g_ != _g_.m.g0 {
		throw("bad runtime·mstart")
	}

	// Record the caller for use as the top of stack in mcall and
	// for terminating the thread.
	// We're never coming back to mstart1 after we call schedule,
	// so other calls can reuse the current frame.
	// 记录caller用在mcall中栈顶和终止线程
	// 在调用 schedule 后,将不会再返回到 mstart1,所以其它调用可以复用当前 frame
	// 需要关注下 minit() 函数
	save(getcallerpc(), getcallersp())
	asminit()
	minit()

	// Install signal handlers; after minit so that minit can
	// prepare the thread to be able to handle the signals.
	// 安装信息处理器,以便 minit 后,线程可以处理信息
	// 当前g0 是 m0 ,则直接启用 m0, m0是一个全局变量
	if _g_.m == &m0 {
		mstartm0()
	}

	// 当前m0注册有初始化函数
	if fn := _g_.m.mstartfn; fn != nil {
		fn()
	}

	// 当前g0 不是 m0(上面是相等的判断),则从当前绑定的m 里获取一个准备好的P (_g_.m.nextp.ptr())并关联到当前 m 上
	if _g_.m != &m0 {
		acquirep(_g_.m.nextp.ptr())
		_g_.m.nextp = 0
	}

	// 调度  重点!重点!重点!
	schedule()
}

主要相关的函数有 minit()mstartm0()acquirep()schedule()

执行顺序从上到下依次为:

minit() 调用以初始化新的m(包括引导程序m),在新线程上调用,无法分配内存
mstartm0() 此函数实现了仅在 m0上运行的mstart1的一部分
acquirep() 关联一个P到当前的m
schedule() 一轮调度,找到一个runnable状态的goroutine并执行

参考