然后对比新旧栈的 hi 的值计算出两块内存之间的差值 delta,这个 delta 会在调用 adjustsudogs、adjustctxt 等函数的时候判断旧栈的内存指针位置,然后加上 delta 然后就获取到了新栈的指针位置,这样就可以将指针也调整到新栈了;
调用 memmove 将源栈中的整片内存拷贝到新的栈中;
然后继续调用调整指针的函数继续调整栈中 txt、defer、panic 位置的指针;
接下来将 G 上的栈引用切换成新栈;
最后调用 stackfree 释放原始栈的内存空间;
栈的收缩栈的收缩发生在 GC 时对栈进行扫描的阶段:
func scanstack(gp *g, gcw *gcWork) { ... // 进行栈收缩 shrinkstack(gp) ... }如果还不清楚 GC 的话不妨看一下我这篇文章:《Go语言GC实现原理及源码分析 https://www.luozhiyun.com/archives/475 》。
runtime.shrinkstack
shrinkstack 这个函数我屏蔽了一些校验函数,只留下面的核心逻辑:
func shrinkstack(gp *g) { ... oldsize := gp.stack.hi - gp.stack.lo newsize := oldsize / 2 // 当收缩后的大小小于最小的栈的大小时,不再进行收缩 if newsize < _FixedStack { return } avail := gp.stack.hi - gp.stack.lo // 计算当前正在使用的栈数量,如果 gp 使用的当前栈少于四分之一,则对栈进行收缩 // 当前使用的栈包括到 SP 的所有内容以及栈保护空间,以确保有 nosplit 功能的空间 if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 { return } // 将旧栈拷贝到新收缩后的栈上 copystack(gp, newsize) }新栈的大小会缩小至原来的一半,如果小于 _FixedStack (2KB)那么不再进行收缩。除此之外还会计算一下当前栈的使用情况是否不足 1/4 ,如果使用超过 1/4 那么也不会进行收缩。
最后判断确定要进行收缩则调用 copystack 函数进行栈拷贝的逻辑。
总结如果对于没有了解过内存布局的同学,理解起来可能会比较吃力,因为我们在看堆的时候内存增长都是从小往大增长,而栈的增长方向是相反的,导致在做栈指令操作的时候将 SP 减小反而是将栈帧增大。
除此之外就是 Go 使用的是 plan9 这种汇编,资料比较少,看起来很麻烦,想要更深入了解这种汇编的可以看我下面的 Reference 的资料。
Reference聊一聊goroutine stack https://kirk91.github.io/posts/2d571d09/
Anatomy of a Program in Memory https://manybutfinite.com/post/anatomy-of-a-program-in-memory/
stack-frame-layout-on-x86-64 https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64
深入研究goroutine栈 深入研究goroutine栈/
x86-64 下函数调用及栈帧原理 https://zhuanlan.zhihu.com/p/27339191
Call stack https://en.wikipedia.org/wiki/Call_stack
plan9 assembly 完全解析 https://github.com/cch123/golang-notes/blob/master/assembly.md
Go语言内幕(5):运行时启动过程 https://studygolang.com/articles/7211
Go 汇编入门 https://github.com/go-internals-cn/go-internals/blob/master/chapter1_assembly_primer/README.md
Go Assembly by Example https://davidwong.fr/goasm/
https://golang.org/doc/asm