0%

《Golang》深入Golang启动过程

地鼠们应该都知道Golang具有运行时,golang启动必然会先启动运行时,然后才是你写的main函数

广义上讲,你写的main函数是入口,但是往深了讲,真正的入口并不在这里

这篇文章我们要探讨下真正的入口

注意 我使用的mac系统,由于go运行时具有很多区分于各个平台的代码,所以一些地方会不一样

定位入口

下面的测试代码

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)


func main() {

fmt.Println("haha")

}

编译它(-gcflags “-N -l”可以禁用优化)

1
go build -gcflags "-N -l" ./bin/test/

动用gdb神器

1
gdb test

接着输入 info files

可以看到入口地址了,我这里是 0x105cbf0

好了,可以退出gdb了,输入 quit

接下来动用神器 delve

1
dlv exec test

下个断点,断在刚才看到的程序入口

b *0x105cbf0

好了,可以看到下面的输出:

1
Breakpoint 1 set at 0x105cbf0 for _rt0_amd64_darwin() /usr/local/go/src/runtime/rt0_darwin_amd64.s:8

那么,入口就在 /usr/local/go/src/runtime/rt0_darwin_amd64.s:8 这里了

入口分析

该打开golang的源码了,我使用goland

src/runtime/rt0_darwin_amd64.s

1
2
TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB) // /usr/local/go/src/runtime/rt0_darwin_amd64.s:8 指向了这里,跳向了另一个函数

找到 _rt0_amd64 这个函数

src/runtime/asm_amd64.s

1
2
3
4
5
6
7
8
// _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 // argument count的缩写,表示传入main函数的参数个数,压入DI寄存器
LEAQ 8(SP), SI // argv // argument vector的缩写,表示传入main函数的参数序列或指针,压入SI寄存器
JMP runtime·rt0_go(SB) // 调用 runtime·rt0_go 函数

找到runtime·rt0_go函数,这个函数比较长

src/runtime/asm_amd64.s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
TEXT runtime·rt0_go(SB),NOSPLIT,$0
// copy arguments forward on an even stack
MOVQ DI, AX // argc // 取出参数个数,放入AX寄存器
MOVQ SI, BX // argv // 取出所有参数,放入BX寄存器
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP
MOVQ AX, 16(SP)
MOVQ BX, 24(SP)

// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI)
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI)

// find out information about the processor we're on
MOVL $0, AX
CPUID
MOVL AX, SI
CMPL AX, $0
JE nocpuinfo

// Figure out how to serialize RDTSC.
// On Intel processors LFENCE is enough. AMD requires MFENCE.
// Don't know about the rest, so let's do MFENCE.
CMPL BX, $0x756E6547 // "Genu"
JNE notintel
CMPL DX, $0x49656E69 // "ineI"
JNE notintel
CMPL CX, $0x6C65746E // "ntel"
JNE notintel
MOVB $1, runtime·isIntel(SB)
MOVB $1, runtime·lfenceBeforeRdtsc(SB)
notintel:

// Load EAX=1 cpuid flags
MOVL $1, AX
CPUID
MOVL AX, runtime·processorVersionInfo(SB)

nocpuinfo:
// if there is an _cgo_init, call it.
MOVQ _cgo_init(SB), AX
TESTQ AX, AX
JZ needtls
// arg 1: g0, already in DI
MOVQ $setg_gcc<>(SB), SI // arg 2: setg_gcc
#ifdef GOOS_android // 如果是安卓
MOVQ $runtime·tls_g(SB), DX // arg 3: &tls_g
// arg 4: TLS base, stored in slot 0 (Android's TLS_SLOT_SELF).
// Compensate for tls_g (+16).
MOVQ -16(TLS), CX
#else
MOVQ $0, DX // arg 3, 4: not used when using platform's TLS
MOVQ $0, CX
#endif
#ifdef GOOS_windows // 如果是windows
// Adjust for the Win64 calling convention.
MOVQ CX, R9 // arg 4
MOVQ DX, R8 // arg 3
MOVQ SI, DX // arg 2
MOVQ DI, CX // arg 1
#endif
CALL AX // 调用 _cgo_init 函数

// update stackguard after _cgo_init
MOVQ $runtime·g0(SB), CX
MOVQ (g_stack+stack_lo)(CX), AX
ADDQ $const__StackGuard, AX
MOVQ AX, g_stackguard0(CX)
MOVQ AX, g_stackguard1(CX)

#ifndef GOOS_windows
JMP ok
#endif
needtls:
#ifdef GOOS_plan9
// skip TLS setup on Plan 9
JMP ok
#endif
#ifdef GOOS_solaris
// skip TLS setup on Solaris
JMP ok
#endif
#ifdef GOOS_illumos
// skip TLS setup on illumos
JMP ok
#endif
#ifdef GOOS_darwin
// skip TLS setup on Darwin
JMP ok
#endif

LEAQ runtime·m0+m_tls(SB), DI // 将m0.tls的地址存入DI寄存器
CALL runtime·settls(SB) // linux中通过arch_prctl系统调用以及ARCH_SET_FS参数设置FS寄存器,这样的话就可以通过TLS访问数据

// store through it, to make sure it works
get_tls(BX) // TLS寄存器内容装载到BX寄存器。方法位于src/runtime/go_tls.h
MOVQ $0x123, g(BX) // 0x123放入TLS的g中
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
CALL runtime·abort(SB)
ok:
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ runtime·g0(SB), CX // runtime·g0变量压入CX
MOVQ CX, g(BX) // g0放入TLS虚拟寄存器中
LEAQ runtime·m0(SB), AX // runtime·m0压入AX

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

CLD // convention is D is always left cleared
CALL runtime·check(SB) // 调用runtime包下的check函数

MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB) // 调用runtime包下的args函数,设置参数
CALL runtime·osinit(SB) // 调用runtime包下的osinit函数,获取cpu个数以及获取页大小
CALL runtime·schedinit(SB) // 调用runtime包下的schedinit函数

// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry mainPC方法(也就是runtime·main函数,是一个全局变量)压入AX寄存器
PUSHQ AX // 压入第二个参数到栈
PUSHQ $0 // arg size 压入第一个参数到栈
CALL runtime·newproc(SB) // 调用 newproc 函数创建一个新的g
POPQ AX
POPQ AX

// start this M
CALL runtime·mstart(SB) // 调用 runtime·mstart函数

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

接下来看看调度初始化函数 schedinit函数 吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
_g_ := getg() // 获取当前g,就是g0
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit() // 开启了race,就初始化race
}

sched.maxmcount = 10000 // 设置允许的最大的m的数量,即线程数量

tracebackinit()
moduledataverify() // 校验go可执行文件以及各个模块格式
stackinit() // 初始化栈池变量
mallocinit() // 向OS申请内存,初始化m的堆
fastrandinit() // must run before mcommoninit 初始化随机种子
mcommoninit(_g_.m) // 初始化m的一些信息
cpuinit() // must run before alginit 初始化cpu信息
alginit() // maps must not be used before this call 算法相关初始化
modulesinit() // provides activeModules 模块初始化
typelinksinit() // uses maps, activeModules 初始化各个模块的typelinks
itabsinit() // uses activeModules 初始化各个模块的itabs

msigsave(_g_.m) // 初始化m的signal mask
initSigmask = _g_.m.sigmask

goargs() // 参数放到argslice变量中
goenvs() // 环境变量放到envs中
parsedebugvars() // 初始化一系列debug相关的变量
gcinit() // 初始化gc

sched.lastpoll = uint64(nanotime()) // 初始化上次netpool执行时间
procs := ncpu // procs设置成cpu个数
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { // 如果GOMAXPROCS有设置,则覆盖procs的值
procs = n
}
if procresize(procs) != nil { // 增加或减少p的实例个数(填procs个p到存放所有p的全局变量allp中),多了就清理多的p,少了就新建p,但是并没有启动m,m启动后会从这里取p并挂钩上
throw("unknown runnable goroutine during bootstrap")
}

// 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 = ""
}
}

m0代表主线程,是第一个启动的m,g0代表0号协程(其实就是m线程的执行函数,其实就是m上的调度器,严格讲不算是g,每个m都有一个g0),直接在m0下运行。

g0创建新的g(函数指定为runtime.main)放到m0的队列中,接着启动m0,m0进入调度

M与P并不是绑死在一起的,M可能随时丢掉它当前的p然后从p列表中取另一个p。可以设置100个p,但是m不一定是100个,可能比p小很多,小多少取决于g多不多,g不多的话,空闲的m就多,m的总数就越少(m的最佳数量是cpu的个数)。m一旦创建就不会销毁,最多进入空闲状态

m是可以在没有附加p的情况下,切换到g0栈执行调度逻辑的。比如m进入系统调用前,p被剥离,系统调用后想重新附加p,发现p已经被其他m抢走了,m就会切到g0进入调度

接下来看 newproc函数 吧

src/runtime/proc.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
func newproc(siz int32, fn *funcval) {  // go关键字的执行函数
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg() // 获取当前goroutine的指针,函数没有相关源码,编译器会进行指令填充
pc := getcallerpc() // 获取伪寄存器PC的内容,函数也是由编译器填充
systemstack(func() {
newproc1(fn, argp, siz, gp, pc)
})
}

func newproc1(fn *funcval, argp unsafe.Pointer, 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")
}
acquirem() // disable preemption because it can be holding p in a local var 独占m
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")
}

