为了账号安全,请及时绑定邮箱和手机立即绑定

千难万险 —— goroutine 从生到死(六)

标签:
Go

上一讲说到调度器将 main goroutine 推上舞台,为它铺好了道路,开始执行 runtime.main 函数。这一讲,我们探索 main goroutine 以及普通 goroutine 从执行到退出的整个过程。

  1. // The main goroutine.

  2. func main() {

  3.    // g = main goroutine,不再是 g0 了

  4.    g := getg()


  5.    // ……………………


  6.    if sys.PtrSize == 8 {

  7.        maxstacksize = 1000000000

  8.    } else {

  9.        maxstacksize = 250000000

  10.    }


  11.    // Allow newproc to start new Ms.

  12.    mainStarted = true


  13.    systemstack(func() {

  14.        // 创建监控线程,该线程独立于调度器,不需要跟 p 关联即可运行

  15.        newm(sysmon, nil)

  16.    })


  17.    lockOSThread()


  18.    if g.m != &m0 {

  19.        throw("runtime.main not on m0")

  20.    }


  21.    // 调用 runtime 包的初始化函数,由编译器实现

  22.    runtime_init() // must be before defer

  23.    if nanotime() == 0 {

  24.        throw("nanotime returning zero")

  25.    }


  26.    // Defer unlock so that runtime.Goexit during init does the unlock too.

  27.    needUnlock := true

  28.    defer func() {

  29.        if needUnlock {

  30.            unlockOSThread()

  31.        }

  32.    }()


  33.    // Record when the world started. Must be after runtime_init

  34.    // because nanotime on some platforms depends on startNano.

  35.    runtimeInitTime = nanotime()


  36.    // 开启垃圾回收器

  37.    gcenable()


  38.    main_init_done = make(chan bool)


  39.    // ……………………


  40.    // main 包的初始化,递归的调用我们 import 进来的包的初始化函数

  41.    fn := main_init

  42.    fn()

  43.    close(main_init_done)


  44.    needUnlock = false

  45.    unlockOSThread()


  46.    // ……………………


  47.    // 调用 main.main 函数

  48.    fn = main_main

  49.    fn()

  50.    if raceenabled {

  51.        racefini()

  52.    }


  53.    // ……………………


  54.    // 进入系统调用,退出进程,可以看出 main goroutine 并未返回,而是直接进入系统调用退出进程了

  55.    exit(0)

  56.    // 保护性代码,如果 exit 意外返回,下面的代码会让该进程 crash 死掉

  57.    for {

  58.        var x *int32

  59.        *x = 0

  60.    }

  61. }

main 函数执行流程如下图:

https://img1.sycdn.imooc.com//5d73bb380001453e05080510.jpg

从流程图可知,main goroutine 执行完之后就直接调用 exit(0) 退出了,这会导致整个进程退出,太粗暴了。

不过,main goroutine 实际上就是代表用户的 main 函数,它都执行完了,肯定是用户的任务都执行完了,直接退出就可以了,就算有其他的 goroutine 没执行完,同样会直接退出。

  1. package main


  2. import "fmt"


  3. func main() {

  4.     go func() {fmt.Println("hello qcrao.com")}()

  5. }

在这个例子中,main gorutine 退出时,还来不及执行 go出去 的函数,整个进程就直接退出了,打印语句不会执行。因此,main goroutine 不会等待其他 goroutine 执行完再退出,知道这个有时能解释一些现象,比如上面那个例子。

这时,心中可能会跳出疑问,我们在新创建 goroutine 的时候,不是整出了个“偷天换日”,风风火火地设置了 goroutine 退出时应该跳到 runtime.goexit 函数吗,怎么这会不用了,闲得慌?

回顾一下上一讲的内容,跳转到 main 函数的两行代码:

// 把 sched.pc 值放入 BX 寄存器MOVQ    gobuf_pc(BX), BX// JMP 把 BX 寄存器的包含的地址值放入 CPU 的 IP 寄存器,于是,CPU 跳转到该地址继续执行指令JMP    BX

直接使用了一个跳转,并没有使用 CALL 指令,而 runtime.main 函数中确实也没有 RET 返回的指令。所以,main goroutine 执行完后,直接调用 exit(0) 退出整个进程。

