Linux下线程栈最大大小可以通过ulimit -s查看,单位KB。
go@ubuntu:~$ ulimit -s
8192
go@ubuntu:~$
可以通过ulimit -s 指定全局栈大小。也可以通过 pthread_attr_setstacksize()
指定当前线程的栈大小。
这两种方式前者会造成系统内存的浪费,后者需要评估线程大小,使用起来并不方便。
为了解决这个问题,gcc给出了split stacks方法。在运行期间动态调整栈大小。
但是这个方法有两个比较严重的性能问题:
- 拆分热点:反复调用的函数,会反复进行栈空间的申请、释放操作。
- 重复申请释放内存:栈内存的申请、释放在涉及栈大小调整时,总是会发生。
go对split stacks进行了优化,提出了连续栈方案。(Contiguous stacks)。
1. go栈
goroutine的栈大小通过stack结构描述。
- stack.lo: 栈空间的低地址
- stack.hi: 栈空间的高地址
- stackguard0: stack.lo+StackGuard,用于stack overlow的检测
- StackGuard: 保护区大小,常量Linux上为880字节
- StackSmall: 常量大小为128字节,用于小函数调用的优化
- StackPreempt: -1314用于抢占判断
- StackLimit:752字节
type stack struct {
lo uintptr
hi uintptr
}
type g struct {
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
。。。
}
const (
STACKSYSTEM = 0
StackSystem = STACKSYSTEM
StackBig = 4096
StackSmall = 128
)
const (
StackPreempt = -1314 // 0xfff...fade
)
// Initialize StackGuard and StackLimit according to target system.
var StackGuard = 880*stackGuardMultiplier() + StackSystem
var StackLimit = StackGuard - StackSystem - StackSmall
func stackGuardMultiplier() int {
// On AIX, a larger stack is needed for syscalls.
if GOOS == "aix" {
return 2
}
return stackGuardMultiplierDefault
}
2. 栈伸缩判断
src\runtime\stack.go
每个goroutine都将g->stackguard
设置为指向栈底向上偏移StackGuard
字节的位置。每个函数都比较栈顶指针(SP)和g->stackguard
,判断是否会发生栈溢出。 为了对带有微小栈的函数减少一些指令,对于栈小于StackSmall=128字节的函数进行了优化处理。对于栈帧巨大的函数,不进行检查,直接进行扩栈调用morestack
。
在判断栈空间是否需要扩容的时候,可以根据被调用函数栈帧的大小, 分为以下3种情况:
- 栈帧 < StackSmall(128字节)
直接调用函数,不会进行栈检查。也不会触发扩栈。
可以看到函数声明带有nosplit
标记
"".my_func_1 STEXT nosplit size=73 args=0x8 locals=0x70
0x0000 00000 (stackCheck.go:13) TEXT "".my_func_1(SB), NOSPLIT|ABIInternal, $112-8
0x0000 00000 (stackCheck.go:13) SUBQ $112, SP
0x0004 00004 (stackCheck.go:13) MOVQ BP, 104(SP)
0x0009 00009 (stackCheck.go:13) LEAQ 104(SP), BP
- StackSamll(128字节) < 栈帧 < StackBig
进行栈帧检查,需要时才进行扩栈。
可以看到调用了CALL runtime.morestack_noctxt(SB)
,之后又JMP
到函数开始重新执行。
"".my_func_3 STEXT size=99 args=0x8 locals=0x7d8
0x0000 00000 (stackCheck.go:23) TEXT "".my_func_3(SB), ABIInternal, $2008-8
0x0000 00000 (stackCheck.go:23) MOVQ (TLS), CX
0x0009 00009 (stackCheck.go:23) LEAQ -1880(SP), AX
0x0011 00017 (stackCheck.go:23) CMPQ AX, 16(CX)
0x0015 00021 (stackCheck.go:23) JLS 92
0x0017 00023 (stackCheck.go:23) SUBQ $2008, SP
0x001e 00030 (stackCheck.go:23) MOVQ BP, 2000(SP)
0x0026 00038 (stackCheck.go:23) LEAQ 2000(SP), BP
...
0x005c 00092 (stackCheck.go:23) CALL runtime.morestack_noctxt(SB)
0x0061 00097 (stackCheck.go:23) JMP 0
- 栈帧 >= StackBig
不进行栈帧检查,直接扩栈。
可以看到调用了CALL runtime.morestack_noctxt(SB)
,之后又JMP
到函数开始重新执行。
尤其,这里还进行了抢占判断0x000d 00013 (stackCheck.go:27) CMPQ SI, $-1314
,将g.stack.hi
与StackPreempt=-1314
进行比较判断是否需要扩栈(抢占在扩栈中进行)。
"".my_func_4 STEXT size=126 args=0x8 locals=0x1390
0x0000 00000 (stackCheck.go:27) TEXT "".my_func_4(SB), ABIInternal, $5008-8
0x0000 00000 (stackCheck.go:27) MOVQ (TLS), CX
0x0009 00009 (stackCheck.go:27) MOVQ 16(CX), SI
0x000d 00013 (stackCheck.go:27) CMPQ SI, $-1314
0x0014 00020 (stackCheck.go:27) JEQ 119
0x0016 00022 (stackCheck.go:27) LEAQ 880(SP), AX
0x001e 00030 (stackCheck.go:27) SUBQ SI, AX
0x0021 00033 (stackCheck.go:27) CMPQ AX, $5760
0x0027 00039 (stackCheck.go:27) JLS 119
0x0029 00041 (stackCheck.go:27) SUBQ $5008, SP
0x0030 00048 (stackCheck.go:27) MOVQ BP, 5000(SP)
...
0x0077 00119 (stackCheck.go:27) CALL runtime.morestack_noctxt(SB)
0x007c 00124 (stackCheck.go:27) JMP 0
4. 栈伸缩实现
在汇编代码中src\runtime\asm_amd64.s
定义了栈伸缩函数runtime·morestack_noctxt()
,实际实现为调用runtime·morestack()
,最终调用运行时函数runtime·newstack(SB)
。
// morestack but not preserving ctxt.
TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
MOVL $0, DX
JMP runtime·morestack(SB)
TEXT runtime·morestack(SB),NOSPLIT,$0-0
// Cannot grow scheduler stack (m->g0).
// Cannot grow signal stack (m->gsignal).
// Call newstack on m->g0's stack.
MOVQ m_g0(BX), BX
MOVQ BX, g(CX)
MOVQ (g_sched+gobuf_sp)(BX), SP
CALL runtime·newstack(SB)
CALL runtime·abort(SB) // crash if newstack returns
RET
newstack()
src\runtime\stack.go
- 判断是否需要抢占,通过
gp.stackguard0) == stackPreempt
进行判断。以下几种情形不会触发抢占,调用gogo()
回到g继续执行。
- M被锁定
- M正在分配内存
- M设置了当前不能抢占
- M的状态不是运行中
- 如果设置了抢占标记,但是出于gc扫描中,则放弃抢占,回到G继续执行。
- 执行抢占,抢占后会直接执行新的G。
- 没有抢占发生,则将栈扩展到原来的2倍大小。调用
gogo()
继续执行当前的g。
func newstack() {
thisg := getg()
gp := thisg.m.curg
// 1. 判断是否需要抢占,通过```gp.stackguard0) == stackPreempt```进行判断。
preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt
// 2. 需要进行抢占,执行gogo()
if preempt {
if thisg.m.locks != 0 || thisg.m.mallocing != 0 || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning {
// Let the goroutine keep running for now.
// gp->preempt is set, so it will be preempted next time.
gp.stackguard0 = gp.stack.lo + _StackGuard
gogo(&gp.sched) // never return
}
}
if preempt {
// Act like goroutine called runtime.Gosched.
casgstatus(gp, _Gwaiting, _Grunning)
// 抢占g
gopreempt_m(gp) // never return
}
// 新栈大小为旧栈的2倍。
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize * 2
// 最大栈大小为1GB。
if newsize > maxstacksize {
throw("stack overflow")
}
casgstatus(gp, _Grunning, _Gcopystack)
// 复制旧栈到新栈
copystack(gp, newsize, true)
// 修改G状态为 _Grunning
casgstatus(gp, _Gcopystack, _Grunning)
gogo(&gp.sched)
}
gogo()
gogo直接跳转到g的执行位置继续执行,不会再执行gogo()函数调用之后的代码。
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX
MOVQ 0(DX), CX // make sure g != nil
get_tls(CX)
MOVQ DX, g(CX)
MOVQ gobuf_sp(BX), SP // restore SP
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
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)
MOVQ gobuf_pc(BX), BX
JMP BX
抢占g
- 修改G状态为可以运行。
_Grunning
->_Grunnable
- 调用
dropg()
,将m与g解除关系。 - 调用
globrunqput()
,将g放到全局待执行列表sched.runq
。 - 调用
schedule()
进行goroutine调度,来继续运行其它可运行的G.
func gopreempt_m(gp *g) {
if trace.enabled {
traceGoPreempt()
}
goschedImpl(gp)
}
func goschedImpl(gp *g) {
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
casgstatus(gp, _Grunning, _Grunnable)
dropg()
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)
schedule()
}