栈扩大
stack.go中的常量,用于检查goroutinue的状态
uintptrMask = 1<<(8*sys.PtrSize) - 1
// Goroutine preemption request.
// Stored into g->stackguard0 to cause split stack check failure.
// Must be greater than any real sp.
// 0xfffffade in hex.
stackPreempt = uintptrMask & -1314
// Thread is forking.
// Stored into g->stackguard0 to cause split stack check failure.
// Must be greater than any real sp.
stackFork = uintptrMask & -1234
当检查到栈不足够的时候,调用runtime·morestack_noctxt,检查完成之后将重新从函数其实处运行,再进行一次栈check,因为进入newstack有可能并不会增长栈。
0x0000 00000 (x.go:19) MOVQ TLS,CX 0x0009 00009 (x.go:19) MOVQ (CX)(TLS*2),CX 0x0010 00016 (x.go:19) LEAQ -160(SP),AX 0x0018 00024 (x.go:19) CMPQ AX, 16(CX) 0x001c 00028 (x.go:19) JLS 637
... 0x027d 00637 (x.go:19) CALL runtime.morestack_noctxt(SB) 0x0282 00642 (x.go:19) JMP 0
这个函数中:
- 检查是否是从g或者是gsignal中调用的这个函数,如果是,则报错
- 假设调用调用morestack_noctx的函数为f,
- 首先保存f的caller’s SP到m的morebuf 还有PC还有当前g
- 然后保存f的SP PC和BP以及g到当前goroutinue的gobuf
- morebuf主要用于在morestack中出现throw的时候可以正常打印调用栈
- 切换到m.g0的栈上调用runtime.newstack
runtime.newstack
栈扩容安全性检查
if thisg.m.morebuf.g.ptr().stackguard0 == stackFork {
throw("stack growth after fork")
}
不能再fork之后调用morestack,fork就是栈还没有初始化完的时候,会把栈顶设置为stackFork。
如果morebuf中的g不等于m.curg,报错
gp := thisg.m.curg
// Write ctxt to gp.sched. We do this here instead of in
// morestack so it has the necessary write barrier.
gp.sched.ctxt = ctxt
if thisg.m.curg.throwsplit {
// Update syscallsp,syscallpc in case traceback uses them.
morebuf := thisg.m.morebuf
gp.syscallsp = morebuf.sp
gp.syscallpc = morebuf.pc
print("runtime: newstack sp=",hex(gp.sched.sp)," stack=[",hex(gp.stack.lo),",",hex(gp.stack.hi),"]\n","\tmorebuf={pc:",hex(morebuf.pc)," sp:",hex(morebuf.sp)," lr:",hex(morebuf.lr),"}\n","\tsched={pc:",hex(gp.sched.pc),hex(gp.sched.lr)," ctxt:",gp.sched.ctxt,"}\n")
traceback(morebuf.pc,morebuf.sp,morebuf.lr,gp)
throw("runtime: stack split at bad time")
}
不能在throwsplit为ture的时候调用morestack,这个标志表示现在不能进行栈的扩容
判断是否是真的需要扩容
接下来清空m.morebuf
这里判断是不是因为设置了stackPreempt而进去的morestack,如果是,直接把g.stackguard0重新设置为gp.stack.lo + _StackGuard
然后切换到goroutinue继续执行,这里是因为如果一个goroutinue需要被抢占,会设置两个状态 g.stackguard0 = stackPreempt和g.preempt = true
这里当g.stackguard0 == stackpreempt的时候,
如果有锁,内存分配,条件的时候,不进行goroutinue切换
后面如果gp.preemptscan为true,也不进行切换
否则调用gopreempt_m
进行切换
这个函数最终调用schedule和execute来切换到一个新的goroutinue上运行。
扩容
copystack(gp,uintptr(newsize),true)
casgstatus(gp,_Gcopystack,_Grunning)
gogo(&gp.sched)
调用copystack分配并拷贝栈,然后继续gogo切换到goroutinue继续执行
也就是newstack不但实现了栈的扩容,同时还实现了go的抢占式调度算法
一个小小的疑问及答案不
如果刚好在goroutinue的栈即将不够用的时候设置了stackpreempt,那么是不是有可能因为stackpreempt而没有进行栈扩容而导致栈溢出呢?
关于这个疑问的答案是不会导致栈溢出,请看这里:
0x027d 00637 (x.go:19) CALL runtime.morestack_noctxt(SB)
0x0282 00642 (x.go:19) JMP 0
在runtime.morestack_noctext调用之后会JMP0,也就是如果是因为stackpreempt而进入newstack,那么当goroutinue继续运行之后,会返回函数起始处,重新检查sp和栈顶,这时候栈顶已经被newstack恢复。
栈收缩
收缩栈是在mgcmark.go中触发的,主要是在scanstack和markrootFreeGStacks函数中,也就是垃圾回收的时候会根据情况收缩栈
// Maybe shrink the stack being used by gp.
// Called at garbage collection time.
// gp must be stopped,but the world need not be.
shrinkstack 收缩栈在必要的时候
- 如果这个g是Gdead状态,则会释放栈空间
- 如果已经使用的栈空间大于总栈空间的1/4,则不进行栈收缩,如果是在正在进行系统调用也不能进行栈缩放,因为system使用的参数可能在栈上面。
- 缩小栈的空间为原来的一半