栈初始化
主协程初始化
Golang的主协程指的是运行main函数的协程,而子协程指的是在程序运行过程中由主协程创建的协程。每个线程(m)只会有一个主协程,而子协程可能会有很多很多。
子协程和主协程在概念和内部实现上几乎没有任何区别,唯一的不同在于它们的初始栈大小不同。
我们先看看测试过程中生成的主协程堆栈示例。我测试代码中就生成了一个主协程,通过反汇编代码看到他的样子大概如下:
主协程启动
分析连接器(libinit())发现go程序的入口函数是_rt0_amd64_linux(linux amd64机器)
子协程初始化
Golang子协程堆栈在协程被创建时也一并创建,代码如下:
unc newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
_g_ := getg()
......
_p_ := _g_.m.p.ptr()
newg := gfget(_p_)
if newg == nil {
// 创建协程栈
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
......
totalSize := 4*regSize + uintptr(siz) // extra space in case of reads slightly beyond frame
if hasLinkRegister {
totalSize += ptrSize
}
totalSize += -totalSize & (spAlign - 1) // align to spAlign
// 新协程的栈顶计算,将栈的基地址减去参数占用的空间
sp := newg.stack.hi - totalSize
spArg := sp
if hasLinkRegister {
// caller's LR
*(*unsafe.Pointer)(unsafe.Pointer(sp)) = nil
spArg += ptrSize
}
...
// 设置新建协程的栈顶sp
newg.sched.sp = sp
}
// Allocate a new g, with a stack big enough for stacksize bytes.
func malg(stacksize int32) *g {
newg := new(g)
if stacksize >= 0 {
stacksize = round2(_StackSystem + stacksize)
systemstack(func() {
newg.stack, newg.stkbar = stackalloc(uint32(stacksize))
})
// 设置stackguard,在协程栈不够用时再重新申请新的栈
newg.stackguard0 = newg.stack.lo + _StackGuard
newg.stackguard1 = ^uintptr(0)
newg.stackAlloc = uintptr(stacksize)
}
return newg
}
在go1.5.1版本中,_StackMin大小被定义为2048(而在1.3.2版本中该值还是8192),也即每个协程的初始堆栈大小为2KB,相当小了。缩小该值的好处是即使创建了很多的协程也不会导致内存使用的急剧增长。
另外,在协程栈空间被分配出来后,还需要作一些其他的初始化,主要是协程栈顶的设置以及堆栈保护的设置。
栈顶的设置方法比较简单,将当前栈的起始地址减去参数占用的空间即可(注意栈是从高地址向低地址延伸的)。
栈保护的设置指的是设置一个临界点,当sp到达该临界点时认为栈空间可能会不足,需要进行栈扩容。当前版本的协程栈保护大小事640B。
以下是我对源码加入一些调试信息后打印的协程创建堆栈的详细信息:
create goroutine, stack:{stackLow: 859530534912, stackHi:859530536928, sp:859530536880, stackguard:859530535552}
根据上面的分析以及打印信息可以大致勾勒出协程初始化状态的堆栈: