go gmp --- 启动流程源码分析

这里我们已经知道了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启动过程初始化可以通过下面这个流程图来加深理解(忽略了一些不重要环节)。
在这里插入图片描述
接下来我们会对schedinitnewprocmstart进行单独的分析。

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个函数为mcommoninitprocresize

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的主要逻辑就是调用scheduleschedule的特点就是它永远不会返回,我们来看一下它是怎么实现的。

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的调度逻辑进行分析。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值