引言:
基于golang1.14 (公司当前使用的go版本就是1.14 正好1.14之后开始支持抢占式调度,不失为分析go源码的一个good version)的源码分析下golang的启动过程和后续的调度循环是如何实现~涉及到linux虚拟内存相关,plan9汇编相关,调度相关知识
** Go大法好准备过程
大概记录下后面分析过程需要用到的相关知识点
1. 寄存器分类
通用寄存器 AX BX 等通用寄存器
程序计数器 IP寄存器 执行下一条执行的指令的地址
段寄存器 fs,gs 附加段寄存器
cs 代码段寄存器 ss 堆栈段寄存器 ds 数据段寄存器
另外这两个寄存器也是后面需要使用的,特别列出来 rsp 栈顶寄存器 存放函数调用栈的栈顶地址 rbp 栈基址寄存器 存放栈帧的起始地址
2. 汇编命令分类
常用汇编命令 方便看懂启动时的汇编数据
- MOV 用于将字面值移动到寄存器、字面值移到内存、寄存器之间的数据传输、寄存器和 内存之间的数据传输
(B W D Q 1 2 4 8)
ADD 加法正常用于栈上移
SUB 减法正常用于栈下移
LEA 取地址并加载到对应的寄存器
CALL 调用函数
RET 函数返回
JMP 跳转语句
3.虚拟内存
简单的使用32位的操作系统,对应虚拟内存分布来说明程序运行时的虚拟内存的段页式管理
4GB的虚拟内存空间,其中1GB为内核空间,用户态3GB 从高地址往地址展示如图
Go启动过程分析
1.启动中的对象分析
g
type g struct {
// Stack parameters.
stack stack // offset known to runtime/cgo
//TODO 用于检查栈溢出 实现栈的自动伸缩 抢占调度的时候也会用到 stackguard0
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
//TODO 当前绑定的m
m *m // current m; offset known to arm liblink
//TODO 保存调度相关的信息 寄存器的值和栈相关 用于保存调度上下文
sched gobuf
goid int64
schedlink guintptr //TODO 明显是个链表结构 让全局运行队列中的g形成一个链表
// 抢占式调度的标志 需要抢占式调度 为true 触发基于信号的抢占式调度
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
preemptStop bool // transition to _Gpreempted on preemption; otherwise, just deschedule
preemptShrink bool // shrink stack at synchronous safe point
}
去掉了和本次分析启动无关的代码,stack为goroutine自身的私有栈,stackguard0,stackguard1用来检查栈是否溢出,方便实现栈的自动伸缩。m为当前g绑定的m,sched里保存调度相关的信息,寄存器的值和栈相关的值,用在调度时保存上下文,schedlink以链表方式保存着全局运行队列中的g,preempt抢占式调度的标示,true代表启动基于signal的抢占式调度。
m
type m struct {
//TODO g0 需要记录工作线程使用的栈信息,执行调度时需要使用这个栈
g0 *g // goroutine with scheduling stack
morebuf gobuf // gobuf arg to morestack
divmod uint32 // div/mod denominator for arm - known to liblink
//信号处理的g
gsignal *g // signal-handling g
goSigStack gsignalStack // Go-allocated signal handling stack
sigmask sigset // storage for saved signal mask
//TODO 通过tls 实现 m结构体对象 与 工作线程之间的绑定
tls [6]uintptr // thread-local storage (for x86 extern register)
mstartfn func()
//TODO 正在运行的goroutine对象
curg *g // current running goroutine
caughtsig guintptr // goroutine running during fatal signal
//TODO 与当前工作线程绑定的p Processor
p puintptr // attached p for executing go code (nil if not executing go code)
nextp puintptr
oldp puintptr // the p that was attached before executing a syscall
//TODO 没有goroutine需要运行,工作线程在park上睡眠,其余线程可以通过park进行唤醒。
park note
//TODO 链表格式 记录工作线程
alllink *m // on allm
schedlink muintptr
}
p
type p struct {
id int32
status uint32 // one of pidle/prunning/...
m muintptr // back-link to associated m (nil if idle)
// Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
goidcache uint64
goidcacheend uint64
//本地 goroutine 运行队列
runqhead uint32
runqtail uint32
runq [256]guintptr //数据实现的循环队列 ringBuffer 类似
runnext guintptr
}
schedt
type schedt struct {
// accessed atomically. keep at top to ensure alignment on 32-bit systems.
goidgen uint64
lastpoll uint64 // time of last network poll, 0 if currently polling
pollUntil uint64 // time to which current poll is sleeping
lock mutex
//TODO 空闲工作线程链表
midle muintptr // idle m's waiting for work
//TODO 空闲工作线程数量
nmidle int32 // number of idle m's waiting for work
nmidlelocked int32 // number of locked m's waiting for work
mnext int64 // number of m's that have been created and next M ID
maxmcount int32 // maximum number of m's allowed (or die)
nmsys int32 // number of system m's not counted for deadlock
nmfreed int64 // cumulative number of freed m's
ngsys uint32 // number of system goroutines; updated atomically
//TODO 空闲p组成的链表
pidle puintptr // idle p's
//TODO 空闲的p的数量
npidle uint32
nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
//TODO 全局运行队列 goroutine
// Global runnable queue.
runq gQueue
runqsize int32
//TODO 缓存已经退出的goroutine对象 避免每次都需要进行g的内存分配 复用对象
// Global cache of dead G's.
gFree struct {
lock mutex
stack gList // Gs with stacks
noStack gList // Gs without stacks
n int32
}
}
全局对象
var (
//TODO 全局对象 所有m构成的一个链表 包括m0
allm *m
allp []*p // len(allp) == gomaxprocs; may change at safe points, otherwise immutable
allpLock mutex // Protects P-less reads of allp and all writes
gomaxprocs int32
ncpu int32
forcegc forcegcstate
//TODO 全局调度器对象
sched schedt
newprocs int32
)
2.从入口开始
主要做了一下几件事情: 1. 初始化全局变量 allp 2. 创建并初始化 nprocs 个 p 结构体对象 保存在 allp 切片之中 3. m0.p = allp[0],allp[0].m = m0 4. 除了 allp[0] 之外的所有 p 放入到全局变量 sched 的 pidle 空闲队列
从汇编的入口函数到现在的schedinit函数做的事情 简单概括来说
- 分配好g0的栈空间 把m0的tls和g0绑定,方便寻址
- m0放入allm中
- 新建好procs梳理的p 放入allp中
- 建立好m0 p0 g0之间的绑定关系 并初始化全局变量 等待下一步执行过程
我们从入口开始一步步分析
通过gdb调试找到入口函数 rt0_linux_amd64.s
//TODO 程序的入口 gdb 发现
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
其中NOSPLIT代表自己管理栈大小,不进行栈溢出的检查 之后JMP到下一个处理单元
_rt0_amd64 ===> asm_amd64.s
//TODO 把操作系统内核传递过来的参数 argc 和 argv 分别放入 DI SI 寄存器中 LEAQ是指放入的是argv的地址
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB) //TODO 跳转执行
继续跳转 rt0_go 同文件内
TEXT runtime·rt0_go(SB),NOSPLIT,$0
MOVQ DI, AX // argc //argc 拷贝到 AX 8byte
MOVQ SI, BX // argv //argv的地址拷贝到BX 8byte
SUBQ $(4*8+7), SP // 2args 2auto //CQS 栈顶往下移动39 按照16字节对齐
ANDQ $~15, SP //TODO 最低4位全部置为0 这样可以得到对齐 16字节
MOVQ AX, 16(SP)
MOVQ BX, 24(SP)
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
//TODO 分配栈空间给g0 ==> DI 寄存器
MOVQ $runtime·g0(SB), DI
//TODO BX = SP -64*1024+104
LEAQ (-64*1024+104)(SP), BX
//TODO stackguard0 = SP -64*1024+104
MOVQ BX, g_stackguard0(DI)
//TODO stackguard1 = SP -64*1024+104
MOVQ BX, g_stackguard1(DI)
//TODO g0.stack.lo = SP -64*1024+104
MOVQ BX, (g_stack+stack_lo)(DI)
//TODO g0.stack.hi = SP
MOVQ SP, (g_stack+stack_hi)(DI)
//TODO JMP 检测CPU代码
...
...
...
//TODO JMP 检测CPU代码
//TODO 初始化m的tls di = &m0.tls 把m0的tls成员地址存储到DI
LEAQ runtime·m0+m_tls(SB), DI
//TODO 调用settls 设置线程本地存储 之后可以通过fs端寄存器进行访问 找到m0.tls DI绑定
CALL runtime·settls(SB)
// store through it, to make sure it works
//TODO 获取fs段基址 并放入BX寄存器中 其实就是m0.tls[1]的地址 ps: get_tls为编译器生成代码
get_tls(BX)
//TODO m0.tls[0] g是编译器实现 地址-8
MOVQ $0x123, g(BX)
//TODO AX = m0.tls[0]
MOVQ runtime·m0+m_tls(SB), AX //CQS 检测线程本地存储是否初始化成功
CMPQ AX, $0x123
//TODO 跳跃两个地址指令
JEQ 2(PC)
CALL runtime·abort(SB)
ok:
// set the per-goroutine and per-mach "registers"
//TODO fs段基址放到bx m0.tls[1]
get_tls(BX)
//TODO g0的地址放到CX
LEAQ runtime·g0(SB), CX
//TODO m0.tls[0] = &g0 地址赋值
MOVQ CX, g(BX)
//TODO 把m0的地址放到AX
LEAQ runtime·m0(SB), AX
// save m->g0 = g0
//TODO m0.g0 = &g0
MOVQ CX, m_g0(AX)
// save m0 to g0->m
//TODO g0.m0 = &m0
MOVQ AX, g_m(CX)
//CQS fs ==> tls[1] ==> g() ==> tls[0] ==> g0 ==> g0.m0 = &m0 ==> m0.g0 = &g0
CLD // convention is D is always left cleared
CALL runtime·check(SB)
MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
//TODO 参数初始化 栈空余的16利用
CALL runtime·args(SB)
//TODO 初始化系统核心数
CALL runtime·osinit(SB)
//TODO 开始初始化调度器
CALL runtime·schedinit(SB)
上面一大堆代码,我们一步一步来
主要从rt0_go开始
- 首先栈顶指针下移 39 之后 与 15的取反做AND 等于做了16位对齐,之后把argv的地址存在 SP+24,argc存在SP+16, SP 与 argc 中间多出了 16 个字节的空位
- 初始化g0栈
- 主线程绑定m0
- 初始化m0
- 先把参数拷贝到sp+16的位置
- runtime·args 初始化系统参数
- runtime·osinit 初始化系统核心数,将全局变量 ncpu 初始化的核心数
- schedinit 调度器初始化
我们重点来分析schedinit ~
func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
//TODO 获取当前运行的goroutine指针
_g_ := getg()
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit()
}
//TODO 最多启动10000个工作线程(M)
sched.maxmcount = 10000
tracebackinit()
moduledataverify()
// 栈初始化
stackinit()
// 内存分配初始化
mallocinit()
fastrandinit() // must run before mcommoninit
// 主要把m0加入allm 全局链表 _g_.m 刚刚在汇编代码里已经完成绑定
mcommoninit(_g_.m)
cpuinit() // must run before alginit
alginit() // maps must not be used before this call
modulesinit() // provides activeModules
typelinksinit() // uses maps, activeModules
itabsinit() // uses activeModules
msigsave(_g_.m)
initSigmask = _g_.m.sigmask
//处理命令行参数
goargs()
//处理全局变量
goenvs()
parsedebugvars()
//垃圾回收器初始化
gcinit()
sched.lastpoll = uint64(nanotime())
//TODO 根据GOMAXPROCS的全局变量设置 获取初始化的p个数 processor
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
//TODO 初始化的时候一定是返回空
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
// For cgocheck > 1, we turn on the write barrier at all times
// and check all pointer writes. We can't do this until after
// procresize because the write barrier needs a P.
if debug.cgocheck > 1 {
writeBarrier.cgo = true
writeBarrier.enabled = true
for _, p := range allp {
p.wbBuf.reset()
}
}
if buildVersion == "" {
// Condition should never trigger. This code just serves
// to ensure runtime·buildVersion is kept in the resulting binary.
buildVersion = "unknown"
}
if len(modinfo) == 1 {
// Condition should never trigger. This code just serves
// to ensure runtime·modinfo is kept in the resulting binary.
modinfo = ""
}
}
我们还是一步步来分析
- 首先获取当前运行的goroutine的指针也就是g0 获取方式编译器实现
类似从 fs ==> tls[1] ==> g() ==> tls[0] ==> g0 ==> g0.m0 = &m0 ==> m0.g0 = &g0
从fs段寄存器出发 找到 m0.tls[1] ,地址-8后得到 tls[0] 而 tls[0]正好指向g0获取到
g0的地址
- 正常进行初始化 栈初始化,内存分配初始化,然后执行mcommoninit
//TODO 主要把m0加入allm 全局链表
func mcommoninit(mp *m) {
//TODO 初始化过程 g0
_g_ := getg()
// g0 stack won't make sense for user (and is not necessary unwindable).
if _g_ != _g_.m.g0 {
callers(1, mp.createstack[:])
}
//TODO 对全局变量进行操作 加锁
lock(&sched.lock)
if sched.mnext+1 < sched.mnext {
throw("runtime: thread ID overflow")
}
mp.id = sched.mnext // 给id复制
sched.mnext++ // m0.id = 0 之后递增
checkmcount() //检查数量限制 默认 10000
//TODO 初始化random
mp.fastrand[0] = uint32(int64Hash(uint64(mp.id), fastrandseed))
mp.fastrand[1] = uint32(int64Hash(uint64(cputicks()), ^fastrandseed))
if mp.fastrand[0]|mp.fastrand[1] == 0 {
mp.fastrand[1] = 1
}
//TODO 创建gsignal 简单从堆上分配一个g结构体对象 设置好栈 返回
mpreinit(mp)
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
}
// Add to allm so garbage collector doesn't free g->m
// when it is just in a register or thread-local storage.
//TODO 挂在m到全局链表中
mp.alllink = allm
// NumCgoCall() iterates over allm w/o schedlock,
// so we need to publish it safely.
atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
//m.allink ==> alllink
unlock(&sched.lock)
// Allocate memory to hold a cgo traceback if the cgo call crashes.
if iscgo || GOOS == "solaris" || GOOS == "illumos" || GOOS == "windows" {
mp.cgoCallers = new(cgoCallers)
}
}
主要就是初始化全局的allm 并把m的alllink和m挂在一起
- 继续处理参数初始化,全局变量初始化,垃圾回收器初始化
- procresize 开始初始化p 根据procs数量进行,默认是cpu的数量
func procresize(nprocs int32) *p {
//TODO 系统初始化时 gomaxprocs = 0
old := gomaxprocs
if old < 0 || nprocs <= 0 {
throw("procresize: invalid arg")
}
if trace.enabled {
traceGomaxprocs(nprocs)
}
// update statistics
now := nanotime()
if sched.procresizetime != 0 {
sched.totaltime += int64(old) * (now - sched.procresizetime)
}
sched.procresizetime = now
// Grow allp if necessary.
//TODO 初始化全局队列为空 所以肯定要进行初始化
if nprocs > int32(len(allp)) {
// Synchronize with retake, which could be running
// concurrently since it doesn't run on a P.
//TODO 对全局变量操作需要加锁
lock(&allpLock)
if nprocs <= int32(cap(allp)) {
allp = allp[:nprocs]
} else {
nallp := make([]*p, nprocs)
// Copy everything up to allp's cap so we
// never lose old allocated Ps.
//TODO 当发生调整时保证 旧值不丢
copy(nallp, allp[:cap(allp)])
allp = nallp
}
unlock(&allpLock)
}
// initialize new P's
for i := old; i < nprocs; i++ {
pp := allp[i]
//TODO 如果取到全局队列的值为nil则进行创建
if pp == nil {
pp = new(p)
}
pp.init(i)
//TODO 类似于allm 把p 放入到allp内
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
//TODO 初始化时获取g0
_g_ := getg()
//TODO 初始化时默认执行下面的分支 因为m和p未绑定
if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {
// continue to use the current P
_g_.m.p.ptr().status = _Prunning
_g_.m.p.ptr().mcache.prepareForSweep()
} else {
// release the current P and acquire allp[0].
//
// We must do this before destroying our current P
// because p.destroy itself has write barriers, so we
// need to do that from a valid P.
if _g_.m.p != 0 {
if trace.enabled {
// Pretend that we were descheduled
// and then scheduled again to keep
// the trace sane.
traceGoSched()
traceProcStop(_g_.m.p.ptr())
}
_g_.m.p.ptr().m = 0
}
_g_.m.p = 0
_g_.m.mcache = nil
//TODO 取出第0个
p := allp[0]
p.m = 0
//TODO 修改状态为 空闲
p.status = _Pidle
//TODO 管理p和m0 状态到running **
acquirep(p)
if trace.enabled {
traceGoStart()
}
}
// release resources from unused P's
//TODO 调整P个数
for i := nprocs; i < old; i++ {
p := allp[i]
p.destroy()
// can't free P itself because it can be referenced by an M in syscall
}
// Trim allp.
if int32(len(allp)) != nprocs {
lock(&allpLock)
allp = allp[:nprocs]
unlock(&allpLock)
}
//TODO for循环把所有的空闲的p放到空闲链表中
//TODO 把非空闲的放入了runnablePs 并返回 等待调度 p
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
p := allp[i]
if _g_.m.p.ptr() == p { //初始化时 p0绑定了m0不能放入
continue
}
//TODO 状态修改为idle
p.status = _Pidle
//如果P的本地运行G队列为空
if runqempty(p) {
pidleput(p)
} else {
p.m.set(mget())
p.link.set(runnablePs)
runnablePs = p
}
}
//TODO 为了随机偷取 随机数初始化
stealOrder.reset(uint32(nprocs))
var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
return runnablePs
}
这个函数本身是为了resize p存在的,在g0初始化时,给全局的allp加入了p,并且把编号p0的第一个p和m0绑定,并把状态修改为running 最后返回可以运行的p链表 初始化时为空~