传统的Linux进程内存布局
user stack大小固定,Linux 默认8M,运行时内存占用超过上限,程序会崩溃掉并报告segment错误
- 可以调大内核 stack size 参数,简单但影响系统所有thread
- 创建线程时显式传入所需要内存块大小,需要开发者精确计算每个thread的大小, 负担比较高
- 既不影响所有thread又不给开发者增加太多,比如在函数调用处插桩检查当前栈的空间是否能够满足新函数的执行
满足直接执行,否则创建新的栈空间并将老的栈拷贝到新的栈然后再执行
但当前Linux kernel thread模型却不能满足,只能在用户空间实现,并且有不小的难度
go runtime解决了这个问题, 每个routine(g0除外)初始化时stack都=2KB, 运行过程中会根据不同的场景做动态的调整
栈扩容和缩容
协程栈的内存布局和一些重要的术语
- stack.lo: 栈空间低地址
- stack.hi: 栈空间高地址
- stackguard0: stack.lo + StackGuard, 用于stack overlow的检测
- StackGuard: 保护区大小,Linux=880字节
- StackSmall: 128字节,用于小函数调用的优化
在判断栈空间是否需要扩容的时候,可以根据被调用函数栈帧的大小, 分为以下两种情况:
-
小于StackSmall SP小于stackguard0, 执行栈扩容,否则直接执行
-
大于StackSamll SP - Function’s Stack Frame Size + StackSmall 小于stackguard0, 执行栈扩容,否则直接执行
runtime中还有个StackBig的常量,默认4096,被调用函数栈帧大小大于StackBig的时候, 一定会发生栈的扩容
package main
func main() {
a, b := 1, 2
_ = add1(a, b)
_ = add2(a, b)
}
func add1(x, y int) int {
return x + y
}
func add2(x, y int) int {
_ = make([]byte, 200)
return x + y
}
go tool compile -N -l -S stack.go > stack.s
"".main t=1 size=112 args=0x0 locals=0x30
// 栈大小为48,无参数
0x0000 00000 (stack.go:3) TEXT "".main(SB), $48-0
// 通过thread local storage获取当前g(g为goroutine的的数据结构)
0x0000 00000 (stack.go:3) MOVQ (TLS), CX
// 比较SP和g.stackguard0
0x0009 00009 (stack.go:3) CMPQ SP, 16(CX)
// 小于g.stackguard0,jump到105执行栈的扩容
0x000d 00013 (stack.go:3) JLS 105
// 继续执行
0x000f 00015 (stack.go:3) SUBQ $48, SP
0x0013 00019 (stack.go:3) MOVQ BP, 40(SP)
0x0018 00024 (stack.go:3) LEAQ 40(SP), BP
// 用于垃圾回收
0x001d 00029 (stack.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (stack.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (stack.go:4) MOVQ $1, "".a+32(SP)
0x0026 00038 (stack.go:4) MOVQ $2, "".b+24(SP)
// 将a放入AX寄存器
0x002f 00047 (stack.go:5) MOVQ "".a+32(SP), AX
// 参数a压栈
0x0034 00052 (stack.go:5) MOVQ AX, (SP)
// 将b放入AX寄存器
0x0038 00056 (stack.go:5) MOVQ "".b+24(SP), AX
// 参数b压栈
0x003d 00061 (stack.go:5) MOVQ AX, 8(SP)
0x0042 00066 (stack.go:5) PCDATA $0, $0
// 调用add1
0x0042 00066 (stack.go:5) CALL "".add1(SB)
// 将a放入AX寄存器
0x0047 00071 (stack.go:6) MOVQ "".a+32(SP), AX
// 参数a压栈
0x004c 00076 (stack.go:6) MOVQ AX, (SP)
// 将b放入AX寄存器
0x0050 00080 (stack.go:6) MOVQ "".b+24(SP), AX
// 参数b压栈
0x0055 00085 (stack.go:6) MOVQ AX, 8(SP)
0x005a 00090 (stack.go:6) PCDATA $0, $0
// 调用add2
0x005a 00090 (stack.go:6) CALL "".add2(SB)
0x005f 00095 (stack.go:7) MOVQ 40(SP), BP
0x0064 00100 (stack.go:7) ADDQ $48, SP
0x0068 00104 (stack.go:7) RET
0x0069 00105 (stack.go:7) NOP
0x0069 00105 (stack.go:3) PCDATA $0, $-1
// 调用runtime.morestack_noctxt执行栈扩容
0x0069 00105 (stack.go:3) CALL runtime.morestack_noctxt(SB)
// 返回到函数开始处继续执行
0x006e 00110 (stack.go:3) JMP 0
...
"".add1 t=1 size=28 args=0x18 locals=0x0
// 栈大小为0,参数为24字节, 栈帧小于StackSmall不进行栈空间判断直接执行
0x0000 00000 (stack.go:9) TEXT "".add1(SB), $0-24
0x0000 00000 (stack.go:9) FUNCDATA $0, gclocals·54241e171da8af6ae173d69da0236748(SB)
0x0000 00000 (stack.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (stack.go:9) MOVQ $0, "".~r2+24(FP)
0x0009 00009 (stack.go:10) MOVQ "".x+8(FP), AX
0x000e 00014 (stack.go:10) MOVQ "".y+16(FP), CX
0x0013 00019 (stack.go:10) ADDQ CX, AX
0x0016 00022 (stack.go:10) MOVQ AX, "".~r2+24(FP)
0x001b 00027 (stack.go:10) RET
"".add2 t=1 size=151 args=0x18 locals=0xd0
// 栈大小为208字节,参数为24字节
0x0000 00000 (stack.go:13) TEXT "".add2(SB), $208-24
// 获取当前g
0x0000 00000 (stack.go:13) MOVQ (TLS), CX
// 栈大小大于StackSmall, 计算 SP - FramSzie + StackSmall 并放入AX寄存器
0x0009 00009 (stack.go:13) LEAQ -80(SP), AX
// 比较上面计算出来的值和g.stackguard0
0x000e 00014 (stack.go:13) CMPQ AX, 16(CX)
// 小于g.stackguard0, jump到141执行栈的扩容
0x0012 00018 (stack.go:13) JLS 141
// 继续执行
0x0014 00020 (stack.go:13) SUBQ $208, SP
0x001b 00027 (stack.go:13) MOVQ BP, 200(SP)
0x0023 00035 (stack.go:13) LEAQ 200(SP), BP
0x002b 00043 (stack.go:13) FUNCDATA $0, gclocals·54241e171da8af6ae173d69da0236748(SB)
0x002b 00043 (stack.go:13) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x002b 00043 (stack.go:13) MOVQ $0, "".~r2+232(FP)
0x0037 00055 (stack.go:14) MOVQ $0, ""..autotmp_0(SP)
0x003f 00063 (stack.go:14) LEAQ ""..autotmp_0+8(SP), DI
0x0044 00068 (stack.go:14) XORPS X0, X0
0x0047 00071 (stack.go:14) DUFFZERO $247
0x005a 00090 (stack.go:14) LEAQ ""..autotmp_0(SP), AX
0x005e 00094 (stack.go:14) TESTB AL, (AX)
0x0060 00096 (stack.go:14) JMP 98
0x0062 00098 (stack.go:15) MOVQ "".x+216(FP), AX
0x006a 00106 (stack.go:15) MOVQ "".y+224(FP), CX
0x0072 00114 (stack.go:15) ADDQ CX, AX
0x0075 00117 (stack.go:15) MOVQ AX, "".~r2+232(FP)
0x007d 00125 (stack.go:15) MOVQ 200(SP), BP
0x0085 00133 (stack.go:15) ADDQ $208, SP
0x008c 00140 (stack.go:15) RET
0x008d 00141 (stack.go:15) NOP
0x008d 00141 (stack.go:13) PCDATA $0, $-1
// 调用runtime.morestack_noctxt完成栈扩容
0x008d 00141 (stack.go:13) CALL runtime.morestack_noctxt(SB)
// jump到函数开始的地方继续执行
0x0092 00146 (stack.go:13) JMP 0
...
被调用函数栈帧小于StackSmall时没有执行栈空间大小判断而是直接执行,在一定程度上优化了小函数的调用
于StackSmall的,执行栈空间大小判断,栈空间不足时,调用runtime.morestack_noctxt完成栈的扩容,然后再重新开始执行函数
go 1.3 前,栈扩容用分段栈(Segemented Stack),在栈空间不够的时候新申请一个栈空间用于被调用函数的执行, 执行后销毁新申请的栈空间并回到老的栈空间继续执行,当函数出现频繁调用(递归)时可能会引发hot split
1.3后用连续栈(Contiguous Stack),栈空间不足的时候申请一个2倍于当前大小的新栈,并把所有数据拷贝到新栈, 接下来的所有调用执行都发生在新栈上
栈缩容
long running 的 goroutine 由于某次函数调用中引发了栈的扩容, 被调用函数返回后很大部分空间都未被利用,需要进行栈收缩,节约内存提高利用率
栈收缩不在函数调用时发生,由 GC 时主动触发
基本过程是计算当前使用的空间,小于栈空间的1/4的话, 执行栈的收缩,将栈收缩为现在的1/2,否则直接返回
栈缩容的目标是提高内存利用率,但在缩容过程中会存在栈拷贝和写屏障(write barrier),会有些性能影响
GODEBUG=gcshrinkstackoff=1 可关闭栈缩容, 需要承担栈持续增长的风险,在关闭前需要慎重考虑