接着上文我们继续

本文需要大量对函数栈帧的理解 直通车:

上文我们进行到了schedinit函数,这是 runtime·rt0_go 函数里的一步

golang 从启动到调度循环建立 (2) - 知乎 - 图1

上面展示了 schedinit 执行完之后 g0 m0 p(…) 都已经初始化完毕 并互相绑定 全局链表allp allm 等也初始化完成。

那接下我们要做什么?正题开始: 启动 main goroutine 正式干活了

main goroutine 来了

  1. TEXT runtime·rt0_go(SB),NOSPLIT,$0
  2. ***
  3. 上个章节代码
  4. ***
  5. //TODO 参数初始化 栈空余的16利用
  6. CALL runtime·args(SB)
  7. //TODO 初始化系统核心数
  8. CALL runtime·osinit(SB)
  9. //TODO 开始初始化调度器
  10. CALL runtime·schedinit(SB)
  11. // create a new goroutine to start program
  12. //mainPC 代表的main函数的地址 main函数保存在AX
  13. MOVQ $runtime·mainPC(SB), AX // entry
  14. //压栈 参数AX
  15. PUSHQ AX
  16. //size main函数入参size main函数没有参数 为0
  17. PUSHQ $0 // arg size
  18. //调用关键函数 newproc 创建main goroutine
  19. CALL runtime·newproc(SB)
  20. POPQ AX
  21. POPQ AX
  22. // start this M
  23. CALL runtime·mstart(SB)
  24. //主循环里调用了 schedule 不会返回 故出错了直接crash
  25. CALL runtime·abort(SB) // mstart should never return
  26. RET
  27. //栈顶指针恢复 **
  28. // Prevent dead-code elimination of debugCallV1, which is
  29. // intended to be called by debuggers.
  30. MOVQ $runtime·debugCallV1(SB), AX
  31. RET
  32. DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
  33. GLOBL runtime·mainPC(SB),RODATA,$8

大概描述了一下过程

  1. 获取 main 函数的地址 并放入 AX 寄存器
  2. 发起函数调用 runtime.newproc 创建 main goroutine
  3. 启动调度循环
  4. 防止循环失败 crash

现在我们开始 newproc 核心函数的解析

  1. /*
  2. newproc用于创建一个新的g 并运行fn函数
  3. 而我们根据函数调用栈分析得知 当前栈在被调用函数的栈帧上
  4. 故新的goroutine需要进行fn函数的参数拷贝,而拷贝就要知道参数大小 所以一个是size 一个是fn代表被调函数
  5. */
  6. //go:nosplit
  7. func newproc(siz int32, fn *funcval) {
  8. //获取fn函数的参数
  9. argp := add(unsafe.Pointer(&fn), sys.PtrSize)
  10. //获取当前g 启动时为g0
  11. gp := getg()
  12. //获取调用者的指令地址 调用newproc 由call指令压栈的返回地址 POPQ AX
  13. pc := getcallerpc()
  14. //切换到g0栈 执行 但是本身就在g0栈 忽略
  15. systemstack(func() {
  16. newproc1(fn, argp, siz, gp, pc)
  17. })
  18. }

