栈的演变
分段栈(Segment Stacks)热分裂(hot split

分段栈(Segment Stack)

stack
segment stack
热分裂(hot split)

当一个 stack 即将用完的时候,任意一个函数都会导致堆栈的扩容,当函数执行完返回后,又要触发堆栈的收缩。如果这个操作是在一个for语句里执行的话,则过多的 malloc 和 free 重复操作将导致系统资源开销非常的大。

Stack Frame

连续栈(Contiguous stacks)

Contiguous stacks 扩容与收缩

连续栈使用了另一种管理机制,每当一个stack 空间不够的时候,直接再申请一个2倍大小的空间,然后再将stack数据拷贝过去,同时修改指向原来stack 的指针到新stack,最后再将旧stack删除。

这种机制可以在当stack 空间快用尽的时候,避免在for语句里频繁触发扩容的问题。也正是官方采用这种机制的原因。

栈的初始化

在上篇文章《Golang 的底层引导流程/启动顺序》中介绍过,在应用启动时会有一系列的初始化工作,其中就包括对栈的初始化 (源码),调用函数 。

func stackinit() {
	if _StackCacheSize&_PageMask != 0 {
		throw("cache size must be a multiple of page size")
	}
	for i := range stackpool {
		stackpool[i].item.span.init()
		lockInit(&stackpool[i].item.mu, lockRankStackpool)
	}
	for i := range stackLarge.free {
		stackLarge.free[i].init()
		lockInit(&stackLarge.lock, lockRankStackLarge)
	}
}
stackpoolstackLarge_NumStackOrderslarge stack

在应用刚开始时,会分别对这两种全局stack变量进行初始化。

扩容
newstack()runtime.morestack()

当G的堆栈空间不够用时,系统会再申请一块两倍大小(源码)的空间,然后将原堆栈数据拷贝过去,再删除原来的堆栈。

步骤

收缩

当一个 G 占用的stack非常大,后期却很少使用的stack,这时候则会有大量的stack处于空间状态,我们需要对空间进行收缩,以释放资源。

收缩原则

  • 在GC期间,如果一个goroutine未使用stack的大小占用超过X% ,则需要将其复制到一个small stack中。当需要的时候再进行扩容。
  • 在GC期间,如果一个goroutine未使用stack的大小占用超过X%,将其stack guard 降低到一个较小的值。如果它在下一次GC时没有受到保护,则将其复制到一个small stack 中。
1/41/2
栈的释放

对stack的申请与释放请参考 《goroutine栈的申请与释放》

其它
Stack Frame 
  • Local variables
  • Saved copies of registers modified by subprograms that could need restoration
  • Argument parameters
  • Return address
FPSPPCSB
FPSPPCSB

所有用户空间的数据都可以通过FP/SP(局部数据、输入参数、返回值)和SB(全局数据)访问。 通常情况下,不会对SB/FP寄存器进行运算操作,通常情况以会以SB/FP/SP作为基准地址,进行偏移解引用 等操作。

伪SP是一个比较特殊的寄存器,因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部,一般用于定位调用其它函数的参数和返回值。

(SP)+8(SP)a(SP)b+8(SP)
// Stack frame layout
//
// (x86)
// +------------------+
// | args from caller |
// +------------------+ <- frame->argp
// |  return address  |
// +------------------+
// |  caller's BP (*) | (*) if framepointer_enabled && varp < sp
// +------------------+ <- frame->varp
// |     locals       |
// +------------------+
// |  args to callee  |
// +------------------+ <- frame->sp
//
// (arm)
// +------------------+
// | args from caller |
// +------------------+ <- frame->argp
// | caller's retaddr |
// +------------------+ <- frame->varp
// |     locals       |
// +------------------+
// |  args to callee  |
// +------------------+
// |  return address  |
// +------------------+ <- frame->sp
x86armx86

调用栈

这里以一个函数调用过程A->B->C为例了来解释调用栈过程

分配从高到低顺序进行。

推荐阅读针对 heap 的GC: Go:内存管理与内存清理,

参考