在 stackpoolalloc 函数中会去找 stackpool 对应 order 下标的 span 链表的头节点,如果不为空,那么直接将头节点的属性 manualFreeList 指向的节点从链表中移除,并返回;
如果 list.first为空,那么调用 mheap_的 allocManual 函数从堆中分配 mspan,具体的内存分配相关的文章可以看我这篇:《详解Go中内存分配源码实现 https://www.luozhiyun.com/archives/434 》。
从 allocManual 函数会分配 32KB 大小的内存块,分配好新的 span 之后会根据 elemsize 大小将 32KB 内存进行切割,然后通过单向链表串起来并将最后一块内存地址赋值给 manualFreeList 。
比如当前的 elemsize 所代表的内存大小是 8KB大小:
runtime.stackcacherefill
func stackcacherefill(c *mcache, order uint8) { var list gclinkptr var size uintptr lock(&stackpool[order].item.mu) //_StackCacheSize = 32 * 1024 // 将 stackpool 分配的内存组成一个单向链表 list for size < _StackCacheSize/2 { x := stackpoolalloc(order) x.ptr().next = list list = x // _FixedStack = 2048 size += _FixedStack << order } unlock(&stackpool[order].item.mu) c.stackcache[order].list = list c.stackcache[order].size = size }stackcacherefill 函数会调用 stackpoolalloc 从 stackpool 中获取一半的空间组装成 list 链表,然后放入到 stackcache 数组中。
大栈内存分配 func stackalloc(n uint32) stack { thisg := getg() var v unsafe.Pointer if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize { ... } else { // 申请的内存空间过大,从 runtime.stackLarge 中检查是否有剩余的空间 var s *mspan // 计算需要分配多少个 span 页, 8KB 为一页 npage := uintptr(n) >> _PageShift // 计算 npage 能够被2整除几次,用来作为不同大小内存的块的索引 log2npage := stacklog2(npage) lock(&stackLarge.lock) // 如果 stackLarge 对应的链表不为空 if !stackLarge.free[log2npage].isEmpty() { //获取链表的头节点,并将其从链表中移除 s = stackLarge.free[log2npage].first stackLarge.free[log2npage].remove(s) } unlock(&stackLarge.lock) lockWithRankMayAcquire(&mheap_.lock, lockRankMheap) //这里是stackLarge为空的情况 if s == nil { // 从堆上申请新的内存 span s = mheap_.allocManual(npage, &memstats.stacks_inuse) if s == nil { throw("out of memory") } // OpenBSD 6.4+ 系统需要做额外处理 osStackAlloc(s) s.elemsize = uintptr(n) } v = unsafe.Pointer(s.base()) } ... return stack{uintptr(v), uintptr(v) + uintptr(n)} }对于大栈内存分配,运行时会查看 stackLarge 中是否有剩余的空间,如果不存在剩余空间,它也会调用 mheap_.allocManual 从堆上申请新的内存。
栈的扩容 栈溢出检测编译器会在目标代码生成的时候执行:src/cmd/internal/obj/x86/obj6.go:stacksplit 根据函数栈帧大小插入相应的指令,检查当前 goroutine 的栈空间是否足够。
当栈帧大小(FramSzie)小于等于 StackSmall(128)时,如果 SP 小于 stackguard0 那么就执行栈扩容;
当栈帧大小(FramSzie)大于 StackSmall(128)时,就会根据公式 SP - FramSzie + StackSmall 和 stackguard0 比较,如果小于 stackguard0 则执行扩容;
当栈帧大小(FramSzie)大于StackBig(4096)时,首先会检查 stackguard0 是否已转变成 StackPreempt 状态了;然后根据公式 SP-stackguard0+StackGuard <= framesize + (StackGuard-StackSmall)判断,如果是 true 则执行扩容;
我们先来看看伪代码会更清楚一些:
当栈帧大小(FramSzie)小于等于 StackSmall(128)时:
CMPQ SP, stackguard JEQ label-of-call-to-morestack当栈帧大小(FramSzie)大于 StackSmall(128)时:
LEAQ -xxx(SP), AX CMPQ AX, stackguard JEQ label-of-call-to-morestack这里 AX = SP - framesize + StackSmall,然后执行 CMPQ 指令让 AX 与 stackguard 比较;
当栈帧大小(FramSzie)大于StackBig(4096)时:
MOVQ stackguard, SI // SI = stackguard CMPQ SI, $StackPreempt // compare SI ,StackPreempt JEQ label-of-call-to-morestack LEAQ StackGuard(SP), AX // AX = SP + StackGuard SUBQ SI, AX // AX = AX - SI = SP + StackGuard -stackguard CMPQ AX, $(framesize+(StackGuard-StackSmall))