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

  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重新调度。

最新文章

  1. StringUtils的isBlank与isEmply
  2. Java编程思想重点笔记(Java开发必看)
  3. C#深入.NET平台的软件系统分层开发
  4. 1-02 启动和停止Sql Sever的服务
  5. 腾讯优测干货精选|Android双卡双待适配——隐藏在数据库中的那些秘密
  6. Nginx upstream 长连接
  7. LeetCode Inorder Successor in BST
  8. rsync实现免密码操作的一种实现方式
  9. js DOM的几个常用方法
  10. IE10用video标签播放本地mp4文件失败的解决办法
  11. jBPM5 vs Actitivi
  12. 【转】Python BeautifulSoup 中文乱码解决方法
  13. Win CE 6.0 获取手持机GPS定位2----示例代码 (C#)
  14. 决策树--ID3 算法(一)
  15. 【mysql】mysql内置函数
  16. [转]Rancher 1.6 Docs
  17. acm 2015北京网络赛 F Couple Trees 主席树+树链剖分
  18. JS调用webservice服务
  19. java实现导入excel功能
  20. Visual Studio2013 配置opencv3.3.0 x64系统

热门文章

  1. ip地址与运算 ipcalc命令
  2. 百度Openrasp开源的应用运行时自我保护产品,安装教程。
  3. Pointers and Memory
  4. 图论--差分约束--HDU\HDOJ 4109 Instrction Arrangement
  5. Android Library 发布开源库 JCenter &amp; JitPack 攻略
  6. Linux中的程序和进程,PID和PPID
  7. 过滤idea一些不需要的文件和文件夹的显示,在使用svn的时候可以很方便的过滤不需要提交的文件
  8. 转载acm几何基础(2)
  9. ssh框架整合时的延迟加载问题(no session问题)的分析以及解决方案
  10. README.md编写