主要操作在 newproc1 里

  1. /*
  2. 被调函数,g0栈上的参数(启动过程),size,原协程g,返回地址
  3. */
  4. func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {
  5. //获取当前g 启动时为g0
  6. _g_ := getg()
  7. if fn == nil {
  8. //crash
  9. _g_.m.throwing = -1 // do not dump full stacks
  10. throw("go of nil func value")
  11. }
  12. //内存对齐相关
  13. acquirem() // disable preemption because it can be holding p in a local var
  14. siz := narg
  15. siz = (siz + 7) &^ 7
  16. // We could allocate a larger initial stack if necessary.
  17. // Not worth it: this is almost always an error.
  18. // 4*sizeof(uintreg): extra space added below
  19. // sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
  20. if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
  21. throw("newproc: function arguments too large for new goroutine")
  22. }
  23. _p_ := _g_.m.p.ptr()
  24. //从gfree里看看有没有g p的local
  25. newg := gfget(_p_)
  26. if newg == nil {
  27. //分配新的g 2048b
  28. newg = malg(_StackMin)
  29. //cas控制状态切换 为 dead状态
  30. casgstatus(newg, _Gidle, _Gdead)
  31. //放入全局变量allgs中
  32. allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
  33. }
  34. //栈分配检查
  35. if newg.stack.hi == 0 {
  36. throw("newproc1: newg missing stack")
  37. }
  38. //状态检查
  39. if readgstatus(newg) != _Gdead {
  40. throw("newproc1: new g is not Gdead")
  41. }
  42. //内存对齐
  43. totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
  44. totalSize += -totalSize & (sys.SpAlign - 1) // align to spAlign
  45. sp := newg.stack.hi - totalSize
  46. //确定参数入栈位置
  47. spArg := sp
  48. if usesLR {
  49. // caller's LR
  50. *(*uintptr)(unsafe.Pointer(sp)) = 0
  51. prepGoExitFrame(sp)
  52. spArg += sys.MinFrameSize
  53. }
  54. if narg > 0 {
  55. //从g0拷贝参数到 新的g栈 栈到栈copy
  56. memmove(unsafe.Pointer(spArg), argp, uintptr(narg))
  57. // This is a stack-to-stack copy. If write barriers
  58. // are enabled and the source stack is grey (the
  59. // destination is always black), then perform a
  60. // barrier copy. We do this *after* the memmove
  61. // because the destination stack may have garbage on
  62. // it.
  63. if writeBarrier.needed && !_g_.m.curg.gcscandone {
  64. f := findfunc(fn.fn)
  65. stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
  66. if stkmap.nbit > 0 {
  67. // We're in the prologue, so it's always stack map index 0.
  68. bv := stackmapdata(stkmap, 0)
  69. bulkBarrierBitmap(spArg, spArg, uintptr(bv.n)*sys.PtrSize, 0, bv.bytedata)
  70. }
  71. }
  72. }
  73. //清空newg的sched gobuf 上下文保存相关的
  74. memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
  75. //设置gobuf寄存器相关
  76. newg.sched.sp = sp
  77. newg.stktopsp = sp
  78. //设置调度的执行开始指令 获取goexit的地址后增加1个地址的偏移
  79. newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
  80. newg.sched.g = guintptr(unsafe.Pointer(newg))
  81. gostartcallfn(&newg.sched, fn)

记录下当前状态

golang 从启动到调度循环建立 (2) - 知乎 - 图2

gostartcallfn

  1. //拆解出了包含在 funcval 结构体里的函数指针
  2. //调用gostartcall
  3. func gostartcallfn(gobuf *gobuf, fv *funcval) {
  4. var fn unsafe.Pointer
  5. if fv != nil {
  6. fn = unsafe.Pointer(fv.fn)
  7. } else {
  8. fn = unsafe.Pointer(funcPC(nilfunc))
  9. }
  10. gostartcall(gobuf, fn, unsafe.Pointer(fv))
  11. }

gostartcall

  1. func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
  2. //g的栈顶 现在入栈的只有fn的参数 通过拷贝
  3. sp := buf.sp
  4. if sys.RegSize > sys.PtrSize {
  5. sp -= sys.PtrSize
  6. *(*uintptr)(unsafe.Pointer(sp)) = 0
  7. }
  8. //预留返回地址空间
  9. sp -= sys.PtrSize
  10. //装入goexit函数 等于把goexit函数插入栈顶
  11. *(*uintptr)(unsafe.Pointer(sp)) = buf.pc
  12. buf.sp = sp
  13. buf.pc = uintptr(fn)
  14. buf.ctxt = ctxt
  15. }

上面的gostartcallfn、gostartcall 主要就做了一件事情
把 goexit 函数强行插入 newg 的栈顶,等 newg 结束后方便进行清扫的工作

初始化完 newg 的 sched gobuf

golang 从启动到调度循环建立 (2) - 知乎 - 图3

  1. //修改运行状态为runnable cas修改
  2. casgstatus(newg, _Gdead, _Grunnable)
  3. if _p_.goidcache == _p_.goidcacheend {
  4. // Sched.goidgen is the last allocated id,
  5. // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
  6. // At startup sched.goidgen=0, so main goroutine receives goid=1.
  7. _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
  8. _p_.goidcache -= _GoidCacheBatch - 1
  9. _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
  10. }
  11. //goid ~
  12. newg.goid = int64(_p_.goidcache)
  13. _p_.goidcache++
  14. if raceenabled {
  15. newg.racectx = racegostart(callerpc)
  16. }
  17. if trace.enabled {
  18. traceGoCreate(newg, newg.startpc)
  19. }
  20. //放入本地p的待运行队列
  21. runqput(_p_, newg, true)

修改状态 放入本地运行队列 p

  1. //next为假 把g放到可运行队列的尾部
  2. //next为真 把g放到p.runnext
  3. //满了放到全局g队列
  4. func runqput(_p_ *p, gp *g, next bool) {
  5. if randomizeScheduler && next && fastrand()%2 == 0 {
  6. next = false
  7. }
  8. if next {
  9. retryNext:
  10. oldnext := _p_.runnext
  11. //cas操作是否有其他的并发操作
  12. //修改当前p的runnext 为新的g
  13. if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
  14. goto retryNext
  15. }
  16. if oldnext == 0 {
  17. return
  18. }
  19. //取出旧的g丢到队列尾部
  20. gp = oldnext.ptr()
  21. }
  22. retry:
  23. h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumers
  24. t := _p_.runqtail
  25. //如果p本地队列未满 入队
  26. if t-h < uint32(len(_p_.runq)) {
  27. _p_.runq[t%uint32(len(_p_.runq))].set(gp)
  28. atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumption
  29. return
  30. }
  31. //本地队列慢了,放入全局队列
  32. if runqputslow(_p_, gp, h, t) {
  33. return
  34. }
  35. // the queue is not full, now the put above must succeed
  36. goto retry
  37. }

这里不展开分析 属于调度相关的源码,后面我们再进行专项调度代码的分析

这里我们已经做到了,新创建了一个 goroutine,设置好了 sched 成员的 sp 和 pc 字段,并且将其添加到了 p0 的本地可运行队列,坐等调度器的调度。

golang 从启动到调度循环建立 (2) - 知乎 - 图4

还剩余这个两个主要的 call 了,现在开始准备启动调度循环

mstart(0) 里调用了 mstart1

  1. func mstart1() {
  2. //启动时为g0
  3. _g_ := getg()
  4. if _g_ != _g_.m.g0 {
  5. throw("bad runtime·mstart")
  6. }
  7. // Record the caller for use as the top of stack in mcall and
  8. // for terminating the thread.
  9. // We're never coming back to mstart1 after we call schedule,
  10. // so other calls can reuse the current frame.
  11. //调用schedule之后永远不会返回 所以复用栈帧
  12. //准备调度前保存调度信息到g0.sched
  13. save(getcallerpc(), getcallersp())
  14. asminit()
  15. minit()
  16. // Install signal handlers; after minit so that minit can
  17. // prepare the thread to be able to handle the signals.
  18. if _g_.m == &m0 {
  19. mstartm0()
  20. }
  21. //执行启动函数 g0fn为nil
  22. if fn := _g_.m.mstartfn; fn != nil {
  23. fn()
  24. }
  25. if _g_.m != &m0 {
  26. acquirep(_g_.m.nextp.ptr())
  27. _g_.m.nextp = 0
  28. }
  29. // 进入调度循环。永不返回
  30. schedule()
  31. }

主要做的是协程切换时一些寄存器信息的保存和处理,之后开始调用 schedule 进入调度循环

golang 从启动到调度循环建立 (2) - 知乎 - 图5

