golang内幕之程序启动流程

文件:startup.go ,代码如下:

package main

import "fmt"

func main() {
	fmt.Println("startup")
}

上面是golang程序的入口函数,即main包中的main函数。但main.main并发golang真正的程序入口,golang存在一个运行时(runtime),与我们的编写的golang代码一起编译、链接成可执行文件。操作系统加载可执行文件运行时,找到真正的entrypoint,开始执行程序指令,程序就已经正在运行了。

问题:golang程序的真正入口(entrypoint)在哪里?

首先,先看看生成的汇编代码,执行编译命令如下:

go tool compile -N -l -S startup.go > startup.s

编译参数(gcflags)简单说明:

-N: 禁止优化代码(disable optimizations),优化后生成的汇编代码并不利于分析代码

-l :禁止内联(disable inlining)

-S:打印输出汇编代码(print assembly listing),这里把输出重定向到startup.s文件

具体编译参数,可参考go compiler source code: src/cmd/compile/internal/gc/main.go文件。

startup.s文件内容如下:

"".main STEXT size=152 args=0x0 locals=0x68
0x0000 00000 (startup.go:5) TEXT    "".main(SB), $104-0
0x0000 00000 (startup.go:5) MOVQ    (TLS), CX
0x0009 00009 (startup.go:5) CMPQ    SP, 16(CX)
0x000d 00013 (startup.go:5) JLS 142
0x000f 00015 (startup.go:5) SUBQ    $104, SP
0x0013 00019 (startup.go:5) MOVQ    BP, 96(SP)
0x0018 00024 (startup.go:5) LEAQ    96(SP), BP
...
0x0069 00105 (startup.go:6) MOVQ    AX, (SP)
0x006d 00109 (startup.go:6) MOVQ    $1, 8(SP)
0x0076 00118 (startup.go:6) MOVQ    $1, 16(SP)
0x007f 00127 (startup.go:6) PCDATA  $0, $1
0x007f 00127 (startup.go:6) CALL    fmt.Println(SB)
0x0084 00132 (startup.go:7) MOVQ    96(SP), BP
0x0089 00137 (startup.go:7) ADDQ    $104, SP
0x008d 00141 (startup.go:7) RET
...
"".init STEXT size=104 args=0x0 locals=0x8
0x0000 00000 (<autogenerated>:1)	TEXT	"".init(SB), $8-0
0x0000 00000 (<autogenerated>:1)	MOVQ	(TLS), CX
0x0009 00009 (<autogenerated>:1)	CMPQ	SP, 16(CX)
...
0x0031 00049 (<autogenerated>:1)	MOVBLZX	"".initdone·(SB), AX
0x0038 00056 (<autogenerated>:1)	CMPB	AL, $1
0x003a 00058 (<autogenerated>:1)	JEQ	62
0x003c 00060 (<autogenerated>:1)	JMP	69
0x003e 00062 (<autogenerated>:1)	PCDATA	$0, $0
0x003e 00062 (<autogenerated>:1)	CALL	runtime.throwinit(SB)
0x0043 00067 (<autogenerated>:1)	UNDEF
0x0045 00069 (<autogenerated>:1)	MOVB	$1, "".initdone·(SB)
0x004c 00076 (<autogenerated>:1)	PCDATA	$0, $0
0x004c 00076 (<autogenerated>:1)	CALL	fmt.init(SB)
0x0051 00081 (<autogenerated>:1)	MOVB	$2, "".initdone·(SB)
...

从上面汇编代码来看,我们只看到main.main和main.init代码,并没有看到任何运行时的代码段。

直接生成可执行文件,命令如下:

go tool objdump startup > ../startup.S

由于汇编代码太多,不方便查看,因此另辟蹊径。

