Go: Stack
Backlinks (0)
No backlinks found
1 · stack#
每个goroutine都有自己的栈; 栈初始很小(2KB),按需增长或者收缩; 使用#连续栈,不够时会自动扩容(分配更大新栈,把旧栈内容移动过去); 栈由一帧一帧的函数调用组成; 栈上放函数参数、返回值、局部参数等(如果放不下会逃逸到堆上);
按需增长/收缩,说明是运行时控制的,先看看栈是如何定义的, 其次是操作,怎么触发的扩容,怎么扩容,怎么把旧栈内容移动到新栈,说完扩容,就有缩容了。
从g定义中可知,
函数调用栈:todo
分为调用者栈、被调用者栈
调用者栈:caller parent BP,(BP pseudo SP)local var0 …local varN,callee arg2 … callee arg0 ,(FP virtual register)return addr
被调用者栈:caller BP(caller frame pointer),Local var0 …local varN,(SP real register)
可见:调用子函数时,先在栈顶准备好参数,再执行CALL指令。CALL会将IP寄存器(PC)的值压栈,也就是执行完子函数后的下一条指令。然后进入被调用者栈,首先将caller BP压栈(栈基址,栈底)
CALL类似push ip ,JMP somefuc结合,push 当前ip的地址到栈,然后把调用函数地址放到ip地址,实现调整。
RET 和CALL相反,pop 在CALL时push的ip指令到ip,实现函数返回。
总结:调用子函数前,准备参数、返回地址;CALL将返回地址入栈;进入被调用函数,汇编器插入BP寄存器相关指令;下面就是被调用函数栈的局部变量,与再次调用其他子函数的参数;被调用函数RET,从栈恢复BP、SP,接着取回返回地址调整??(RET这部分不明白)
go汇编引入的伪寄存器:FP,PC,SB,SP
FP,帧指针,参数和局部变量 symbol+offset(FP)
PC,程序计数器,跳转和分支
SB,静态基指针全局符号 用来申明函数或者全局变量
SP,栈指针,栈顶 指向当前栈帧的局部变量的开始位置,symbol+offset(SP)方式引用函数的局部变量,offset 的合法取值是 [-framesize, 0)
手写汇编代码时,如果是 symbol+offset(SP) 形式,则表示伪寄存器 SP,表示栈底。如果是 offset(SP) 则表示硬件寄存器 SP,表示栈顶。务必注意。对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的 SP 都是硬件寄存器 SP,无论是否带 symbol.
2 · stack#
go栈、用户栈
函数调用栈:todo
分为调用者栈、被调用者栈
调用者栈:caller parent BP,(BP pseudo SP)local var0 …local varN,callee arg2 … callee arg0 ,(FP virtual register)return addr
被调用者栈:caller BP(caller frame pointer),Local var0 …local varN,(SP real register)
可见:调用子函数时,先在栈顶准备好参数,再执行CALL指令。CALL会将IP寄存器(PC)的值压栈,也就是执行完子函数后的下一条指令。然后进入被调用者栈,首先将caller BP压栈(栈基址,栈底)
CALL类似push ip ,JMP somefuc结合,push 当前ip的地址到栈,然后把调用函数地址放到ip地址,实现调整。
RET 和CALL相反,pop 在CALL时push的ip指令到ip,实现函数返回。
总结:调用子函数前,准备参数、返回地址;CALL将返回地址入栈;进入被调用函数,汇编器插入BP寄存器相关指令;下面就是被调用函数栈的局部变量,与再次调用其他子函数的参数;被调用函数RET,从栈恢复BP、SP,接着取回返回地址调整??(RET这部分不明白)
go汇编引入的伪寄存器:FP,PC,SB,SP
FP,帧指针,参数和局部变量 symbol+offset(FP)
PC,程序计数器,跳转和分支
SB,静态基指针全局符号 用来申明函数或者全局变量
SP,栈指针,栈顶 指向当前栈帧的局部变量的开始位置,symbol+offset(SP)方式引用函数的局部变量,offset 的合法取值是 [-framesize, 0)
手写汇编代码时,如果是 symbol+offset(SP) 形式,则表示伪寄存器 SP。如果是 offset(SP) 则表示硬件寄存器 SP。务必注意。对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的 SP 都是硬件寄存器 SP,无论是否带 symbol。
3 · think in stack#
Go 1.2 :协程的堆栈大小从 4Kb 增加到 8Kb。
Go 1.4 :协程的堆栈大小从 8Kb 减小到 2Kb。
连续堆栈 VS 分段堆栈。
4 · 连续栈 contiguous stack#
https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzWtrZFQ5suE8qr2sD8uWQ/pub
给每一个 goroutine 的栈分配一段连续的空间,当空间填满时,使用 reallocation/copy 增长空间。
4.1 · Why#
非连续栈(split stack)的问题:
- 不是一整块连续栈,而是”栈不够了就再挂一块新 chunk”
- 热点路径上不断扩容/缩容
- 运行时的持续开销:chunk 链接/解绑 等
连续栈(contiguous stack)的优势:
- 是一整段连续内存
- 栈到合适位置,后续通常不需要折腾;不会在热点上不断扩容/缩容
- 运行时开销较简单
总结: 连续栈值扩一次,基本稳定;非连续栈会在阈值处来回跨,导致性能抖动。
4.2 · How#
如何复制栈:依赖逃逸分析保证,栈内指针指向的值都在栈上,可以复制整段栈后修复指针。
- 栈溢出检查:
- 复制栈:将栈看作一个指针数组,复制到新栈,根据 GC 的指针元数据修正所有真实指针
- reflect.call: 比较特殊 TODO
- GC, defer/panic/recover, stack tracing
- 收缩:在 GC 时按使用率缩回去
缺点:连续内存压力大,内存碎片化;严格限制进入栈的指针;
5 · 栈回溯 traceback#
栈回溯的实现在 src/runtime/traceback.go 里,核心是:
- unwinder:按帧遍历栈
- traceback / traceback1 / traceback2:从某个 (pc, sp, lr) 开始打印或收集调用栈
- tracebackPCs:只收集 PC(用于 pprof、Callers 等)