简单看下调度循环吧

  1. func schedule() {
  2. //每个m对应的工作线程 启动时为m0的g0
  3. _g_ := getg()
  4. if _g_.m.locks != 0 {
  5. throw("schedule: holding locks")
  6. }
  7. if _g_.m.lockedg != 0 {
  8. stoplockedm()
  9. execute(_g_.m.lockedg.ptr(), false) // Never returns.
  10. }
  11. ...
  12. var gp *g
  13. var inheritTime bool
  14. ...
  15. if gp == nil {
  16. // Check the global runnable queue once in a while to ensure fairness.
  17. // Otherwise two goroutines can completely occupy the local runqueue
  18. // by constantly respawning each other.
  19. //防止全局队列的g被饿死 所以写死61次就要从全局队列中获取
  20. if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
  21. lock(&sched.lock)
  22. gp = globrunqget(_g_.m.p.ptr(), 1)
  23. unlock(&sched.lock)
  24. }
  25. }
  26. //从p的local queue里面获取g
  27. if gp == nil {
  28. gp, inheritTime = runqget(_g_.m.p.ptr())
  29. // We can see gp != nil here even if the M is spinning,
  30. // if checkTimers added a local goroutine via goready.
  31. }
  32. if gp == nil {
  33. //全局队列和本地队列都没有找到 从其他的工作线程进行偷取 偷不到当前工作线程睡眠 block阻塞等待获取到可以运行的g
  34. gp, inheritTime = findrunnable() // blocks until work is available
  35. }
  36. // This thread is going to run a goroutine and is not spinning anymore,
  37. // so if it was marked as spinning we need to reset it now and potentially
  38. // start a new spinning M.
  39. //清空自旋状态
  40. if _g_.m.spinning {
  41. resetspinning()
  42. }
  43. ...
  44. //执行goroutine的被调函数
  45. //切换栈空间回到newG的栈和栈空间去执行
  46. execute(gp, inheritTime)
  47. }

摘抄了部分关键代码块,最后开始执行 execute 函数切换到 newG 开始执行

  1. func execute(gp *g, inheritTime bool) {
  2. //当前获取的是g0
  3. _g_ := getg()
  4. // 关联gp和m
  5. _g_.m.curg = gp
  6. gp.m = _g_.m
  7. //修改运行状态 cas ==> _Grunning
  8. casgstatus(gp, _Grunnable, _Grunning)
  9. gp.waitsince = 0
  10. gp.preempt = false
  11. gp.stackguard0 = gp.stack.lo + _StackGuard
  12. if !inheritTime {
  13. //调度次数+1
  14. _g_.m.p.ptr().schedtick++
  15. }
  16. //核心函数
  17. //完成栈切换
  18. //cpu执行权转让
  19. gogo(&gp.sched)
  20. }

gogo 函数是一个汇编函数 这样可以精确地控制 cpu 寄存器和函数栈帧切换

  1. TEXT runtime·gogo(SB), NOSPLIT, $16-8
  2. //buf = &gp.sched 进入bx
  3. MOVQ buf+0(FP), BX // gobuf
  4. //DX = gp.sched.g
  5. MOVQ gobuf_g(BX), DX
  6. MOVQ 0(DX), CX // make sure g != nil
  7. //把tls保存在CX寄存器上
  8. get_tls(CX)
  9. //gp.sched.g 放到tls0
  10. MOVQ DX, g(CX)
  11. //设置cpu的sp寄存器为 sched.sp 栈切换
  12. MOVQ gobuf_sp(BX), SP // restore SP
  13. //恢复调度上下文
  14. MOVQ gobuf_ret(BX), AX
  15. MOVQ gobuf_ctxt(BX), DX
  16. MOVQ gobuf_bp(BX), BP
  17. //清空原上下文
  18. MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
  19. MOVQ $0, gobuf_ret(BX)
  20. MOVQ $0, gobuf_ctxt(BX)
  21. MOVQ $0, gobuf_bp(BX)
  22. //把sched.pc 指令值放入BX寄存器
  23. MOVQ gobuf_pc(BX), BX
  24. //调到 sched.pc 开始执行
  25. JMP BX

从 g0 栈切换到 newg 栈 并把之前保存好的 sched 插入到对应的寄存器中,清空原来的 sched
减少 gc 的压力,最后 JMP 开始执行 main 函数。

简单用一张图表示下全部过程

golang 从启动到调度循环建立 (2) - 知乎 - 图6
https://zhuanlan.zhihu.com/p/425005455