• 本篇是接着上一篇《go关键字》的后续,goroutine运行完后的回收阶段。

goexit()

  1. goroutine运行结束后返回到goexit+PCQuantum处。const PCQuantum = 1
  2. 也就是接着执行CALL runtime·goexit1(SB)这条指令。
  3. 文件位置:go1.19.3/src/runtime/asm_amd64.s
1591
1592
1593
1594
1595
1596
1597
1598
// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
    BYTE    $0x90               // NOP 从下一条指令开始执行
    CALL    runtime·goexit1(SB) // 这条指令调用函数将永不返回
    // traceback from goexit1 must hit code range of goexit
    // 从goexit1回溯必须达到goexit的代码范围
    BYTE    $0x90               // NOP

goexit1()

  1. goexit1()函数通过调用mcall从当前运行的g2 goroutine切换到g0,然后在g0栈上调用和执行goexit0()这个函数。
  2. 文件位置:go1.19.3/src/runtime/proc.go
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
// Finishes execution of the current goroutine.
func goexit1() {
    if raceenabled {    //与竞态检查有关,不关注
        racegoend()
    }
    if trace.enabled {  //与backtrace有关,不关注
        traceGoEnd()
    }
    // 注意,mcall函数的参数是一个函数goexit0
    // Function Value 结构 
    // type funcval struct {
    //      fn uintptr
    //      // 闭包捕获的参数在这
    // }
    mcall(goexit0)
}

mcall()

  1. 函数原型:func mcall(fn func(*g))
  2. runtime.mcall()函数和systemstack()函数很像,也是切换到系统栈去执行某个Function Value
  3. 但是也有些不同,mcall()函数不能在g0栈上调用,而且也不会再切换回来。
  4. 切换到m.g0栈,调用fn(g)
  5. Fn必须永不返回。它应该调用gogo(&g->sched)来保持g的运行。这里的g应该是g0
  6. 从当前运行的g切换到g0,这一步包括保存当前g的调度信息,把g0设置到tls中,修改CPUrsp寄存器使其指向g0的栈。
  7. 以当前运行的g为参数调用fn函数(此处为goexit0)。
  8. mcall函数不能在g0栈上调用,而且也不会再切换回来。
  9. 文件位置:go1.19.3/src/runtime/asm_amd64.s
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# func mcall(fn func(*g))
# Switch to m->g0's stack, call fn(g).
# Fn must never return. It should gogo(&g->sched)
# to keep running g.
TEXT runtime·mcall<ABIInternal>(SB), NOSPLIT, $0-8
    # 1) 从AX中获取参数,注意这里还是再普通goroutine中不是g0
    
    # 在go1.17后版本中采用寄存器传参,因此AX作为第一个参数存储的是macll的参数
    # 传参顺序 AX、BX、CX、DI、SI、R8、R9、R10、R11
    MOVQ    AX, DX  # DX = &funcval; &funcval -> goexit0

    # 2) 保存状态到 g->sched。这里的g是当前正在运行的goroutine。

    # save state in g->sched
    # 以下保存当前状态到g->sched中,在go 1.17版本后R14寄存器存储的是当前工作线程运行的goroutine
    # 0(SP):存储的是goexit1函数调用mall函数的下一条指令地址。也就是goexit1()函数的返回地址
    MOVQ    0(SP), BX                   # caller's PC	mcall返回地址放入BX
    # g.sched.pc = BX
    MOVQ    BX, (g_sched+gobuf_pc)(R14) # g.sched.pc = BX,保存g的rip
    # fn+0(FP)表当前参数所在栈位置,也就是goexit1函数的SP位置处。因为参数在调用者栈上。
    LEAQ    fn+0(FP), BX                # caller's SP
    MOVQ    BX, (g_sched+gobuf_sp)(R14) # g.sched.sp = BX,保存g的rsp
    # g.sched.bp = BP
    MOVQ    BP, (g_sched+gobuf_bp)(R14) # g.sched.bp = BP,保存g的rbp

    # 3) 切换到 m->g0 及其堆栈,调用fn函数。

    # switch to m->g0 & its stack, call fn
    # 
    # BX = m
    MOVQ    g_m(R14), BX    # BX = g.m,拿到当前工作线程M
    # SI = g0
    MOVQ    m_g0(BX), SI    # SI = g.m.g0,那当当前工作线程M的g0栈
    # 此刻,SI = g0, R14 = g,所以这里在判断g是否是g0,如果g == g0则一定是哪里代码写错了
    CMPQ    SI, R14         # if g == m->g0 call badmcall
    JNE goodm # SI和R14不相等则跳转
    JMP runtime·badmcall(SB)
goodm:  # 正常流程跳转到这里
    # AX = g
    MOVQ    R14, AX     # AX (and arg 0) = g,AX = g2,当前g不是g0,AX也是goexit0函数需要的参数
    # R14 = g0
    MOVQ    SI, R14     # g = g.m.g0,R14 = g0,设置当前正在运行的是g0
    # CX = &m.tls[1]
    get_tls(CX)         # Set G in TLS
    # TLS = g0
    MOVQ    R14, g(CX)
    # sp = g0.sched.sp
    MOVQ    (g_sched+gobuf_sp)(R14), SP
    # AX = g 入栈,此时已经在g0的栈上了
    # AX存储的时普通的goroutine,这里入栈也是goexit0()函数的参数
    PUSHQ   AX          # open up space for fn's arg spill slot
    # R12 = funcval.fn; DX = &funcval
    MOVQ    0(DX), R12  # fn的第一个成员是goexit0函数代码地址处,R12 = fn.fn
    # 调用goexit0()函数,参数再AX寄存器中,上下文在DX寄存器中。
    CALL    R12         # 调用 goexit0(g),这里【永不会返回】
    POPQ    AX
    JMP	runtime·badmcall2(SB)
    RET

