golang-main的执行
参考链接
- go语言调度器初始化
- go语言调度器之调度main goroutine
- 非main goroutine的退出和调度循环
- 详解Go语言调度循环源码实现
- schedule 循环如何运转
- 工作线程的唤醒和创建
GOT
通过对执行流程了解,可以对gmp的理解更加深印象。
- 对main启动的流程认识。
- 非main goroutine的退出执行流程,
- 循环调度流程
main-goroutine
使用dlv在linux下面调试就能看到程序的启动入口,使用dlv调试golang的程序入口
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
// 执行下面调试步骤,就能看到程序的入口 TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
> go build main.go
> dlv exec main
> r
> list
> si
> b runtime.rt0_go
> c
非main-goroutine exit
package main
import (
"fmt"
)
func g2(n int, ch chan int) {
ch <- n * n
}
func main() {
ch := make(chan int)
go g2(100, ch)
fmt.Println(<-ch)
}
// 执行下面命令,打印出 g2的调用栈信息
dlv debug main.go
b g2
bt # 打印调用栈信息就能发现exit函数
0 0x00000001042ddd30 in main.g2
at ./main.go:12
1 0x0000000104298b64 in runtime.goexit
at /Users/xupenghao/.gvm/gos/go1.17.8/src/runtime/asm_arm64.s:1133
总结
g和g0的切换
- gogo:完成 g0到g的切换
- mcall:完成 g到g0的切换
循环调度(g0栈复用)
- schedule 调用是永远不会返回的,schedule的无返回循环调用可能导致我们的g0栈空间最终会消耗尽的,所以我们要复用g0栈.
schedule()->execute()->gogo()->g2()->goexit()->goexit1()->mcall()->goexit0()->schedule()
在mstat1中我们 _g_.sched.pc = getcallerpc()
_g_.sched.sp = getcallersp()
保存了g0的调度信息
在mcall中完成 g->g0的栈的切换,此时并指向了,g0->sched.sp的位置(mstart1执行完的返回的地址),复用g0栈空间,执行schedule
TEXT runtime·mcall(SB), NOSPLIT, $0-8
#恢复g0的栈顶指针到CPU的rsp积存,这一条指令完成了栈的切换,从g的栈切换到了g0的栈
MOVQ (g_sched+gobuf_sp)(SI), SP# rsp = g0->sched.sp
mcall(fn) ,传递的fn有多种,这里只是举了,goexit0为例子。
Q&A
协程上下文切换
- 协程经历 g->g0->g的过程,完成一次调度循环,切换的过程我们叫做,协程的上下文切换
什么是g0?
- 协程g0运行在操作系统线程栈上,其主要作用是 执行一些runtime代码运行,
- 协程g0栈会被复用,执行流程相对固定(mstart->mstart1->schedule->execute->gogo)
- g->调度执行->g0->切换->g->调度执行
全局的g0和m0
global-m0和global-g0在程序启动时两者绑定了关系
- global-m0
进程启动的第一个线程,即主线程和全局的m0绑定,在go程序启动时候,设置了,global-m0,global-g0的关系和本地线程存储。
一个go进程只有给m0,其他的m都是runtime自己创建的。newm
- global-g0
在程序启动的时候,分配了g0的栈的大小,并和 globa-m0绑定,其他m.g0都是在runtime阶段分配的。
- m.g0(每个m都有一个g0)
newm,每次newm,都会重新分配m.g0的栈空间的。并设置tls,最后执行 mstart()函数。
func newm(fn func(), _p_ *p, id int64) {
mp := allocm(_p_, fn, id)
newm1(mp)
}
func newm1(mp *m) {
newosproc(mp)
}
func newosproc(mp *m) {
ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))
}
为什么要把g和m绑定在一起?
协程你可以认为是一个轻量级的线程,运行在线程上的代码块,当线程被操作系统调度到cpu执行时,就执行该线程中代码块协程。
所以要协程要想执行则必须绑定一个线程m
tls线程本地存储?
线程的本地存储,其实就是,线程私有的全局变量。简单的说就是每个线程都有一个全局变量的副本,修改后彼此之间互不影响。
这里指的是工作线程绑定的 m0
rip是什么?
指令寄存器,指令寄存器用于存放下一条指令的地址,CPU 的工作模式,就是从 rip
指向的内存地址取一条指令,然后执行这条指令,同时 rip
指向下一条指令,如此循环,就是 CPU 的基本工作