Go 的 GMP 模型真的很"简单" - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
tigerbcode

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

  •  
  •   tigerbcode 2024 年 8 月 6 日 1623 次点击
    这是一个创建于 626 天前的主题,其中的信息可能已经有所发展或是发生改变。

    查看带图原文 请移步 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是类似面相对象思想的封装

    类型 结构体含义 结构体职责
    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(){}()

    1. Gfunc具体绑定在哪?
    2. Gfunc何时绑定?
    // `go`关键字示例 func main() { // 使用 go 关键并发执行一个函数 go func() { fmt.Println("demo") }() } 

    Gfunc具体绑定在哪?

    位于 g 的结构体 g.startpc属性,详细如下:

    // Goroutine // 代码位置:go1.19/src/runtime/proc.go type g struct { //...略... gopc uintptr // go 关键字创建 Goroutine 的代码位置 //...略... startpc uintptr // Goroutine 绑定的函数代码地址 //...略... } 

    Gfunc何时绑定?

    1. 当通过 go 关键字运行一个函数时
    2. 从 g 的闲置队列获取一个 g ,并通过g.startpc属性绑定上待执行的函数 fn
    // 当你用 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切换上下文过程

    1. goroutine的上下文信息具体保存在哪?
    2. goroutine的上下文如何切换?

    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 恢复上下文过程:

    触发调度时:

    1. 找到可执行的 g (来源本地队列、全局队列、netpoll list 读或写就绪的 g 列表)
    2. 把 g 的上下文g.sched通过汇编代码中的函数gogo恢复到对应的寄存器中
    // 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(Gobu*) // restore state from Gobuf; longjmp TEXT runtimegogo(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 runtimesave_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 保存上下文过程:

    其中两个关键函数如下

    1. func save(pc, sp uintptr)触发保存上下文
    2. func mcall(fn func(*g))触发保存上下文

    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 runtimemcall<ABIInternal>(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 runtimesave_g(SB) CMP g, R3 BNE 2(PC) B runtimebadmcall(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 runtimebadmcall2(SB) 

    调用func mcall(fn func(*g))的场景如下:

    1. Gosched():触发协作&抢占式式调度时
    2. gopark:g 从运行状态转换为等待状态时
    3. goexit1()goroutine 执行完成时
    4. exitsyscall() 退出系统调用时

    详细展开,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职责解析

    1. 绑定真正执行代码的系统线程
    2. 系统线程执行G的调度
    3. 系统线程执行被调度的G绑定的函数
    4. 维护P链表(可以从下一个P的队列找G
    // 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职责解析

    1. 维护可执行G的队列(M从该队列找可执行的G);
    2. 堆内存缓存层(mcache
    3. 维护 g 的闲置队列
    // 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 是三个各司其职的结构体,被系统线程运行。
    类型 结构体含义 结构体职责
    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 恢复和保存上下文过程:

      1. 当前 g 保存上下文( save/mcall )
      2. 当前 g 切换到 g0 ,g0 执行schedule调度,找到新的可执行的 g
      3. 新的 g 恢复上下文( gogo )

      具体如下图所示:

    查看带图原文 请移步 https://mp.weixin.qq.com/s/bj98rCXtnGJT87vBd4JEAw

    更多内容请移步《 Go 语言轻松进阶:从入门、实战到内核揭秘》全面解析 Go 语言,从新手入门到实战应用,再到内核机制,一应俱全。https://tigerb.cn/go/#/

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     886 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 53ms UTC 20:53 PVG 04:53 LAX 13:53 JFK 16:53
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86