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

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

Golang源码学习:调度逻辑(三)工作线程的执行流程与调度循环的更多相关文章

  1. Golang源码学习:调度逻辑(二)main goroutine的创建

    接上一篇继续分析一下runtime.newproc方法. 函数签名 newproc函数的签名为 newproc(siz int32, fn *funcval) siz是传入的参数大小(不是个数):fn ...

  2. Spring源码学习-容器BeanFactory(三) BeanDefinition的创建-解析Spring的默认标签

    写在前面 上文Spring源码学习-容器BeanFactory(二) BeanDefinition的创建-解析前BeanDefinition的前置操作中Spring对XML解析后创建了对应的Docum ...

  3. Java 源码学习系列(三)——Integer

    Integer 类在对象中包装了一个基本类型 int 的值.Integer 类型的对象包含一个 int 类型的字段. 此外,该类提供了多个方法,能在 int 类型和 String 类型之间互相转换,还 ...

  4. async-validator 源码学习笔记(三):rule

    系列文章: 1.async-validator 源码学习(一):文档翻译 2.async-validator 源码学习笔记(二):目录结构 rule 主要实现的是校验规则,文件结构为下图: 一.rul ...

  5. Golang源码学习:调度逻辑(四)系统调用

    Linux系统调用 概念:系统调用为用户态进程提供了硬件的抽象接口.并且是用户空间访问内核的唯一手段,除异常和陷入外,它们是内核唯一的合法入口.保证系统的安全和稳定. 调用号:在Linux中,每个系统 ...

  6. Dubbo源码学习总结系列三 dubbo-cluster集群模块

    Dubbo集群模块的目的是将集群Invokers构造一个透明的Invoker对象,其中包含了容错机制.负载均衡.目录服务(服务地址集合).路由机制等,为RPC层提供高可用.高并发.自动发现.可治理的S ...

  7. Golang源码学习:使用gdb调试探究Golang函数调用栈结构

    本文所使用的golang为1.14,gdb为8.1. 一直以来对于函数调用都仅限于函数调用栈这个概念上,但对于其中的详细结构却了解不多.所以用gdb调试一个简单的例子,一探究竟. 函数调用栈的结构(以 ...

  8. Golang源码学习:监控线程

    监控线程是在runtime.main执行的时候在系统栈中创建的,监控线程与普通的工作线程区别在于,监控线程不需要绑定p来运行. 监控线程的创建与启动 简单的调用图 先给出个简单的调用图,好心里有数,逐 ...

  9. JDK源码学习--String篇(三) 存储篇

    在进一步解读String类时,先了解下内存分配和数据存储的. 数据存储 1.寄存器:最快的存储区,位于处理器的内部.由于寄存器的数量有限,所以寄存器是按需分配. 2.堆栈:位于RAM中,但是通过堆栈指 ...

随机推荐

  1. ACM-ICPC 2019 山东省省赛 A Calandar

    这个题,呃完全的送分题,签到题,一周只有五天,一年12个月,一个月30天,公式为((year1-year2)*360%5+(month1-month2)*30%5+day1-day2+初始星期)%5, ...

  2. 数学--数论--HDU 1098 Ignatius's puzzle (费马小定理+打表)

    Ignatius's puzzle Problem Description Ignatius is poor at math,he falls across a puzzle problem,so h ...

  3. The Preliminary Contest for ICPC Asia Xuzhou 2019 徐州网络赛 XKC's basketball team

    XKC , the captain of the basketball team , is directing a train of nn team members. He makes all mem ...

  4. 题解 CF588A 【Duff and Meat】

    题意 有一个人,想吃 $n$ 天肉,第 $i$ 天需要吃 $a[i]$ 块肉,第 $i$ 天超市肉价为每块 $b[i]$ 元,买来的肉可以留到后面吃,求这个人在每天都吃到肉的情况下花费的最小值. 题目 ...

  5. Servlet 教程——检视阅读

    Servlet 教程--检视阅读 参考 Servlet教程--菜鸟--蓝本 Servlet教程--w3cschool Servlet教程--易百 servlet依赖maven依赖: <!--se ...

  6. 内存迟迟下不去,可能你就差一个GC.Collect

    一:背景 1. 讲故事 我们有一家top级的淘品牌店铺,为了后续的加速计算,在程序启动的时候灌入她家的核心数据到内存中,灌入完成后内存高达100G,虽然云上的机器内存有256G,然被这么划掉一半看着还 ...

  7. jQuery简单竖排手风琴折叠菜单代码

    项目需求1.刚开始只显示,每个标题, 2.让每个 li列表隔行换色 3.当我点击某个标题时,下面的列表会缓慢的展开,其他列表展开的内容会收起 <!DOCTYPE html> <htm ...

  8. B. Sorted Adjacent Differences(思维构造)

    \(给出n个数字,要求构造一个由这n个数组成的序列,使得|a_1-a_2|<=|a_2-a_3|...<=|a_{n-1}-a_n|\) \(排序后,从数列中间取个数,然后从左右分别循环取 ...

  9. Centos7下tomcat关闭异常问题

    目录 出错原因 解决方法 出错原因 ​ 在阿里云服务器上买的轻量级应用服务器,装上了tomcat,访问tomcat自带的首页,8080端口,第一次启动成功了,关闭也正常,但在服务器重启后,或者第二次启 ...

  10. Spring Cloud学习 之 Spring Cloud Ribbon(负载均衡策略)

    文章目录 AbstractLoadBalancerRule: RandomRule: RoundRobinRule: RetryRule: WeightedResponseTimeRule: 定时任务 ...