栈扩大

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

这个函数调用runtime·morestack

这个函数中:
- 检查是否是从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

栈扩容安全性检查

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), " sp:", hex(gp.sched.sp), " lr:", 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使用的参数可能在栈上面。
  • 缩小栈的空间为原来的一半