_p_ := _g_.m.p.ptr()
newg := gfget(_p_) // 从p的dead g列表中获取一个g对象,没有的话就从全局g列表中抓取一批g对象放入p的的dead g列表中,再从中获取。g在运行结束后会重新放入dead g列表等待重复利用
if newg == nil { // 一开始启动应该取不到
newg = malg(_StackMin) // 新建一个g
casgstatus(newg, _Gidle, _Gdead) // 设置g的状态从idle到dead
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack. // 添加到allg数组,防止gc扫描清除掉
}
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), 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 将goexit设置成g的退出函数(g执行完后会执行这个函数)
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&newg.sched, fn)
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn // 将mainPC方法(就是runtime·main方法)指定为这个协程的启动方法
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels
}
if isSystemGoroutine(newg, false) { // 判断是不是系统协程(g启动函数包含runtime.*前缀的都是系统协程,除了runtime.main, runtime.handleAsyncEvent)
atomic.Xadd(&sched.ngsys, +1)
}
casgstatus(newg, _Gdead, _Grunnable) // 设置g的状态从dead状态到runnable状态

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) // 初始化g的唯一id
_p_.goidcache++
if raceenabled {
newg.racectx = racegostart(callerpc)
}
if trace.enabled {
traceGoCreate(newg, newg.startpc)
}
runqput(_p_, newg, true) // g放入p的本地队列(如果满了会放到全局队列),并将g指定为p的下一个运行的g(为什么指定为下一个运行的g?)
// 如果有空闲的p 且 0个m处于自旋(自旋表示M没有工作可做,正在寻找G来运行)状态(如果有m在自旋,表明它在找工作,就无需唤醒闲置的m或新起m来处理了,在找工作的m会找到这个g) 且 main goroutine(就是runtime.main函数)已经启动,那么唤醒某个m来执行任务
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep() // 唤醒一个m(m被唤醒后又会开始找工作)或者新建一个m(m新建后就会执行mstart函数)
}
releasem(_g_.m) // 放弃独占m
}

这个时候,还并没有创建m对应的系统线程,只是初始化了m的一些内存以及数据,初始化了cpu个数的p的相关数据,启动了一个新的goroutine(函数是runtime·main)

newproc执行完,接下来就是 mstart 了,mstart函数是每个m(即线程)启动后执行的第一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
func mstart() {  // 每个m的启动函数
_g_ := getg()

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.
size := _g_.stack.hi
if size == 0 {
size = 8192 * sys.StackGuardMultiplier
}
_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 = _g_.stack.lo + _StackGuard
// This is the g0, so we can also call go:systemstack
// functions, which check stackguard1.
_g_.stackguard1 = _g_.stackguard0
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)
}

func mstart1() {
_g_ := getg()

if _g_ != _g_.m.g0 { // 判断是不是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.
save(getcallerpc(), getcallersp()) // 保存pc、sp信息到g0
asminit() // asm初始化
minit() // m初始化

// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
if _g_.m == &m0 {
mstartm0() // 启动m0的signal handler
}

if fn := _g_.m.mstartfn; fn != nil {
fn()
}

if _g_.m != &m0 { // 如果不是m0
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}
schedule() // 进入调度。这个函数会阻塞
}

到这里,m0主线程已经开始调度了。这里可以可以联想一下,调度中会将前面新建的一个goroutine运行起来,那么就会执行runtime.main函数,这个我们下一篇再来揭晓。

总结一下启动过程:

  1. 入口:_rt0_amd64_darwin 汇编函数
  2. 初始化m0,g0,并挂钩
  3. runtime·check 检查各个类型占用内存大小的正确性
  4. runtime·args 设置argc、argv参数
  5. runtime·osinit 操作系统相关的init,比如页大小
  6. runtime·schedinit 初始化所有p,初始化其他细节
  7. runtime·newproc 当前m(m0)的p下新建一个g,指定为p的下一个运行的g
  8. runtime·mstart m0启动,接着进入调度,这里阻塞
  9. runtime·abort

每个新启动的g都指定为下一个运行的g

g的可运行队列是一个先进先出的队列,看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"runtime"
"sync"
)

func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("i: ", i)
wg.Done()
}(i)
}
wg.Wait()
}

你以为输出结果是:依次输出0-9吗

其实不是,虽然g的可运行队列是一个先进先出的队列,但是每个新启动的g都指定为下一个运行的g,所以输出结果是:先输出9,然后是0-8依次输出

至于为什么每个新启动的g都指定为下一个运行的g,恐怕需要从开发者那里得到答案了(知道答案的欢迎留下你的答案)

下篇预告

goroutine的调度




微信关注我,及时接收最新技术文章