这里我们已经知道了go程序的入口,这篇文章主要是介绍go在启动时是怎么初始化的。先来看一下go启动函数rt0_go()
的汇编代码。
启动流程
//go程序启动时初始化工作
TEXT runtime·rt0_go(SB),NOSPLIT,$0
// 拷贝argc、argv
// copy arguments forward on an even stack
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP //sp16字节对齐
MOVQ AX, 16(SP) //argc放在sp+16处
MOVQ BX, 24(SP) //argv放在sp+24处
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
//初始化g0
//下面这段代码从主线程栈中分出一部分作为g0栈
MOVQ $runtime·g0(SB), DI//g0的地址放入DI寄存器
LEAQ (-64*1024+104)(SP), BX//BX=SP- 64*1024 + 104
MOVQ BX, g_stackguard0(DI)//g0.stackguard0 =SP- 64*1024 + 104
MOVQ BX, g_stackguard1(DI)//g0.stackguard1 =SP- 64*1024 + 104
MOVQ BX, (g_stack+stack_lo)(DI)//g0.stack.lo =SP- 64*1024 + 104
MOVQ SP, (g_stack+stack_hi)(DI)//g0.stack.hi =SP g0栈大小约64kb
// find out information about the processor we're on
#ifdef
...... // xxxx,一些cpu型号检查相关代码先忽略
#endif
//设置线程本地变量thread local storage,完后会有m0.tls[0]=&g0
LEAQ runtime·m0+m_tls(SB), DI//取m0.tls到DI寄存器
CALL runtime·settls(SB)//调用settls
// store through it, to make sure it works
get_tls(BX)
MOVQ $0x123, g(BX)//m0.tls[0]=0x123
MOVQ runtime·m0+m_tls(SB), AX//AX=m0.tls[0]
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//CX=&g0
MOVQ CX, g(BX)//m0.tls[0]=&g0
LEAQ runtime·m0(SB), AX//AX=&m0
//绑定g0和m0
// save m->g0 = g0
MOVQ CX, m_g0(AX)//m0.g0=g0
// save m0 to g0->m
MOVQ AX, g_m(CX)//g0.m=m0
CLD // convention is D is always left cleared
CALL runtime·check(SB)
MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP) //m0放入sp
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP) //g0放入sp+8
CALL runtime·args(SB)//除以操作系统传来的参数和env,忽略
CALL runtime·osinit(SB)//设置全局变量ncpu的值
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,习惯成为main goroutine
POPQ AX
POPQ AX
// start this M
CALL runtime·mstart(SB) //主线程调度上面创建的main goroutine
//mstart永远不会返回,如果返回了则立马abort
CALL runtime·abort(SB) // mstart should never return
RET
go启动过程初始化可以通过下面这个流程图来加深理解(忽略了一些不重要环节)。
接下来我们会对schedinit
、newproc
、mstart
进行单独的分析。
schedinit
先来看schedinit
的源码:
func schedinit() {
...... //环境初始化,先忽略
mcommoninit(_g_.m, -1)//初始化m0,已经有g0.m=m0
...... //堆栈、cpu、gc初始化,先忽略
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {//初始化p
throw("unknown runnable goroutine during bootstrap")
}
...... //不重要代码,先忽略
}
schedinit
功能就是初始化调度器,其中主要的2个函数为mcommoninit
、procresize
。
mcommoninit
mcommoninit
的逻辑是初始化m0,代码如下:
func mcommoninit(mp *m, id int64) {
_g_ := getg()
// g0 stack won't make sense for user (and is not necessary unwindable).
if _g_ != _g_.m.g0 {
callers(1, mp.createstack[:])
}
lock(&sched.lock)
//设置m.id
if id >= 0 {
mp.id = id
} else {
mp.id = mReserveID()
}
//fastrand初始化
mp.fastrand[0] = uint32(int64Hash(uint64(mp.id), fastrandseed))
mp.fastrand[1] = uint32(int64Hash(uint64(cputicks()), ^fastrandseed))
if mp.fastrand[0]|mp.fastrand[1] == 0 {
mp.fastrand[1] = 1
}
mpreinit(mp)//设置用于处理信号的gsignal 栈大小为32kb
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
}
// Add to allm so garbage collector doesn't free g->m
// when it is just in a register or thread-local storage.
mp.alllink = allm
// NumCgoCall() iterates over allm w/o schedlock,
// so we need to publish it safely.
atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))//头插法入allm链表
unlock(&sched.lock)
......
}
mcommoninit
的逻辑就是初始化m0的id、fastrand、gsignal等属性,并将其放入全局变量allm中。
procresize
procresize
的逻辑就是初始化p,代码如下:
func procresize(nprocs int32) *p {
old := gomaxprocs //初始化为0
...... //一些变量检查和赋值,先忽略
//初始化时allp为空
if nprocs > int32(len(allp)) {
// Synchronize with retake, which could be running
// concurrently since it doesn't run on a P.
lock(&allpLock)
if nprocs <= int32(cap(allp)) {
allp = allp[:nprocs]
} else {
//初始化时走这个分支
nallp := make([]*p, nprocs)
// Copy everything up to allp's cap so we
// never lose old allocated Ps.
copy(nallp, allp[:cap(allp)])
allp = nallp
}
unlock(&allpLock)
}
// initialize new P's
// 循环创建p
for i := old; i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
}
pp.init(i)//初始化p,这个p可能是之前的p或者新建的p
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
_g_ := getg()//g0
if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {//初始化时m0.p尚未设置,这里走不到
// continue to use the current P
_g_.m.p.ptr().status = _Prunning
_g_.m.p.ptr().mcache.prepareForSweep()
} else {
...... //初始化p无关代码,先忽略
_g_.m.p = 0
p := allp[0]
p.m = 0
p.status = _Pidle
acquirep(p)//绑定p和m,初始化时把allp[0]绑定到m0
if trace.enabled {
traceGoStart()
}
}
// g.m.p is now set, so we no longer need mcache0 for bootstrapping.
mcache0 = nil
// release resources from unused P's
// 如果p数量减少,则释放多余p
for i := nprocs; i < old; i++ {
p := allp[i]
p.destroy()
// can't free P itself because it can be referenced by an M in syscall
}
// Trim allp.
if int32(len(allp)) != nprocs {
lock(&allpLock)
allp = allp[:nprocs]
unlock(&allpLock)
}
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
p := allp[i]
if _g_.m.p.ptr() == p {
continue //当前g绑定的m.p不会放入空闲p队列
}
p.status = _Pidle
if runqempty(p) {
//本地runq为空就放入空闲p队列
pidleput(p)
} else {
//本地runq不为空就将p绑定当前m,p.m=m
p.m.set(mget())
p.link.set(runnablePs)
runnablePs = p
}
}
stealOrder.reset(uint32(nprocs)) //赋值stealOrder,之后窃取g的时候会用到
var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
return runnablePs
}
procresize
就是循环创建nprocs(默认为cpu核数)个p,并将p放入全局变量allp中,同时将allp[0]和m0关联起来。
newproc
mstart
mstart
作用就是启动一个线程,在这里就是启动m0,让其进入调度循环,先开看看具体代码。
func mstart() {
......
mstart1()
......
}
mstart
就是调用了mstart1
,再来看mstart1
代码。
mstart1
func mstart1() {
...... //保存执行现场及信号相关初始化,这里先忽略
schedule() //进入调度,这函数永远不会返回
}
mstart1
的主要逻辑就是调用schedule
,schedule
的特点就是它永远不会返回,我们来看一下它是怎么实现的。
schedule
//开始一轮调度,永远不会返回
func schedule() {
...... //一些状态检查,先忽略
//查看定时器是否到时间了,time.Tick(),time.Timer{}创建的定时器都会加到定时器堆p.timers中,然后每次调度时都会执行已经到时间的定时器
checkTimers(pp, 0)
var gp *g
...... //一些不重要代码,忽略
if gp == nil {
//为了保证调度公平,每个p每被调用61次就会从全局runq获取g
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1) //全局队列获取g
unlock(&sched.lock)
}
}
if gp == nil {
//从本地队列获取g
gp, inheritTime = runqget(_g_.m.p.ptr())
}
if gp == nil {
//本地队列为空则会从以下地方获取g 全局队列 -> io阻塞的g -> 其他p的本地队列
gp, inheritTime = findrunnable() // blocks until work is available
}
...... //一些不重要代码,忽略
execute(gp, inheritTime)
}
execute
schedule
的逻辑是进入一轮调度,它会尝试找到一个可执行的g并调用execute
运行它,现在我们来看看execute
干了些什么。
func execute(gp *g, inheritTime bool) {
_g_ := getg() //g0
// Assign gp.m before entering _Grunning so running Gs have an
// M.
_g_.m.curg = gp
gp.m = _g_.m //调度的g绑定m0
casgstatus(gp, _Grunnable, _Grunning) //cas改变g状态
gp.waitsince = 0
gp.preempt = false //非抢占
gp.stackguard0 = gp.stack.lo + _StackGuard
//不继承时间片,则调度次数+1
if !inheritTime {
_g_.m.p.ptr().schedtick++
}
...... //trace、profile相关代码,先忽略
gogo(&gp.sched) //真正去执行g,gogo为汇编函数
}
execute
干的事也很少,重要逻辑都在gogo
里面,execute
将g的执行线程sched传给了gogo
,接下来gogo
会从g0栈切换到用户栈并真正执行g。gogo
代码都是汇编,我们来看看它干了些什么。
runtime·gogo
TEXT runtime·gogo(SB), NOSPLIT, $16-8
MOVQ buf+0(FP), BX // gobuf BX=gobud
MOVQ gobuf_g(BX), DX // DX=sched.g
MOVQ 0(DX), CX // make sure g != nil 检查sched.g是否为nil
get_tls(CX)
MOVQ DX, g(CX) //把要运行的g放入线程本地存储tls,方便之后获取
MOVQ gobuf_sp(BX), SP // restore SP 完成栈切换,从g0栈切换到g栈
//以下3条指令为保存执行现场到寄存器
MOVQ gobuf_ret(BX), AX //AX=sched.ret
MOVQ gobuf_ctxt(BX), DX //DX=sched.ctxt
MOVQ gobuf_bp(BX), BP //BP=sched.bp
//清空sched的值,减少gc压力
MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX //BX=sched.pc
JMP BX //跳转到sched.pc开始执行,如果是g0的话就是runtime.main
可以看到,gogo
主要的功能就是从g0栈到要调度的g的栈、保存执行现场到寄存器、清空执行线程、跳转到g.sched.pc(runtime.main
)开始执行。这样,一个go程序就启动了,接下来就是分析runtime.main
具体干了啥了,直接看代码。
runtime.main
//runtime.main
func main() {
g := getg() //被调度的g
......
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
//创建监控线程,该线程不需要关联p,独立于gmp模型
newm(sysmon, nil, -1)
})
}
...... //一些环境初始化,先忽略
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() //调用main.main() 真正执行用户代码
..... // panic检查,先忽略
//系统调用,结束进程
exit(0)
//保护性代码,如果exit()返回下面的代码也会让进程crash掉
for {
var x *int32
*x = 0
}
}
可以看到runtime.main
主要逻辑就是调用main.main
运行用户代码,执行完后调用exit
结束进程。
到现在为止,我们已经知道了一个go程序是怎么启动的以及在启动前go到底做了哪些事情。最后通过一个流程图对上面逻辑做一个总结。
接下来会对gmp的调度逻辑进行分析。