利用Gdb探索GoRoutine的奥秘!

主协程(runtime.main)的 创建

gdb main
info files查看程序入口 在程序入口打断点run
image.png
单步调试
image.png
查看下这两个参数
image.png
image.png
ax = di , bx = si
然后分配32字节的空间 让sp对齐16字节之所以要按16字节对齐,是因为CPU有一组SSE指令,这些指令中出现的内存地址必须是16的倍数,将 两个参数放在栈底

image.png
image.png
di = &g0
然后给g0分配SP - 64*1024 + 104 ~ SP栈空间
g0.g_stackguard0,g0.g_stackguard1 = bx,然后将 stack.lo = bx stack.hi = sp

image.png
接下俩就是检查cpu的信息
di = &m0然后设置 tls,采用寄存器传参数所以di,si...,也就是设置 m0.tls
image.pngimage.pngimage.png
然后检查tls是否设置成功。

#ifdef GOARCH_amd64
#define    get_tls(r) MOVQ TLS, r
#define    g(r)   0(r)(TLS*1)
#endif

image.png

然后bx = fs段基地址``cx = &g0 m0.tls[0] = g0 ax = &m0
image.png
实现m 和 g0双向绑定
image.png

argc,argv再拷贝一份,放入栈顶,现在栈从顶到帝的分布时 spargc,argv,argc,argv sp + 24
image.pngimage.png
argc = 1, argv = image.png
然后 osinit获取ncpu

image.png
我的是 8 核,r =32,然后image.png,然后对buf[:r] 看有多少个 1
(1 << 8 )- 1= 255所以8个核
获取最大页,打开文件读取
image.png
image.png
archInit()
获得 系统信息
image.png
image.png
检查什么的

scheinit() 调度系统初始化
image.png 设置M最大个数
初始化m0以及加入全局m
image.png
image.png分配id
image.png加入全局m链表

P的个数,如果设置了就按照设置的来,没有按照cpu核数
image.png
(func procresize())初始化的时候 allp 为空所以会扩容allp 这段扩容还是很常见的。
image.png
在这一步就初始化了所有的p
image.png
初始化操作包括分配初始deferpoll切片(len = 0,cap =32),然后我们以后都会从buf拿。
image.pngimage.pngimage.png

然后会删除超额的
image.png
image.png
P的安置
image.png

创建runtime.main协程
image.png
image.png
image.png

image.png
什么是systemstack,我将汇编代码贴下来
image.png
image.png

初始化栈空间为2048
image.png
将 sched 就是我们调度所需要的信息
image.png

type gobuf struct {
	// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
	//
	// ctxt is unusual with respect to GC: it may be a
	// heap-allocated funcval, so GC needs to track it, but it
	// needs to be set and cleared from assembly, where it's
	// difficult to have write barriers. However, ctxt is really a
	// saved, live register, and we only ever exchange it between
	// the real register and the gobuf. Hence, we treat it as a
	// root during stack scanning, which means assembly that saves
	// and restores it doesn't need write barriers. It's still
	// typed as a pointer so that any other writes from Go get
	// write barriers.
	sp   uintptr
	pc   uintptr
	g    guintptr
	ctxt unsafe.Pointer
	ret  uintptr
	lr   uintptr
	bp   uintptr // for framepointer-enabled architectures
}

image.png
image.png

准备调度

image.png
image.png
image.png
这里不在引用可以看我另外一篇博客 讲解了 schedule()
这里我们重点看execute 双向绑定 g0 m0
gogo的作用

  1. 把gp.sched的成员恢复到CPU的寄存器完成状态以及栈的切换;
  2. 跳转到gp.sched.pc所指的指令地址(runtime.main)处执行。
// 将 g0.m.curg = g 也就是 m现在执行g
func execute(gp *g, inheritTime bool) {
	_g_ := getg() // g0

	// Assign gp.m before entering _Grunning so running Gs have an
	// M.
	_g_.m.curg = gp // newg
	gp.m = _g_.m // 双向绑定
	...
	gogo(&gp.sched) // 上下文切换
}

TEXT runtime·gogo(SB), NOSPLIT, $16-8
    MOVQ   buf+0(FP), BX    // gobuf,buf = &gp.sched 
    MOVQ   gobuf_g(BX), DX  //DX = gp.sched.g

    //检查gp.sched.g是否是nil,如果是nil进程会crash死掉
    MOVQ   0(DX), CX     // make sure g != nil

    get_tls(CX)

    //把要运行的g的指针放入线程本地存储,这样后面的代码就可以通过线程本地存储
    //获取到当前正在执行的goroutine的g结构体对象,从而找到与之关联的m和p
    MOVQ   DX, g(CX)

    // 把CPU的SP寄存器设置为sched.sp,完成了栈的切换 sp 前压入了 goexit()
    MOVQ   gobuf_sp(BX), SP   // restore SP 
    // 设置恢复调度上下文时需要的寄存器
    MOVQ   gobuf_ret(BX), AX 
    MOVQ   gobuf_ctxt(BX), DX
    MOVQ   gobuf_bp(BX), BP
    // 清空sched的值,因为我们已把相关值放入CPU对应的寄存器了,不再需要,这样做可以少gc的工作量
    MOVQ   $0, gobuf_sp(BX)   // clear to help garbage collector
    MOVQ   $0, gobuf_ret(BX)
    MOVQ   $0, gobuf_ctxt(BX)
    MOVQ   $0, gobuf_bp(BX)

    //把sched.pc值放入BX寄存器 pc = start.pc 原来是 goexit 但是 调用newproc. gostartfn(&sched,fn) 将它复制为 runtime.main

    MOVQ   gobuf_pc(BX), BX
    //JMP把BX寄存器的包含的地址值放入CPU的IP寄存器,于是,CPU跳转到该地址继续执行指令
    JMP BX

runtime.main

  1. 启动一个sysmon系统监控线程,该线程负责整个程序的gc、抢占调度以及netpoll等功能的监控
  2. 执行runtime包的初始化;
  3. 执行main包以及main包import的所有包的初始化;
  4. 执行main.main函数;
  5. main.main函数返回后调用exit系统调用退出进程;(也就是我们的函数哈哈哈,结束了)

非主协程的创建(go func())

go tool compile -+ -L -S main.go > a.asm

package main

import (
    "fmt"
)

func main() {
    go test()
    fmt.Println("Hello World!")
}

func test(){
}

===============================================

0x0028 00040 (main.go:6)        LEAQ    "".test·f(SB), AX
0x002f 00047 (main.go:6)        MOVQ    AX, 8(SP)
0x0034 00052 (main.go:6)        PCDATA  $0, $0
0x0034 00052 (main.go:6)        PCDATA  $1, $0
0x0034 00052 (main.go:6)        CALL    runtime.newproc(SB)

返现会将 ax = test , 放入栈后,然后调用 newproc
gcenable()和doInit()都会开启goroutine前期会开启3个,然后再次c就会回到我们的代码里面。
image.png
会发生一次 g->g0栈的切换
image.png

引用连接

  1. https://juejin.cn/post/6986566386176753694#heading-11
posted @ 2022-12-17 22:04  来个半马庆祝下  阅读(104)  评论(0)    收藏  举报