Go 的 GMP 模型真的很"简单"

查看 8|回复 0
作者:tigerbcode   
查看带图原文 请移步 https://mp.weixin.qq.com/s/bj98rCXtnGJT87vBd4JEAw
更多内容请移步《 Go 语言轻松进阶:从入门、实战到内核揭秘》全面解析 Go 语言,从新手入门到实战应用,再到内核机制,一应俱全。https://tigerb.cn/go/#/
前言
关于 GMP 模型网上已经有很多文章,讲的内容大多都是如下图的逻辑,本系列我们就不再赘述。本系列我们换个视角,核心是搞清楚两个问题:
  • GMP 到底是什么?
  • goroutine 如何恢复和保存上下文的?

    正文开始。
    GMP只是结构体
    GMP并不是你想象的那么神奇的存在,其实就是普通的结构体,如同你写业务代码定义的结构体一样,如下:
    // Goroutine
    // 代码位置:go1.19/src/runtime/proc.go
    type g struct {
            stack     stack
            //...略...
            gopc      uintptr
            startpc   uintptr
            sched     struct {
                    sp   uintptr
                    pc   uintptr
                    //...略...
                    bp   uintptr
            }
            //...略...
    }
    // Machine
    // 代码位置:go1.19/src/runtime/proc.go
    type m struct {
        g0            *g     
            //...略...
            curg          *g
            p             puintptr
            nextp         puintptr
            //...略...
            mOS
    }
    // Processor
    // 代码位置:go1.19/src/runtime/proc.go
    type p struct {
            id          int32
            //...略...
            m           muintptr
            mcache      *mcache
            //...略...
            runqhead uint32
            runqtail uint32
            runq     [256]guintptr
            runnext guintptr
        //...略...
            gFree struct {
                    gList
                    n int32
            }
        //...略...
            mspancache struct {
                    len int
                    buf [128]*mspan
            }
        //...略...
            gcw gcWork
    }
    GMP是系统线程运行的代码片段
    GMP和你写的业务代码一样,都是由系统线程运行。
    GMP是类似面相对象思想的封装
    [td]类型[/td]
    [td]结构体含义[/td]
    [td]结构体职责[/td]
    G
    Goroutine ,代表协程
    1. 封装可被并发执行的函数片段,比如 go func() {// 函数 A}()
    G
    -
    2. 暂存函数片段(协程)切换时的上下文信息
    G
    -
    3. 封装 g 的栈内存空间,暂存函数片段(协程)执行时的临时变量的
    M
    Machine ,和系统线程建立映射,结构体绑定一个系统线程
    1. 绑定真正执行代码的系统线程,系统线程执行G的调度,和被调度的G绑定的函数
    M
    -
    2. 维护P链表(可以从下一个P的队列找G)
    P
    Processor ,和逻辑处理器建立映射
    1. 维护可执行G的队列(M从该队列找可执行的G);
    P
    -
    2. 堆内存缓存层(mcache)
    P
    -
    3. 维护 g 的闲置队列
    G职责解析
    接下来,展开关于G展开两个关键问题:
  • G和函数绑定过程
  • G切换上下文过程

    G和函数绑定过程
    当你使用go关键字执行一个函数时go func(){}():
    [ol]
  • G和func具体绑定在哪?
  • G和func何时绑定?
    [/ol]
    // `go`关键字示例
    func main() {
            // 使用 go 关键并发执行一个函数
            go func() {
                    fmt.Println("demo")
            }()
    }

    G和func具体绑定在哪?

    位于 g 的结构体 g.startpc属性,详细如下:
    // Goroutine
    // 代码位置:go1.19/src/runtime/proc.go
    type g struct {
            //...略...
            gopc      uintptr  // go 关键字创建 Goroutine 的代码位置
        //...略...
            startpc   uintptr // Goroutine 绑定的函数代码地址
        //...略...
    }

    G和func何时绑定?

    [ol]
  • 当通过 go 关键字运行一个函数时
  • 从 g 的闲置队列获取一个 g ,并通过g.startpc属性绑定上待执行的函数 fn
    [/ol]
    // 当你用 go 关键字执行一个函数
    // 通过这个函数 绑定 g 和 待被执行的函数 fn
    func newproc(fn *funcval) {
            gp := getg()
            // 获取使用 go 关键字调用 fn 的代码位置
            // 方便 fn 执行完成之后跳回原代码位置
            pc := getcallerpc()
            systemstack(func() {
                    // 绑定过程在这个函数中
                    // 下面进一步分析 newproc1
                    newg := newproc1(fn, gp, pc)
                    _p_ := getg().m.p.ptr()
                    // 放入本地队列
                    // 等待调度
                    runqput(_p_, newg, true)
                    if mainStarted {
                            wakep()
                    }
            })
    }
    // 绑定过程在这个函数中 分析 newproc1
    func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
            //...略...
            newg := gfget(_p_) // 从 g 的闲置队列获取一个 g
            //...略...
            newg.gopc = callerpc // 重点:设置 go 关键字的位置,便于 fn 执行完毕跳回原代码位置
            newg.startpc = fn.fn // 重点:这里绑定待被执行的函数 fn
            //...略...
            return newg
    }
    函数绑定过程如下:
    G切换上下文过程
    [ol]
  • goroutine的上下文信息具体保存在哪?
  • goroutine的上下文如何切换?
    [/ol]

    goroutine的上下文信息具体保存在哪?

    位于 g 的结构体 g.sched属性,详细如下:
    // Goroutine
    // 代码位置:go1.19/src/runtime/proc.go
    type g struct {
            stack     stack // 协程栈 执行过程临时变量存放的地方
            sched     gobuf // Goroutine 上下文信息 保存在这个结构
        //...略...
    }
    // Goroutine 上下文信息
    type gobuf struct {
            sp   uintptr // 栈指针:指向栈顶
            pc   uintptr // 代码(指令)执行位置的地址
            //...略...
            bp   uintptr // 基指针:指向栈基
    }

    goroutine的上下文如何切换?

  • g 恢复上下文过程
  • g 保存上下文过程

    g 恢复上下文过程:
    触发调度时:
    [ol]
  • 找到可执行的 g (来源本地队列、全局队列、netpoll list 读或写就绪的 g 列表)
  • 把 g 的上下文g.sched通过汇编代码中的函数gogo恢复到对应的寄存器中
    [/ol]
    // g 的调度方法
    func schedule() {
           
            //...略...
            // 找可执行的 g (本地队列、全局队列、netpoll list 读或写就绪的 g 列表 等)
            gp, inheritTime, tryWakeP := findRunnable()
           
            //...略...
           
            //在这里 继续往下看
            execute(gp, inheritTime)
    }
    func execute(gp *g, inheritTime bool) {
            //...略...
            // 关键就是通过 gogo 这个函数 恢复
            gogo(&gp.sched)
    }
    gogo 函数汇编代码,arm64 架构示例汇编代码如下:
    // void gogo(Gobuf*)
    // restore state from Gobuf; longjmp
    TEXT runtime·gogo(SB), NOSPLIT|NOFRAME, $0-8
            MOVD        buf+0(FP), R5
            MOVD        gobuf_g(R5), R6
            MOVD        0(R6), R4
            B        gogo(SB)
    TEXT gogo(SB), NOSPLIT|NOFRAME, $0
            MOVD        R6, g
            BL        runtime·save_g(SB)
            MOVD        gobuf_sp(R5), R0 // 恢复栈指针
            MOVD        R0, RSP
            MOVD        gobuf_bp(R5), R29 // 恢复基指针
            MOVD        gobuf_lr(R5), LR
            MOVD        gobuf_ret(R5), R0
            MOVD        gobuf_ctxt(R5), R26
            MOVD        $0, gobuf_sp(R5)
            MOVD        $0, gobuf_bp(R5)
            MOVD        $0, gobuf_ret(R5)
            MOVD        $0, gobuf_lr(R5)
            MOVD        $0, gobuf_ctxt(R5)
            CMP        ZR, ZR
            MOVD        gobuf_pc(R5), R6 // 恢复 PC 计数器 指向下一个待执行的指令
            B        (R6)
    g 保存上下文过程:
    其中两个关键函数如下
    [ol]
  • func save(pc, sp uintptr)触发保存上下文
  • func mcall(fn func(*g))触发保存上下文
    [/ol]
    save 函数
    func save(pc, sp uintptr) {
            _g_ := getg()
            //...略...
            _g_.sched.pc = pc // 保存代码执行位置
            _g_.sched.sp = sp // 保存栈指针
           
            //...略...
    }
    调用func save(pc, sp uintptr)的场景如下:
  • 进入系统调用时

    // 进入系统调用
    func entersyscall() {
            reentersyscall(getcallerpc(), getcallersp())
    }
    func reentersyscall(pc, sp uintptr) {
            _g_ := getg()
            //...略...
            // 保存上下文
            save(pc, sp)
            _g_.syscallsp = sp
            _g_.syscallpc = pc
            casgstatus(_g_, _Grunning, _Gsyscall)
            //...略...
    }
    mcall 函数
    func mcall(fn func(*g))执行过程中,从 g 切换到 g0 ,并执行 fn 。fn 内部会执行调度函数 shedule(),触发新的调度,下面会举一个例子。
    TEXT runtime·mcall(SB), NOSPLIT|NOFRAME, $0-8
            MOVD        R0, R26       
            MOVD        RSP, R0
            MOVD        R0, (g_sched+gobuf_sp)(g) // 保存当前 g 的栈指针
            MOVD        R29, (g_sched+gobuf_bp)(g) // 保存当前 g 的基指针
            MOVD        LR, (g_sched+gobuf_pc)(g)// 保存当前 g 的下一个待执行指令的位置 PC 计数器
            MOVD        $0, (g_sched+gobuf_lr)(g)
            // 切换到 g0 ,并执行函数 fn
            MOVD        g, R3
            MOVD        g_m(g), R8
            MOVD        m_g0(R8), g
            BL        runtime·save_g(SB)
            CMP        g, R3
            BNE        2(PC)
            B        runtime·badmcall(SB)
            MOVD        (g_sched+gobuf_sp)(g), R0
            MOVD        R0, RSP       
            MOVD        (g_sched+gobuf_bp)(g), R29
            MOVD        R3, R0       
            MOVD        $0, -16(RSP)
            SUB        $16, RSP
            MOVD        0(R26), R4
            BL        (R4)
            B        runtime·badmcall2(SB)
    调用func mcall(fn func(*g))的场景如下:
    [ol]
  • Gosched():触发协作&抢占式式调度时
  • gopark:g 从运行状态转换为等待状态时
  • goexit1()goroutine 执行完成时
  • exitsyscall() 退出系统调用时

  • [/ol]
    详细展开,Gosched():触发协作&抢占式式调度时看看,如下
    // 触发调度
    func Gosched() {
            checkTimeouts()
            mcall(gosched_m)
    }
    func gosched_m(gp *g) {
            //...略...
            goschedImpl(gp)
    }
    func goschedImpl(gp *g) {
            //...略...
            // 正在运行状态转变为 可运行状态
            casgstatus(gp, _Grunning, _Grunnable)
            dropg()
            lock(&sched.lock)
            globrunqput(gp) // 放入全局队列
            unlock(&sched.lock)
            // 触发调度
            schedule()
    }
    func schedule() {
            //...略...
            // 找到下一个可执行的 g
            gp, inheritTime, tryWakeP := findRunnable()
            //...略...
            // 执行下一个 g
            execute(gp, inheritTime)
    }
    func execute(gp *g, inheritTime bool) {
            //...略...
            // 恢复上下文
            gogo(&gp.sched)
    }
    // gogo 汇编代码(arm64 架构)
    TEXT gogo(SB), NOSPLIT|NOFRAME, $0
            //...略...
            MOVD        gobuf_sp(R5), R0 // 恢复栈指针
            MOVD        gobuf_bp(R5), R29 // 恢复基指针
            //...略...
  • park_m 把 g 从运行状态转换为等待状态时

    func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
            //...略...
            mcall(park_m)
    }
    func park_m(gp *g) {
            //...略...
            casgstatus(gp, _Grunning, _Gwaiting)
            dropg()
            //...略...
            // 触发调度
            schedule()
    }
    //...略...
    // 同上`Gosched()`
  • goexit1()goroutine 执行完成时

    func goexit1() {
            //...略...
            mcall(goexit0)
    }
    // goexit continuation on g0.
    func goexit0(gp *g) {
            //...略...
            // 触发调度
            schedule()
    }
    //...略...
    // 同上`Gosched()`
  • exitsyscall() 退出系统调用时

    func exitsyscall() {
            //...略...
            mcall(exitsyscall0)
            //...略...
    }
    func exitsyscall0(gp *g) {
            casgstatus(gp, _Gsyscall, _Grunnable)
            dropg()
            //...略...
            stopm()
            // 触发调度
            schedule()
    }
    /...略...
    // 同上`Gosched()`
    具体如下图:
    总结下 g 的完整切换过程:
  • 当前 g 保存上下文( save/mcall )
  • 当前 g 切换到 g0 ,g0 执行schedule调度,找到新的可执行的 g
  • 新的 g 恢复上下文( gogo )
  • 最后,实际以上操作都是有系统线程运行的

    M职责解析
    [ol]
  • 绑定真正执行代码的系统线程
  • 系统线程执行G的调度
  • 系统线程执行被调度的G绑定的函数
  • 维护P链表(可以从下一个P的队列找G)
    [/ol]
    // Machine
    // 代码位置:go1.19/src/runtime/proc.go
    type m struct {
            g0            *g     
            //...略...
            curg          *g  // 当前执行的 g
            p             puintptr // m 绑定的 p
            nextp         puintptr // 4. 维护`P`链表(可以从下一个`P`的队列找`G`)
            //...略...
            // 1. 绑定真正执行代码的系统线程
            // 2. 执行`G`的调度
            // 3. 执行被调度的`G`绑定的函数
            mOS
        //...略...
    }
    P职责解析
    [ol]
  • 维护可执行G的队列(M从该队列找可执行的G);
  • 堆内存缓存层(mcache)
  • 维护 g 的闲置队列
    [/ol]
    // Processor
    // 代码位置:go1.19/src/runtime/proc.go
    type p struct {
            id          int32
            //...略...
            m           muintptr
            mcache      *mcache // 堆内存缓存层(`mcache`)
            //...略...
            runqhead uint32 // 1. 维护可执行`G`的队列(`M`从该队列找可执行的`G`);
            runqtail uint32 // 1. 维护可执行`G`的队列(`M`从该队列找可执行的`G`);
            runq     [256]guintptr // 1. 维护可执行`G`的队列(`M`从该队列找可执行的`G`);
            runnext guintptr // 1. 维护可执行`G`的队列(`M`从该队列找可执行的`G`);
        //...略...
            // 3. 维护 g 的闲置队列
            gFree struct {
                    gList
                    n int32
            }
        //...略...
            mspancache struct {
                    len int
                    buf [128]*mspan
            }
        //...略...
            gcw gcWork
    }
    总结
    再来回头看开篇的两个问题?
  • GMP 到底是什么?
  • goroutine 如何恢复和保存上下文的?

    是不是已经很清晰。
  • 关于问题一,GMP 是三个各司其职的结构体,被系统线程运行。

    [td]类型[/td]
    [td]结构体含义[/td]
    [td]结构体职责[/td]
    G
    Goroutine ,代表协程
    1. 封装可被并发执行的函数片段,比如 go func() {// 函数 A}()
    G
    -
    2. 暂存函数片段(协程)切换时的上下文信息
    G
    -
    3. 封装 g 的栈内存空间,暂存函数片段(协程)执行时的临时变量的
    M
    Machine ,和系统线程建立映射,结构体绑定一个系统线程
    1. 绑定真正执行代码的系统线程,系统线程执行G的调度,和被调度的G绑定的函数
    M
    -
    2. 维护P链表(可以从下一个P的队列找G)
    P
    Processor ,和逻辑处理器建立映射
    1. 维护可执行G的队列(M从该队列找可执行的G);
    P
    -
    2. 堆内存缓存层(mcache)
    P
    -
    3. 维护 g 的闲置队列

  • 关于问题二,goroutine 恢复和保存上下文过程:
    [ol]
  • 当前 g 保存上下文( save/mcall )
  • 当前 g 切换到 g0 ,g0 执行schedule调度,找到新的可执行的 g
  • 新的 g 恢复上下文( gogo )
    [/ol]
    具体如下图所示:

    查看带图原文 请移步 https://mp.weixin.qq.com/s/bj98rCXtnGJT87vBd4JEAw
    更多内容请移步《 Go 语言轻松进阶:从入门、实战到内核揭秘》全面解析 Go 语言,从新手入门到实战应用,再到内核机制,一应俱全。https://tigerb.cn/go/#/
  • 您需要登录后才可以回帖 登录 | 立即注册

    返回顶部