goroutine栈的申请与释放

最新版本查看 https://blog.haohtml.com/archives/30403

当我们执行一个 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的释放触发条件有:

  • 当一个g运行结束的时候,可能会释放stack(只是有可能)参考:https://blog.haohtml.com/archives/23437;
  • GC期间会清理所有包含stack的g,直接释放完所有g的stack, 然后再将这些g放在不包含stack的 sched.gFree.noStack 列表中,参考:https://blog.haohtml.com/archives/27003;
  • 其它

你可能感兴趣的:(goroutine栈的申请与释放)