gdb startup
(gdb) info files
Symbols from "/root/code/mq30/src/startup".
Local exec file:
	`/root/code/mq30/src/startup', file type elf64-x86-64.
	Entry point: 0x44f4d0
	0x0000000000401000 - 0x0000000000482178 is .text
	0x0000000000483000 - 0x00000000004c4a7a is .rodata
	0x00000000004c4ba0 - 0x00000000004c56e8 is .typelink
	0x00000000004c56e8 - 0x00000000004c5728 is .itablink
	0x00000000004c5728 - 0x00000000004c5728 is .gosymtab
	0x00000000004c5740 - 0x000000000051368f is .gopclntab
	0x0000000000514000 - 0x0000000000520bdc is .noptrdata
	0x0000000000520be0 - 0x00000000005276f0 is .data
	0x0000000000527700 - 0x0000000000543d88 is .bss
	0x0000000000543da0 - 0x0000000000546438 is .noptrbss
	0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid

从上图可知,入口点Entry Point在0x44f4d0,现在单步进去查看。

(gdb) b *0x44f4d0
Breakpoint 1 at 0x44f4d0: file /usr/local/src/go/src/runtime/rt0_linux_amd64.s, line 8.
(gdb) r
Starting program: /root/code/mq30/src/startup 

Breakpoint 1, _rt0_amd64_linux () at /usr/local/src/go/src/runtime/rt0_linux_amd64.s:8
8		JMP	_rt0_amd64(SB)
(gdb)  

从上图可知,程序入口点在runtime/rt0_linux_amd64.s的代码段_rt0_amd64。

(gdb) n
103		MOVQ	SP, (g_stack+stack_hi)(DI)
(gdb) n
106		MOVL	$0, AX
(gdb) n
107		CPUID
(gdb) info registers
rax            0x0	0
rbx            0x7ffffffee0f8	140737488281848
rcx            0x0	0
rdx            0x0	0
rsi            0x7fffffffe0c8	140737488347336
rdi            0x527f60	5406560
rbp            0x0	0x0
rsp            0x7fffffffe090	0x7fffffffe090
r8             0x0	0
r9             0x0	0
r10            0x0	0
r11            0x0	0
r12            0x0	0
r13            0x0	0
r14            0x0	0
r15            0x0	0
rip            0x44be4b	0x44be4b <runtime.rt0_go+59>
eflags         0x206	[ PF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) 

可以按照上图命令,n和info registers查看调试汇编代码,这里不展开,以后会针对golang汇编深入讨论。

另外,也可使用dlv调试查看,这里不展开,以后会针对dlv深入讨论。

找到入口文件和代码段(函数),就可以直接查看代码,一步步跟踪查看。

为了方便,将会在windows下继续跟踪golang程序是如何启动的。

信息如下:

go版本:go version go1.12.5 windows/amd64

windows版本:windows 10企业版

处理器架构:amd x64

阅读代码IDE:GoLand

文件asm_amd64.s,主要流程代码如下:

// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking. This is the entry point for the program from the
// kernel for an ordinary -buildmode=exe program. The stack holds the
// number of arguments and the C-style argv.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
	MOVQ	0(SP), DI	// argc
	LEAQ	8(SP), SI	// argv
	JMP	runtime·rt0_go(SB)

// main is common startup code for most amd64 systems when using
// external linking. The C startup code will call the symbol "main"
// passing argc and argv in the usual C ABI registers DI and SI.
TEXT main(SB),NOSPLIT,$-8
	JMP	runtime·rt0_go(SB)
TEXT runtime·rt0_go(SB),NOSPLIT,$0
    ...
	// save m->g0 = g0
	MOVQ	CX, m_g0(AX)
	// save m0 to g0->m
	MOVQ	AX, g_m(CX)

    CLD				// convention is D is always left cleared
    CALL	runtime·check(SB)

	MOVL	16(SP), AX		// copy argc
	MOVL	AX, 0(SP)
	MOVQ	24(SP), AX		// copy argv
	MOVQ	AX, 8(SP)
	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)
	POPQ	AX
	POPQ	AX

	// start this M
	CALL	runtime·mstart(SB)

	CALL	runtime·abort(SB)	// mstart should never return
	RET

	// Prevent dead-code elimination of debugCallV1, which is
	// intended to be called by debuggers.
	MOVQ	$runtime·debugCallV1(SB), AX
	RET

此处省略了不少汇编代码,主要与golang栈帧布局,TLS(Thread Local Storage)相关,以后会继续探讨golang汇编、golang栈帧布局。

接下来,会围绕上图中的汇编代码,进一步跟踪golang程序启动流程,主要查看golang程序在启动过程中,都做了哪些工作,才到我们业务程序的入口main.main:

// save m->g0 = g0
MOVQ	CX, m_g0(AX)
// save m0 to g0->m
MOVQ	AX, g_m(CX)

问题:m0,g0在哪里声明的呢?

var (
	m0           m
	g0           g
	raceprocctx0 uintptr
)

在proc.go文件中,我们找到了m0, g0的生命定义,那m和g是什么呢?g就是Goroutine, m就是Machine,对应着系统内核线程,

另外还有p,即Processor,都是golang实现Goroutine协程调用的抽象组件,以后会继续探讨golang协程调度器的实现。

runtime·check(SB)

即在runtime1.go文件定义的args函数:

func check() {
	var (
		a     int8
		b     uint8
		c     int16
		d     uint16
		e     int32
		f     uint32
		g     int64
		h     uint64
		i, i1 float32
		j, j1 float64
		k     unsafe.Pointer
		l     *uint16
		m     [4]byte
	)
	type x1t struct {
		x uint8
	}
	type y1t struct {
		x1 x1t
		y  uint8
	}
	var x1 x1t
	var y1 y1t

	if unsafe.Sizeof(a) != 1 {
		throw("bad a")
	}
	if unsafe.Sizeof(b) != 1 {
		throw("bad b")
	}
	...
}

主要是对一些类型大小进行检查。

CALL	runtime·args(SB)

即在runtime1.go文件定义的check函数:

func args(c int32, v **byte) {
	argc = c
	argv = v
	sysargs(c, v)
}
CALL	runtime·osinit(SB)

即在os_windows.go文件定义的osinit函数:

func osinit() {
	//切换到系统栈调用Windows API
	asmstdcallAddr = unsafe.Pointer(funcPC(asmstdcall))
	//usleep
	usleep2Addr = unsafe.Pointer(funcPC(usleep2))
	//切换到线程地址
	switchtothreadAddr = unsafe.Pointer(funcPC(switchtothread))

	setBadSignalMsg()
	//加载系统调用
	loadOptionalSyscalls()
	//禁止显示windows错误报告窗口
	disableWER()
	//异常回调
	initExceptionHandler()
	//CTRL+C关闭事件回调
	stdcall2(_SetConsoleCtrlHandler, funcPC(ctrlhandler), 1)

	timeBeginPeriodRetValue = osRelax(false)
	//获取CPU逻辑核数
	ncpu = getproccount()
	//获取页大小
	physPageSize = getPageSize()

	// Windows dynamic priority boosting assumes that a process has different types
	// of dedicated threads -- GUI, IO, computational, etc. Go processes use
	// equivalent threads that all do a mix of GUI, IO, computations, etc.
	// In such context dynamic priority boosting does nothing but harm, so we turn it off.
	stdcall2(_SetProcessPriorityBoost, currentProcess, 1)
}

osinit在不同系统下有不同的实现,主要在runtime/os_*.go文件实现。

runtime·schedinit(SB)

即在proc.go文件定义的schedinit函数:

// The bootstrap sequence is:
//
//	call osinit
//	call schedinit
//	make & queue new G
//	call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
	//race detector: 竞态检测器
	// raceinit must be the first call to race detector.
	// In particular, it must be done before mallocinit below calls racemapshadow.
	//获取当前线程的绑定的g,初始化时,只有主线程,返回g0
	_g_ := getg()
	if raceenabled {
		_g_.racectx, raceprocctx0 = raceinit()
	}

	//限制m最大数量10000
	sched.maxmcount = 10000

	//初始栈跟踪
	tracebackinit()
	//校验模块符号表
	moduledataverify()
	//初始化栈空间
	stackinit()
	//初始化栈空间
	mallocinit()
	//初始化m
	mcommoninit(_g_.m)
	//初始化CPU选项
	cpuinit()       // must run before alginit
	//初始化hash
	alginit()       // maps must not be used before this call
	//激活模块
	modulesinit()   // provides activeModules
	typelinksinit() // uses maps, activeModules
	itabsinit()     // uses activeModules

	msigsave(_g_.m)
	initSigmask = _g_.m.sigmask

	//获取命令行参数
	goargs()
	//获取env参数
	goenvs()
	//获取GODEBUG/GOTRACE环境变量参数
	parsedebugvars()
	//初始gc
	gcinit()

	sched.lastpoll = uint64(nanotime())
	//设置最大p数量上限值
	procs := ncpu//默认与系统CPU逻辑核数相关
	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
		procs = n
	}
	//初始化p, 初始状态_Pgcstop
	if procresize(procs) != nil {
		throw("unknown runnable goroutine during bootstrap")
	}

	//写屏障需要P创建后
	// 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"
	}
}

这里是一个大块头,每个大模块的初始化都放在这里进行了,如栈设计、堆内存分配器、gc(垃圾回收器)、协程调度器等,这些都是我们需要进一步探讨的内容。

 

问题:golang是多线程模型还是单线程模型?

//限制m最大数量默认10000,即限制golang程序最大线程数
sched.maxmcount = 10000
//proc.go
func checkmcount() {
	// sched lock is held
	if mcount() > sched.maxmcount {
		print("runtime: program exceeds ", sched.maxmcount, "-thread limit\n")
		throw("thread exhaustion")
	}
}

小总结:golang程序是多线程模型的,而且默认情况下最大线程数不能超过10000。

 

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

此处调用了runtime.newproc函数,并将runtime·mainPC作为runtime.newproc的参数,即回调用函数参入。

runtime.newproc函数在proc.go中定义,那runtime.mainPC对应哪个函数呢?

DATA	runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL	runtime·mainPC(SB),RODATA,$8

从上图可以看出,runtime·mainPC即使runtime.main函数的地址。

问题:runtime.main作为runtime·newproc的参数传入,runtime·newproc是何时调用呢?

先看看runtime·newproc函数的定义:

// 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.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
//go:nosplit
func newproc(siz int32, fn *funcval) {
	println("newproc: create new go routine")
	//创建go routine
	//对routine回调参数的封装
	argp := add(unsafe.Pointer(&fn), sys.PtrSize)
	//获取当前goroutine
	gp := getg()
	//获取程序计数器
	pc := getcallerpc()
	systemstack(func() {
		//使用系统栈创建go routine
		newproc1(fn, (*uint8)(argp), siz, gp, pc)
	})
}
// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
	_g_ := getg()

	if fn == nil {
		_g_.m.throwing = -1 // do not dump full stacks
		throw("go of nil func value")
	}
	_g_.m.locks++ // disable preemption because it can be holding p in a local var
	siz := narg
	siz = (siz + 7) &^ 7

	// We could allocate a larger initial stack if necessary.
	// Not worth it: this is almost always an error.
	// 4*sizeof(uintreg): extra space added below
	// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
	if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
		throw("newproc: function arguments too large for new goroutine")
	}

	//当前goroutine的处理线程m绑定的处理器p
	_p_ := _g_.m.p.ptr()

	//从回收列表中查找goroutine,避免重复创建
	newg := gfget(_p_)
	if newg == nil {
		//回收列表中不存在可用goroutine,则重新分配一个goroutine结构及其栈空间,栈空间使用内核分配
		newg = malg(_StackMin)
		//设置状态为死忙状态
		casgstatus(newg, _Gidle, _Gdead)
		//加到g列表中
		allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
	}
	if newg.stack.hi == 0 {
		throw("newproc1: newg missing stack")
	}

	if readgstatus(newg) != _Gdead {
		throw("newproc1: new g is not Gdead")
	}

	totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
	totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlign
	//栈顶
	sp := newg.stack.hi - totalSize
	spArg := sp
	if usesLR {
		// caller's LR
		*(*uintptr)(unsafe.Pointer(sp)) = 0
		prepGoExitFrame(sp)
		spArg += sys.MinFrameSize
	}
	if narg > 0 {
		memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
		// This is a stack-to-stack copy. If write barriers
		// are enabled and the source stack is grey (the
		// destination is always black), then perform a
		// barrier copy. We do this *after* the memmove
		// because the destination stack may have garbage on
		// it.
		if writeBarrier.needed && !_g_.m.curg.gcscandone {
			f := findfunc(fn.fn)
			stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
			if stkmap.nbit > 0 {
				// We're in the prologue, so it's always stack map index 0.
				bv := stackmapdata(stkmap, 0)
				bulkBarrierBitmap(spArg, spArg, uintptr(bv.n)*sys.PtrSize, 0, bv.bytedata)
			}
		}
	}
	//调度器保存寄存器信息
	memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
	newg.sched.sp = sp
	newg.stktopsp = sp
	newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
	newg.sched.g = guintptr(unsafe.Pointer(newg))
	gostartcallfn(&newg.sched, fn)//存储了回调函数
	newg.gopc = callerpc
	newg.ancestors = saveAncestors(callergp)
	newg.startpc = fn.fn
	if _g_.m.curg != nil {
		newg.labels = _g_.m.curg.labels
	}
	if isSystemGoroutine(newg, false) {
		atomic.Xadd(&sched.ngsys, +1)
	}
	newg.gcscanvalid = false
	casgstatus(newg, _Gdead, _Grunnable)

	if _p_.goidcache == _p_.goidcacheend {
		// Sched.goidgen is the last allocated id,
		// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
		// At startup sched.goidgen=0, so main goroutine receives goid=1.
		_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
		_p_.goidcache -= _GoidCacheBatch - 1
		_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
	}
	newg.goid = int64(_p_.goidcache)
	_p_.goidcache++
	if raceenabled {
		newg.racectx = racegostart(callerpc)
	}
	if trace.enabled {
		traceGoCreate(newg, newg.startpc)
	}

	//将go routine放进p任务队列
	runqput(_p_, newg, true)

	//由空闲的p,直接唤醒新创建的go routine
	if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
		wakep()
	}
	_g_.m.locks--
	if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
		_g_.stackguard0 = stackPreempt
	}
}

从上图可得出小总结:

1、go协程Goroutine有调度状态,栈空间、寄存器sp/pc, 父亲Goroutine, 唯一id(goid) ,入口函数(启动函数)

2、新创建的Goroutine状态为_Grunnable,并放入p的本地队列等待调度

3、golang中go statement入口就是newproc,即go func(){}实际上newproc(func(){})的调用

这里我们不深究golang协程调度器如何实现,这里我们关心,我们传入的回调参数runtime.main是如何给调用的:

这里我直接查看Goroutine执行函数,至于如何调用到这里的,这里不深究:

// Schedules gp to run on the current M.
// If inheritTime is true, gp inherits the remaining time in the
// current time slice. Otherwise, it starts a new time slice.
// Never returns.
//
// Write barriers are allowed because this is called immediately after
// acquiring a P in several places.
//
//go:yeswritebarrierrec
func execute(gp *g, inheritTime bool) {
	_g_ := getg()

	casgstatus(gp, _Grunnable, _Grunning)
	gp.waitsince = 0
	gp.preempt = false
	gp.stackguard0 = gp.stack.lo + _StackGuard
	if !inheritTime {
		_g_.m.p.ptr().schedtick++
	}
	_g_.m.curg = gp
	gp.m = _g_.m

	// Check whether the profiler needs to be turned on or off.
	hz := sched.profilehz
	if _g_.m.profilehz != hz {
		setThreadCPUProfiler(hz)
	}

	if trace.enabled {
		// GoSysExit has to happen when we have a P, but before GoStart.
		// So we emit it here.
		if gp.syscallsp != 0 && gp.sysblocktraced {
			traceGoSysExit(gp.sysexitticks)
		}
		traceGoStart()
	}

	gogo(&gp.sched)
}

关键点在于gogo(&gp.sched),在上面创建Goroutine代码中,已经存储了Goroutine调度需要的上下文sched,并且保存了我们的回调函数:

gostartcallfn(&newg.sched, fn)//存储了回调函数

调用gogo后,就会执行我们的回调函数,即Goroutine开始运行。

假设新创建的Goroutine已经正在运行,即会调用runtime.main,代码如下:

// The main goroutine.
func main() {
	g := getg()

	// Racectx of m0->g0 is used only as the parent of the main goroutine.
	// It must not be used for anything else.
	g.m.g0.racectx = 0

	// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
	// Using decimal instead of binary GB and MB because
	// they look nicer in the stack overflow failure message.
	if sys.PtrSize == 8 {
		maxstacksize = 1000000000
	} else {
		maxstacksize = 250000000
	}

	// Allow newproc to start new Ms.
	mainStarted = true

	if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
		systemstack(func() {
			newm(sysmon, nil)
		})
	}

	// 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()

	if g.m != &m0 {
		throw("runtime.main not on m0")
	}

	runtime_init() // must be before defer
	if nanotime() == 0 {
		throw("nanotime returning zero")
	}

	// Defer unlock so that runtime.Goexit during init does the unlock too.
	needUnlock := true
	defer func() {
		if needUnlock {
			unlockOSThread()
		}
	}()

	// Record when the world started.
	runtimeInitTime = nanotime()

	gcenable()

	main_init_done = make(chan bool)
	if iscgo {
		if _cgo_thread_start == nil {
			throw("_cgo_thread_start missing")
		}
		if GOOS != "windows" {
			if _cgo_setenv == nil {
				throw("_cgo_setenv missing")
			}
			if _cgo_unsetenv == nil {
				throw("_cgo_unsetenv missing")
			}
		}
		if _cgo_notify_runtime_init_done == nil {
			throw("_cgo_notify_runtime_init_done missing")
		}
		// Start the template thread in case we enter Go from
		// a C-created thread and need to create a new thread.
		startTemplateThread()
		cgocall(_cgo_notify_runtime_init_done, nil)
	}

	fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
	fn()
	close(main_init_done)

	needUnlock = false
	unlockOSThread()

	if isarchive || islibrary {
		// A program compiled with -buildmode=c-archive or c-shared
		// has a main, but it is not executed.
		return
	}
	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 {
		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.
	if atomic.Load(&runningPanicDefers) != 0 {
		// Running deferred functions should not take long.
		for c := 0; c < 1000; c++ {
			if atomic.Load(&runningPanicDefers) == 0 {
				break
			}
			Gosched()
		}
	}
	if atomic.Load(&panicking) != 0 {
		gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
	}

	exit(0)
	for {
		var x *int32
		*x = 0
	}
}

也就是我们新创建的Goroutine, 有几个关键点如下:

main_init

main_main

newm(sysmon, nil)

exit(0)

看到main_init,main_main是否似曾相似,没错,这就是我们业务程序的入口main.init,main.main,这称为main routine。

恭喜,我们终于整个启动流程走了一遍。

问题:main.main没有 for {time.Sleep(1000)}等阻塞操作,是否会直接退出应用?

会,看exit(0),退出了整个应用。

newm(sysmon, nil)顺便提一下,new一个m,即创建了一个新系统内核线程,这也可以佐证golang是多线程模型的。

可以看到单独开辟了一个线程用于gc抢占式和epoll相关代码,这里不深究。

刚刚,我们是假设main routine已经运行了,其实是否可运行依赖于可用的m和可用的p。

还有剩余的汇编代码,就可以解释这块:

// start this M
CALL	runtime·mstart(SB)

CALL	runtime·abort(SB)	// mstart should never return
RET
// Called to start an M.
//
// 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() {
	_g_ := getg()
    ...
	mstart1()
    ...
	mexit(osStack)
}
func mstart1() {
	_g_ := getg()
    ...
	save(getcallerpc(), getcallersp())
	asminit()
	minit()
	...
	schedule()
}
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {

	_g_ := getg()
	...
	execute(gp, inheritTime)
}

这是一个用不返回的函数,类似for { func1... },这也是为什么程序没退出,Gorourine一直得到调度的原因:一直没退出,一直会调度可运行的Goroutine。

总结:探讨了从runtime入口到应用入口main.main的流程追踪,追踪过程中,给出了一些小总结,但也留下了很多待进一步探讨的主题,如golang汇编,golang栈帧设计,golang堆内存分配,golang协程调度,gc垃圾回收器等,这里不可能面面俱到,毕竟探讨的是golang程序的一生,只能抓住主要流程,细节仍需继续探讨。

带着问题去学习,总是很有效率的,现在遗留的问题就是我们进步的目标,加油!

下一步,将会继续探讨go协程调度。

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值