主协程(runtime.main
)的 创建
gdb main
info files
查看程序入口 在程序入口打断点run
单步调试
查看下这两个参数
ax = di , bx = si
然后分配32
字节的空间 让sp对齐16字节
之所以要按16字节对齐,是因为CPU有一组SSE指令,这些指令中出现的内存地址必须是16的倍数,将 两个参数放在栈底
di = &g0
然后给g0
分配SP - 64*1024 + 104 ~ SP
栈空间
将g0.g_stackguard0,g0.g_stackguard1 = bx
,然后将 stack.lo = bx stack.hi = sp
接下俩就是检查cpu的信息
将di = &m0
然后设置 tls
,采用寄存器传参数所以di,si...
,也就是设置 m0.tls
然后检查tls是否设置成功。
#ifdef GOARCH_amd64
#define get_tls(r) MOVQ TLS, r
#define g(r) 0(r)(TLS*1)
#endif
然后bx = fs段基地址``cx = &g0
m0.tls[0] = g0
ax = &m0
实现m 和 g0
双向绑定
将argc,argv
再拷贝一份,放入栈顶,现在栈从顶到帝的分布时 spargc,argv,argc,argv
sp + 24
argc = 1, argv =
然后 osinit
获取ncpu
我的是 8 核,r =32,然后,然后对buf[:r] 看有多少个 1
(1 << 8 )- 1= 255所以8个核
获取最大页,打开文件读取
archInit()
获得 系统信息
检查什么的
scheinit()
调度系统初始化
设置M最大个数
初始化m0以及加入全局m
分配id
加入全局m链表
P的个数,如果设置了就按照设置的来,没有按照cpu核数
(func procresize())
初始化的时候 allp 为空
所以会扩容allp
这段扩容还是很常见的。
在这一步就初始化了所有的p
初始化操作包括分配初始deferpoll切片(len = 0,cap =32),然后我们以后都会从buf拿。
然后会删除超额的
P的安置
创建runtime.main
协程
什么是systemstack
,我将汇编代码贴下来
初始化栈空间为2048
将 sched 就是我们调度所需要的信息
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
}
准备调度
这里不在引用可以看我另外一篇博客 讲解了 schedule()
这里我们重点看execute
双向绑定 g0 m0
gogo
的作用
- 把gp.sched的成员恢复到CPU的寄存器完成状态以及栈的切换;
- 跳转到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
- 启动一个
sysmon
系统监控线程,该线程负责整个程序的gc、抢占调度以及netpoll等功能的监控 - 执行
runtime
包的初始化; - 执行
main
包以及main
包import的所有包的初始化; - 执行
main.main
函数; - 从
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
就会回到我们的代码里面。
会发生一次 g->g0
栈的切换