继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

码农桃花源
关注TA
已关注
手记 30
粉丝 88
获赞 320

上一讲说到调度器将 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 函数执行流程如下图:

http://img4.mukewang.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



打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP