V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tigerbcode
V2EX  ›  程序员

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

  •  
  •   tigerbcode · 53 天前 · 1098 次点击
    这是一个创建于 53 天前的主题,其中的信息可能已经有所发展或是发生改变。

    查看带图原文 请移步 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(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 保存上下文过程:

    其中两个关键函数如下

    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 runtime·mcall<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	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))的场景如下:

    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   ·   实用小工具   ·   1071 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 18:16 · PVG 02:16 · LAX 11:16 · JFK 14:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.