zoukankan      html  css  js  c++  java
  • Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环

    本文内容主要分为三部分:

    1. main goroutine 的调度运行
    2. 非 main goroutine 的退出流程
    3. 工作线程的执行流程与调度循环。

    main goroutine 的调度运行

    runtime·rt0_go中在调用完runtime.newproc创建main goroutine后,就调用了runtime.mstart。让我们来分析一下这个函数。

    mstart

    mstart没什么太多工作,然后就调用了mstart1。

    func mstart() {
    	_g_ := getg()
            // 在启动阶段,_g_.stack早就完成了初始化,所以osStack是false,下面被省略的也不会执行。
    	osStack := _g_.stack.lo == 0 
    	......
    	_g_.stackguard0 = _g_.stack.lo + _StackGuard
    	_g_.stackguard1 = _g_.stackguard0
    	mstart1()
            ......
    	mexit(osStack)
    }
    

    mstart1

    • 调用save保存g0的状态
    • 处理信号相关
    • 调用 schedule 开始调度
    func mstart1() {
    	_g_ := getg()
    
    	if _g_ != _g_.m.g0 {
    		throw("bad runtime·mstart")
    	}
    	save(getcallerpc(), getcallersp())	// 保存调用mstart1的函数(mstart)的 pc 和 sp。
    	asminit()				// 空函数
    	minit()					// 信号相关
    
    	if _g_.m == &m0 {			// 初始化时会执行这里,也是信号相关
    		mstartm0()
    	}
    
    	if fn := _g_.m.mstartfn; fn != nil {	// 初始化时 fn = nil,不会执行这里
    		fn()
    	}
    
    	if _g_.m != &m0 {			// 不是m0的话,没有p。绑定一个p
    		acquirep(_g_.m.nextp.ptr())
    		_g_.m.nextp = 0
    	}
    	schedule()
    }
    

    save(pc, sp uintptr) 保存调度信息

    保存当前g(初始化时为g0)的状态到sched字段中。

    func save(pc, sp uintptr) {
    	_g_ := getg()
    	_g_.sched.pc = pc
    	_g_.sched.sp = sp
    	_g_.sched.lr = 0
    	_g_.sched.ret = 0
    	_g_.sched.g = guintptr(unsafe.Pointer(_g_))
    	if _g_.sched.ctxt != nil {
    		badctxt()
    	}
    }
    

    schedule 开始调度

    调用globrunqget、runqget、findrunnable获取一个可执行的g

    func schedule() {
    	_g_ := getg()	// g0
            ......
    	var gp *g	// 初始化时,经过下面一系列查找,会找到main goroutine,因为目前为止整个运行时只有这一个g(除了g0)。
    	var inheritTime bool
            ......
    	if gp == nil {
                    // 该p上每进行61次就从全局队列中获取一个g
    		if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
    			lock(&sched.lock)
    			gp = globrunqget(_g_.m.p.ptr(), 1)
    			unlock(&sched.lock)
    		}
    	}
    	if gp == nil {
                    // 从p的runq中获取一个g
    		gp, inheritTime = runqget(_g_.m.p.ptr())
    		// We can see gp != nil here even if the M is spinning,
    		// if checkTimers added a local goroutine via goready.
    	}
    	if gp == nil {
                    // 寻找可执行的g,会尝试从本地,全局运行对列获取,如果没有,从其他p那里偷取。
    		gp, inheritTime = findrunnable() // blocks until work is available
    	}
    	......
    	execute(gp, inheritTime)
    }
    

    execute:安排g在当前m上运行

    • 被调度的 g 与 m 相互绑定
    • 更改g的状态为 _Grunning
    • 调用 gogo 切换到被调度的g上
    func execute(gp *g, inheritTime bool) {
    	_g_ := getg()	// g0
    
    	_g_.m.curg = gp	// 与下面一行是 gp 和 m 相互绑定。gp 其实就是 main goroutine
    	gp.m = _g_.m
    	casgstatus(gp, _Grunnable, _Grunning)	// 更改状态
    	gp.waitsince = 0
    	gp.preempt = false
    	gp.stackguard0 = gp.stack.lo + _StackGuard
    	if !inheritTime {
    		_g_.m.p.ptr().schedtick++
    	}
    	......
    	gogo(&gp.sched)
    }
    

    gogo(buf *gobuf)

    在本方法下面的讲解中将使用newg代指被调度的g。

    gogo函数是用汇编实现的。其作用是:加载newg的上下文,跳转到gobuf.pc指向的函数。

    // go/src/runtime/asm_amd64.s
    TEXT runtime·gogo(SB), NOSPLIT, $16-8
    	MOVQ	buf+0(FP), BX		// bx = &gp.sched
    	MOVQ	gobuf_g(BX), DX		// dx = gp.sched.g ,也就是存储的 newg 指针
    	MOVQ	0(DX), CX		// make sure g != nil
    	get_tls(CX)
    	MOVQ	DX, g(CX)		// newg指针设置到tls
    	MOVQ	gobuf_sp(BX), SP	// 下面四条是加载上下文到cpu寄存器。
    	MOVQ	gobuf_ret(BX), AX
    	MOVQ	gobuf_ctxt(BX), DX
    	MOVQ	gobuf_bp(BX), BP
    	MOVQ	$0, gobuf_sp(BX)	// 下面四条是清零,减少gc的工作量。
    	MOVQ	$0, gobuf_ret(BX)
    	MOVQ	$0, gobuf_ctxt(BX)
    	MOVQ	$0, gobuf_bp(BX)
    	MOVQ	gobuf_pc(BX), BX	// gobuf.pc 存储的是要执行的函数指针,初始化时此函数为runtime.main
    	JMP	BX			// 跳转到要执行的函数
    

    runtime.main:main函数的执行

    在上面gogo执行最后的JMP指令,其实就是跳转到了runtime.main。

    func main() {
    	g := getg()		// 获取当前g,已经不是g0了,我们暂且称为maing
            
    	if sys.PtrSize == 8 {	// 64位系统,栈最大为1GB
    		maxstacksize = 1000000000
    	} else {
    		maxstacksize = 250000000
    	}
    	mainStarted = true
            // 启动监控进程,抢占调度就是在这里实现的
    	if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
    		systemstack(func() {
    			newm(sysmon, nil)
    		})
    	}
            ......
    	doInit(&runtime_inittask)	// 调用runtime的初始化函数
            ......
    	runtimeInitTime = nanotime()	// 记录世界开始时间
    	gcenable()			// 开启gc
    	......
    	doInit(&main_inittask)		// 调用main的初始化函数
            ......
    	fn := main_main			// 调用main.main,也就是我们经常写hello world的main。
    	fn()
            ......
    	exit(0)				// 退出
    }
    

    runtime.main主要做了以下的工作:

    • 启动监控进程。
    • 调用runtime的初始化函数。
    • 开启gc。
    • 调用main的初始化函数。
    • 调用main.main,执行完后退出。

    非 main goroutine 的退出流程

    首先明确一点,无论是main goroutine还是非main goroutine的都是调用newproc创建的,所以在调度上基本是一致的。

    之前的文章中说过,在gostartcall函数中,会将goroutine要执行的函数fn伪造成是被goexit调用的。但是,当fn是runtime.main的时候是没有用的,因为在runtime.main末尾会调用exit(0)退出程序。所以,这只对非main goroutine起作用。让我们简单验证一下。

    先给出一个简单的例子:

    package main
    
    import "fmt"
    
    func main() {
    	ch := make(chan int)
    	go foo(ch)
    	fmt.Println(<-ch)
    }
    
    func foo(ch chan int) {
    	ch <- 1
    }
    

    dlv调试一波:

    root@xiamin:~/study# dlv debug foo.go
    (dlv) b main.foo // 打个断点
    Breakpoint 1 set at 0x4ad86f for main.foo() ./foo.go:11
    (dlv) c
    > main.foo() ./foo.go:11 (hits goroutine(6):1 total:1) (PC: 0x4ad86f)
         6:		ch := make(chan int)
         7:		go foo(ch)
         8:		fmt.Println(<-ch)
         9:	}
        10:
    =>  11:	func foo(ch chan int) {
        12:		ch <- 1
        13:	}
    (dlv) bt // 可以看到调用栈中确实存在goexit
    0  0x00000000004ad86f in main.foo
       at ./foo.go:11
    1  0x0000000000463df1 in runtime.goexit
       at /root/go/src/runtime/asm_amd64.s:1373
    
    // 此处执行三次 s,得到以下结果,确实是回到了goexit。
    
    > runtime.goexit() /root/go/src/runtime/asm_amd64.s:1374 (PC: 0x463df1)
      1370:	// The top-most function running on a goroutine
      1371:	// returns to goexit+PCQuantum.
      1372:	TEXT runtime·goexit(SB),NOSPLIT,$0-0
      1373:		BYTE	$0x90	// NOP
    =>1374:		CALL	runtime·goexit1(SB)	// does not return
      1375:		// traceback from goexit1 must hit code range of goexit
      1376:		BYTE	$0x90	// NOP
    

    我们暂且将关联foo的g称之为foog,接下来我们看一下它的退出流程。

    goexit

    TEXT runtime·goexit(SB),NOSPLIT,$0-0
    	BYTE	$0x90	// NOP
    	CALL	runtime·goexit1(SB)	// does not return
    	// traceback from goexit1 must hit code range of goexit
    	BYTE	$0x90	// NOP
    

    goexit1

    func goexit1() {
    	if raceenabled {
    		racegoend()
    	}
    	if trace.enabled {
    		traceGoEnd()
    	}
    	mcall(goexit0)
    }
    

    goexit和goexit1没什么可说的,看一下mcall

    mcall(fn func(*g))

    mcall的参数是个函数fn,而fn有个参数是*g,此处fn是goexit0。

    mcall是由汇编编写的:

    TEXT runtime·mcall(SB), NOSPLIT, $0-8
    	MOVQ	fn+0(FP), DI	// 此处 di 存储的是 funcval 结构体指针,funcval.fn 指向的是 goexit0。
    
    	get_tls(CX)
    	MOVQ	g(CX), AX	// 此处 ax 中存储的是foog
    
            // 保存foog的上下文
    	MOVQ	0(SP), BX	// caller's PC。mcall的返回地址,此处就是 goexit1 调用 mcall 时的pc
    	MOVQ	BX, (g_sched+gobuf_pc)(AX)	// foog.sched.pc = caller's PC
    	LEAQ	fn+0(FP), BX			// caller's SP。
    	MOVQ	BX, (g_sched+gobuf_sp)(AX)	// foog.sched.sp = caller's SP
    	MOVQ	AX, (g_sched+gobuf_g)(AX)	// foog.sched.g = foog
    	MOVQ	BP, (g_sched+gobuf_bp)(AX)	// foog.sched.bp = bp
    
            // 切换到m.g0和它的栈,调用fn。
    	MOVQ	g(CX), BX			// 此处 bx 中存储的是foog
    	MOVQ	g_m(BX), BX			// bx = foog.m
    	MOVQ	m_g0(BX), SI			// si = m.g0
    	CMPQ	SI, AX				// if g == m->g0 call badmcall
    	JNE	3(PC)				// 上面的结果不相等就跳转到下面第三行。
    	MOVQ	$runtime·badmcall(SB), AX
    	JMP	AX
    	MOVQ	SI, g(CX)			// g = m->g0。m.g0设置到tls
    	MOVQ	(g_sched+gobuf_sp)(SI), SP	// sp = m->g0->sched.sp。设置g0栈.
    	PUSHQ	AX				// fn的参数压栈,ax = foog
    	MOVQ	DI, DX
    	MOVQ	0(DI), DI			// 读取 funcval 结构的第一个成员,也就是 funcval.fn,此处是goexit0。
    	CALL	DI				// 调用 goexit0(foog)。
    	POPQ	AX
    	MOVQ	$runtime·badmcall2(SB), AX
    	JMP	AX
    	RET
    

    在此场景下,mcall做了以下工作:保存foog的上下文。切换到g0及其栈,调用传入的方法,并将foog作为参数。

    可以看到mcall与gogo的作用正好相反:

    • gogo实现了从g0切换到某个goroutine,执行关联函数。
    • mcall实现了保存某个goroutine,切换到g0及其栈,并调用fn函数,其参数就是被保存的goroutine指针。

    goexit0

    func goexit0(gp *g) {
    	_g_ := getg()	// g0
    
    	casgstatus(gp, _Grunning, _Gdead)	// 更改gp状态为_Gdead
    	if isSystemGoroutine(gp, false) {
    		atomic.Xadd(&sched.ngsys, -1)
    	}
            // 下面的一段就是清零gp的属性
    	gp.m = nil
    	locked := gp.lockedm != 0
    	gp.lockedm = 0
    	_g_.m.lockedg = 0
    	gp.preemptStop = false
    	gp.paniconfault = false
    	gp._defer = nil // should be true already but just in case.
    	gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
    	gp.writebuf = nil
    	gp.waitreason = 0
    	gp.param = nil
    	gp.labels = nil
    	gp.timer = nil
    	......
    	dropg()				// 解绑gp与当前m。_g_.m.curg.m = nil ; _g_.m.curg = nil 。
            ......
    	gfput(_g_.m.p.ptr(), gp)	// 放入空闲列表。如果本地队列太多,会转移一部分到全局队列。
    	......
    	schedule()			// 重新调度
    }
    

    goexit0做了以下工作:

    • 将gp属性清零与m解绑
    • gfput 放入空闲列表
    • schedule 重新调度

    工作线程的执行流程与调度循环

    以下给出一个工作线程的执行流程简图:

    可以看到工作线程的执行是从mstart开始的。schedule->......->goexit0->schedule形成了一个调度循环。

    高度概括一下执行流程与调度循环:

    • mstart:主要是设置g0.stackguard0,g0.stackguard1。
    • mstart1:调用save保存callerpc和callerpc到g0.sched。然后调用schedule开始调度循环。
    • schedule:获得一个可执行的g。下面用gp代指。
    • execute(gp *g, inheritTime bool):绑定gp与当前m,状态改为_Grunning。
    • gogo(buf *gobuf):加载gp的上下文,跳转到buf.pc指向的函数。
    • 执行buf.pc指向函数
    • goexit->goexit1:调用mcall(goexit0)。
    • mcall(fn func(*g)):保存当前g(也就是gp)的上下文;切换到g0及其栈,调用fn,参数为gp。
    • goexit0(gp *g):清零gp的属性,状态_Grunning改为_Gdead;dropg解绑m和gp;gfput放入队列;schedule重新调度。
  • 相关阅读:
    正则表达式验证日期(多种日期格式)——转载
    NPOI导出Excel——精简版
    Ubuntu下安装部署.NET Core多版本环境
    搭建 nuget 私用库 BaGet
    ubuntu 监听端口
    搭建npm 私用库 verdaccio
    pgsql 性能测试 pgbench
    docker-compose 创建网络
    安装 docker-compose 启动
    dotnet sdk安装以及环境变量设置 Ubuntu 18.04 LTS
  • 原文地址:https://www.cnblogs.com/flhs/p/12682881.html
Copyright © 2011-2022 走看看