goroutine栈的申请与释放

当我们执行一个 go func() 语句的时候,runtime 会通过调用 newproc() 函数来创建G。而内部真正创建创建G的函数为 newproc1(),在没有可以复用的G的情况下,会通过 newg = malg(_StackMin) 语句创建一个包含stack的G。

// Allocate a new g, with a stack big enough for stacksize bytes.
func malg(stacksize int32) *g {
	newg := new(g)
	if stacksize >= 0 {
		stacksize = round2(_StackSystem + stacksize)
		systemstack(func() {
			newg.stack = stackalloc(uint32(stacksize))
		})
		newg.stackguard0 = newg.stack.lo + _StackGuard
		newg.stackguard1 = ^uintptr(0)
		// Clear the bottom word of the stack. We record g
		// there on gsignal stack during VDSO on ARM and ARM64.
		*(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
	}
	return newg
}

对新创建的g,需要通过调用 stackalloc() 函数为其分配 stacksize 大小stack,那么分配操作它又是如何工作的呢?

stack的申请

根据申请stack的大小,又分为两种情况,一种是 small stack,另一种是 large stack,两者采用不同的申请策略。主要涉及了内存申请策略,如果对golang 的内存管理比较了解的话,这块理解起来就显的太过于简单了。建议先阅读一下这篇文章《Golang 内存组件之mspan、mcache、mcentral 和 mheap 数据结构》。

func stackalloc(n uint32) stack {
	...

	var v unsafe.Pointer
	if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
		order := uint8(0)
		n2 := n
		for n2 > _FixedStack {
			order++
			n2 >>= 1
		}
		var x gclinkptr
		if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" {
			// thisg.m.p == 0 can happen in the guts of exitsyscall
			// or procresize. Just get a stack from the global pool.
			// Also don't touch stackcache during gc
			// as it's flushed concurrently.
			lock(&stackpool[order].item.mu)
			x = stackpoolalloc(order)
			unlock(&stackpool[order].item.mu)
		} else {
			c := thisg.m.p.ptr().mcache
			x = c.stackcache[order].list
			if x.ptr() == nil {
				stackcacherefill(c, order)
				x = c.stackcache[order].list
			}
			c.stackcache[order].list = x.ptr().next
			c.stackcache[order].size -= uintptr(n)
		}
		v = unsafe.Pointer(x)
	} else {
		...
	}
	return stack{uintptr(v), uintptr(v) + uintptr(n)}
}

对于small stack,会直接通过从G绑定的P中的 mcache 字段申请,这个字段可以理解为内存资源中心,里面包含有多种不同规格大小的内存块,根据申请大小找到一个可以满足其大小的最小规格的内存区域。

这里重点关注下 stackcacherefill() 函数。

func stackalloc(n uint32) stack {
	...

	var v unsafe.Pointer
	if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
		...
	} else {
		var s *mspan
		npage := uintptr(n) >> _PageShift
		log2npage := stacklog2(npage)

		// Try to get a stack from the large stack cache.
		lock(&stackLarge.lock)
		if !stackLarge.free[log2npage].isEmpty() {
			s = stackLarge.free[log2npage].first
			stackLarge.free[log2npage].remove(s)
		}
		unlock(&stackLarge.lock)

		lockWithRankMayAcquire(&mheap_.lock, lockRankMheap)

		if s == nil {
			// Allocate a new stack from the heap.
			s = mheap_.allocManual(npage, spanAllocStack)
			if s == nil {
				throw("out of memory")
			}
			osStackAlloc(s)
			s.elemsize = uintptr(n)
		}
		v = unsafe.Pointer(s.base())
	}
	...
	return stack{uintptr(v), uintptr(v) + uintptr(n)}
}

对于 large stack 而言,先从 stackLarge 全局对象 large stack spans 池中查找,如果未找到的话,则调用 osStackAlloc()mheap_.allocManual() 函数直接从 heap 中直接申请并注册这块内存。

最后根据找到内存的起始地址(低地址)和 申请大小n,返回 stack 结构体。

stack的释放

对于stack的释放,实现函数为 stackfree()

在资源申请时根据其申请大小分为两种情况,同样对于stack的释放也是一样。对于small stack 来讲,从哪里申请的,用完以后再放回去即可。操作对象仍是 P下面的 mcache 字段。

这里重点关注下 stackcacherelease() 函数。

对于 large stack 来讲,则将再放在 stackLarge 全局变量里,以便可以复用,避免重复申请产生的开销;但如果当前GC正处于清扫阶段,则直接调用 osStackFree()mheap_.freeManual() 来释放内存。

对于g中stack的释放触发条件有: