本文是《Go语言调度器源代码情景分析》系列的第13篇,也是第二章的第3小节。
上一节我们分析了调度器的初始化,这一节我们来看程序中的第一个goroutine是如何创建的。
创建main goroutine
接上一节,schedinit完成调度系统初始化后,返回到rt0_go函数中开始调用newproc() 创建一个新的goroutine用于执行mainPC所对应的runtime·main函数,看下面的代码:
runtime/asm_amd64.s : 197
# create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX# entry,mainPC是runtime.main
# newproc的第二个参数入栈,也就是新的goroutine需要执行的函数
PUSHQ AX # AX = &funcval{runtime·main},
# newproc的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,因为runtime.main没有参数,所以这里是0
PUSHQ $0
CALL runtime·newproc(SB) # 创建main goroutine
POPQ AX
POPQ AX
# start this M
CALL runtime·mstart(SB) # 主线程进入调度循环,运行刚刚创建的goroutine
# 上面的mstart永远不应该返回的,如果返回了,一定是代码逻辑有问题,直接abort
CALL runtime·abort(SB)// mstart should never return
RET
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOB Lruntime·mainPC(SB),RODATA,$8
在后面的分析过程中我们会看到这个runtime.main最终会调用我们写的main.main函数,在分析runtime·main之前我们先把重点放在newproc这个函数上。
newproc函数用于创建新的goroutine,它有两个参数,先说第二个参数fn,新创建出来的goroutine将从fn这个函数开始执行,而这个fn函数可能也会有参数,newproc的第一个参数正是fn函数的参数以字节为单位的大小。比如有如下go代码片段:
func start(a, b, c int64) {
......
}
func main() {
go start(1, 2, 3)
}
编译器在编译上面的go语句时,就会把其替换为对newproc函数的调用,编译后的代码逻辑上等同于下面的伪代码
func main() {
push 0x3
push 0x2
push 0x1
runtime.newproc(24, start)
}
编译器编译时首先会用几条指令把start函数需要用到的3个参数压栈,然后调用newproc函数。因为start函数的3个int64类型的参数共占24个字节,所以传递给newproc的第一个参数是24,表示start函数需要24字节大小的参数。
那为什么需要传递fn函数的参数大小给newproc函数呢?原因就在于newproc函数将创建一个新的goroutine来执行fn函数,而这个新创建的goroutine与当前这个goroutine会使用不同的栈,因此就需要在创建goroutine的时候把fn需要用到的参数先从当前goroutine的栈上拷贝到新的goroutine的栈上之后才能让其开始执行,而newproc函数本身并不知道需要拷贝多少数据到新创建的goroutine的栈上去,所以需要用参数的方式指定拷贝多少数据。
了解完这些背景知识之后,下面我们开始分析newproc的代码。newproc函数是对newproc1的一个包装,这里最重要的准备工作有两个,一个是获取fn函数第一个参数的地址(代码中的argp),另一个是使用systemstack函数切换到g0栈,当然,对于我们这个初始化场景来说现在本来就在g0栈,所以不需要切换,然而这个函数是通用的,在用户的goroutine中也会创建goroutine,这时就需要进行栈的切换。
runtime/proc.go : 3232
<