那之前整地“偷天换日”还有用吗?有的!这是针对非 main goroutine 起作用。

参考资料【阿波张 非 goroutine 的退出】中用调试工具验证了非 main goroutine 的退出,感兴趣的可以去跟着实践一遍。

我们继续探索非 main goroutine (后文我们就称 gp 好了)的退出流程。

gp 执行完后,RET 指令弹出 goexit 函数地址(实际上是 funcPC(goexit)+1),CPU 跳转到 goexit 的第二条指令继续执行:

  1. // src/runtime/asm_amd64.s


  2. // The top-most function running on a goroutine

  3. // returns to goexit+PCQuantum.

  4. TEXT runtime·goexit(SB),NOSPLIT,$0-0

  5.    BYTE    $0x90  // NOP

  6.    CALL    runtime·goexit1(SB) // does not return

  7.    // traceback from goexit1 must hit code range of goexit

  8.    BYTE    $0x90  // NOP

直接调用 runtime·goexit1

// src/runtime/proc.go// Finishes execution of the current goroutine.func goexit1() {    // ……………………    mcall(goexit0)}

调用 mcall 函数:

  1. // 切换到 g0 栈,执行 fn(g)

  2. // Fn 不能返回

  3. TEXT runtime·mcall(SB), NOSPLIT, $0-8

  4.    // 取出参数的值放入 DI 寄存器,它是 funcval 对象的指针,此场景中 fn.fn 是 goexit0 的地址

  5.    MOVQ    fn+0(FP), DI


  6.    get_tls(CX)

  7.    // AX = g

  8.    MOVQ    g(CX), AX   // save state in g->sched

  9.    // mcall 返回地址放入 BX

  10.    MOVQ    0(SP), BX   // caller's PC

  11.    // g.sched.pc = BX,保存 g 的 PC

  12.    MOVQ    BX, (g_sched+gobuf_pc)(AX)

  13.    LEAQ    fn+0(FP), BX    // caller's SP

  14.    // 保存 g 的 SP

  15.    MOVQ    BX, (g_sched+gobuf_sp)(AX)

  16.    MOVQ    AX, (g_sched+gobuf_g)(AX)

  17.    MOVQ    BP, (g_sched+gobuf_bp)(AX)


  18.    // switch to m->g0 & its stack, call fn

  19.    MOVQ    g(CX), BX

  20.    MOVQ    g_m(BX), BX

  21.    // SI = g0

  22.    MOVQ    m_g0(BX), SI

  23.    CMPQ    SI, AX  // if g == m->g0 call badmcall

  24.    JNE 3(PC)

  25.    MOVQ    $runtime·badmcall(SB), AX

  26.    JMP AX

  27.    // 把 g0 的地址设置到线程本地存储中

  28.    MOVQ    SI, g(CX)   // g = m->g0

  29.    // 从 g 的栈切换到了 g0 的栈D

  30.    MOVQ    (g_sched+gobuf_sp)(SI), SP  // sp = m->g0->sched.sp

  31.    // AX = g,参数入栈

  32.    PUSHQ   AX

  33.    MOVQ    DI, DX

  34.    // DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址

  35.    // 读取第一个成员到 DI 寄存器

  36.    MOVQ    0(DI), DI

  37.    // 调用 goexit0(g)

  38.    CALL    DI

  39.    POPQ    AX

  40.    MOVQ    $runtime·badmcall2(SB), AX

  41.    JMP AX

  42.    RET

函数参数是:

type funcval struct {    fn uintptr    // variable-size, fn-specific data here}

字段 fn 就表示 goexit0 函数的地址。

L5 将函数参数保存到 DI 寄存器,这里 fn.fn 就是 goexit0 的地址。

L7 将 tls 保存到 CX 寄存器,L9 将 当前线程指向的 goroutine (非 main goroutine,称为 gp)保存到 AX 寄存器,L11 将调用者(调用 mcall 函数)的栈顶,这里就是 mcall 完成后的返回地址,存入 BX 寄存器。

L13 将 mcall 的返回地址保存到 gp 的 g.sched.pc 字段,L14 将 gp 的栈顶,也就是 SP 保存到 BX 寄存器,L16 将 SP 保存到 gp 的 g.sched.sp 字段,L17 将 g 保存到 gp 的 g.sched.g 字段,L18 将 BP 保存 到 gp 的 g.sched.bp 字段。这一段主要是保存 gp 的调度信息。

L21 将当前指向的 g 保存到 BX 寄存器,L22 将 g.m 字段保存到 BX 寄存器,L23 将 g.m.g0 字段保存到 SI,g.m.g0 就是当前工作线程的 g0。

现在,SI = g0, AX = gp,L25 判断 gp 是否是 g0,如果 gp == g0 说明有问题,执行 runtime·badmcall。正常情况下,PC 值加 3,跳过下面的两条指令,直接到达 L30。

L30 将 g0 的地址设置到线程本地存储中,L32 将 g0.SP 设置到 CPU 的 SP 寄存器,这也就意味着我们从 gp 栈切换到了 g0 的栈,要变天了!

L34 将参数 gp 入栈,为调用 goexit0 构造参数。L35 将 DI 寄存器的内容设置到 DX 寄存器,DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址。L36 读取 DI 第一成员,也就是 goexit0 函数的地址。

L40 调用 goexit0 函数,这已经是在 g0 栈上执行了,函数参数就是 gp。

到这里,就会去执行 goexit0 函数,注意,这里永远都不会返回。所以,在 CALL 指令后面,如果返回了,又会去调用 runtime.badmcall2 函数去处理意外情况。

来继续看 goexit0:

  1. // goexit continuation on g0.

  2. // 在 g0 上执行

  3. func goexit0(gp *g) {

  4.    // g0

  5.    _g_ := getg()


  6.    casgstatus(gp, _Grunning, _Gdead)

  7.    if isSystemGoroutine(gp) {

  8.        atomic.Xadd(&sched.ngsys, -1)

  9.    }


  10.    // 清空 gp 的一些字段

  11.    gp.m = nil

  12.    gp.lockedm = nil

  13.    _g_.m.lockedg = nil

  14.    gp.paniconfault = false

  15.    gp._defer = nil // should be true already but just in case.

  16.    gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.

  17.    gp.writebuf = nil

  18.    gp.waitreason = ""

  19.    gp.param = nil

  20.    gp.labels = nil

  21.    gp.timer = nil


  22.    // Note that gp's stack scan is now "valid" because it has no

  23.    // stack.

  24.    gp.gcscanvalid = true

  25.    // 解除 g 与 m 的关系

  26.    dropg()


  27.    if _g_.m.locked&^_LockExternal != 0 {

  28.        print("invalid m->locked = ", _g_.m.locked, "\n")

  29.        throw("internal lockOSThread error")

  30.    }

  31.    _g_.m.locked = 0

  32.    // 将 g 放入 free 队列缓存起来

  33.    gfput(_g_.m.p.ptr(), gp)

  34.    schedule()

  35. }

它主要完成最后的清理工作:


  1. 把 g 的状态从 _Grunning 更新为 _Gdead

  2. 清空 g 的一些字段;

  3. 调用 dropg 函数解除 g 和 m 之间的关系,其实就是设置 g->m = nil, m->currg = nil;

  4. 把 g 放入 p 的 freeg 队列缓存起来供下次创建 g 时快速获取而不用从内存分配。freeg 就是 g 的一个对象池;

  5. 调用 schedule 函数再次进行调度。

到这里,gp 就完成了它的历史使命,功成身退,进入了 goroutine 缓存池,待下次有任务再重新启用。

而工作线程,又继续调用 schedule 函数进行新一轮的调度,整个过程形成了一个循环。

总结一下,main goroutine 和普通 goroutine 的退出过程:

对于 main goroutine,在执行完用户定义的 main 函数的所有代码后,直接调用 exit(0) 退出整个进程,非常霸道。

对于普通 goroutine 则没这么“舒服”,需要经历一系列的过程。先是跳转到提前设置好的 goexit 函数的第二条指令,然后调用 runtime.goexit1,接着调用 mcall(goexit0),而 mcall 函数会切换到 g0 栈,运行 goexit0 函数,清理 goroutine 的一些字段,并将其添加到 goroutine 缓存池里,然后进入 schedule 调度循环。到这里,普通 goroutine 才算完成使命。

参考资料

【阿波张 非 main goroutine 的退出及调度循环】https://mp.weixin.qq.com/s/XttP9q7-PO7VXhskaBzGqA



点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消