golang-main的执行

81 阅读3分钟

golang-main的执行

参考链接

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 
go程序启动流程.png

非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
非maingoroutine启动.png

总结

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为例子。

schedule循环调用.png

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 的基本工作