goexit0()

  1. g0上继续执行goexit0()。参数:gp *g,当前运行完的普通的goroutine
  2. g2栈切换到g0栈之后,下面开始在g0栈执行goexit0()函数,该函数完成最后的清理工作:
    1. g的状态从_Grunning变更为_Gdead
    2. 然后把g的一些字段清空成零值。
    3. 调用dropg函数解除gm之间的关系,其实就是设置 g->m = nil,m->currg = nil
    4. g放入pfreeg队列缓存起来供下次创建g时快速获取而不用从内存分配。freeg就是g的一个对象池。
    5. 调用schedule()函数再次进行调度。
  3. 文件位置:go1.19.3/src/runtime/runtime/proc.go
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
// goexit continuation on g0.
func goexit0(gp *g) {
    _g_ := getg()           // _g_ = g0
    _p_ := _g_.m.p.ptr()    // _p_ = g0.m.p
    
    // _Grunning:2 表示这个 goroutine 可以执行用户代码。 堆栈由这个 goroutine 拥有。 
    // 它不在运行队列中。它被分配了一个 M 和一个 P(g.m 和 g.m.p 是有效的)
    // _Gdead:6 表示这个 goroutine 当前未被使用,它可能刚刚退出,在空闲列表中,或者刚刚被初始化
    casgstatus(gp, _Grunning, _Gdead) // g马上退出,所以设置其状态为_Gdead
    gcController.addScannableStack(_p_, -int64(gp.stack.hi-gp.stack.lo)) // 已分配栈总量
    // sSystemGoroutine 报告在堆栈转储和死锁检测器中是否必须省略 goroutine g
    // 这是在 runtime.* 入口点启动的任何 goroutine,除了 runtime.main、runtime.handleAsyncEvent
    // (仅限 wasm)和有时 runtime.runfinq
    // 如果 fixed 为真,任何可以在用户和系统之间变化的 goroutine(即终结器 goroutine)都被认为是用户 goroutine
    if isSystemGoroutine(gp, false) {
        atomic.Xadd(&sched.ngsys, -1)   // sched.ngsys 记录系统goroutine的数量
    }
    
    // 清空g保存的一些信息
    gp.m = nil
    // lockedm 关联到与当前G绑定的M,可以参考下 LockOSThread。
    locked := gp.lockedm != 0
    gp.lockedm = 0
    _g_.m.lockedg = 0
    gp.preemptStop = false
    gp.paniconfault = false
    gp._defer = nil // should be true already but just in case.
    gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
    gp.writebuf = nil
    gp.waitreason = 0
    gp.param = nil
    gp.labels = nil
    gp.timer = nil

    // gcBlackenEnabled:表示辅助助手和后台标记线程允许将对象置为黑色;
    // gcAssistBytes:表示当前goroutine还有信用值。(GC相关)
    if gcBlackenEnabled != 0 && gp.gcAssistBytes > 0 {
        // Flush assist credit to the global pool. This gives
        // better information to pacing if the application is
        // rapidly creating an exiting goroutines.
        assistWorkPerByte := gcController.assistWorkPerByte.Load()
        scanCredit := int64(assistWorkPerByte * float64(gp.gcAssistBytes))
        atomic.Xaddint64(&gcController.bgScanCredit, scanCredit)
        gp.gcAssistBytes = 0
    }

    // g2->m = nil, m->currg = nil 解绑g和m之关系
    // m->currg记录着前一个g信息
    // func dropg() {
    //      _g_ := getg()                   // _g_ = g0
    //      setMNoWB(&_g_.m.curg.m, nil)    // g2->m = nil
    //      setGNoWB(&_g_.m.curg, nil)      // m->currg = nil
    // }
    // 
    // func setMNoWB(mp **m, new *m) {
    //      (*muintptr)(unsafe.Pointer(mp)).set(new)
    // }
    dropg() // 解绑gp的m和当前m.currg值

    if GOARCH == "wasm" { // no threads yet on wasm
        gfput(_p_, gp)
        schedule() // never returns
    }
    // lockedInt 内部lockOSThread的跟踪
    if _g_.m.lockedInt != 0 {
        print("invalid m->lockedInt = ", _g_.m.lockedInt, "\n")
        throw("internal lockOSThread error")
    }
    // go keyword 文档关于 gfput 和 gfget 函数注解。
    gfput(_p_, gp)  // g2放入p的freeg队列,方便下次重用,免得再去申请内存,提高效率
    if locked {
        // The goroutine may have locked this thread because
        // it put it in an unusual kernel state. Kill it
        // rather than returning it to the thread pool.

        // Return to mstart, which will release the P and exit
        // the thread.
        if GOOS != "plan9" { // See golang.org/issue/22227.
            gogo(&_g_.m.g0.sched)
        } else {
            // Clear lockedExt on plan9 since we may end up re-using
            // this thread.
            _g_.m.lockedExt = 0
        }
    }
    schedule()  // 下面再次调用schedule
}