遥不可及|Go 协程的栈内存管理,解密( 二 )


0xc000030738......0xc000081f38前面说了每个goroutine都维护着自己的栈区 , 栈结构是连续栈 , 是一块连续的内存 , 在goroutine的类型定义的源码里我们可以找到标记着栈区边界的stack信息 , stack里记录着栈区边界的高位内存地址和低位内存地址:
typegstruct{stackstack...}typestackstruct{louintptrhiuintptr}栈空间在运行时中包含两个重要的全局变量 , 分别是runtime.stackpool和runtime.stackLarge , 这两个变量分别表示全局的栈缓存和大栈缓存 , 前者可以分配小于32KB的内存 , 后者用来分配大于32KB的栈空间:
//Numberofordersthatgetcaching.Order0isFixedStack//andeachsuccessiveorderistwiceaslarge.//Wewanttocache2KB,4KB,8KB,and16KBstacks.Largerstacks//willbeallocateddirectly.//SinceFixedStackisdifferentondifferentsystems,we//mustvaryNumStackOrderstokeepthesamemaximumcachedsize.//OS|FixedStack|NumStackOrders//-----------------+------------+---------------//linux/darwin/bsd|2KB|4//windows/32|4KB|3//windows/64|8KB|2//plan9|4KB|3_NumStackOrders=4-sys.PtrSize/4*sys.GoosWindows-1*sys.GoosPlan9varstackpool[_NumStackOrders]mSpanListtypestackpoolItemstruct{mumutexspanmSpanList}varstackLargestruct{lockmutexfree[heapAddrBits-pageShift]mSpanList}//go:notinheaptypemSpanListstruct{first*mspan//firstspaninlist,ornilifnonelast*mspan//lastspaninlist,ornilifnone}可以看到这两个用于分配空间的全局变量都与内存管理单元runtime.mspan有关 , 所以我们栈内容的申请也是跟前面文章里的一样 , 先去当前线程的对应尺寸的mcache里去申请 , 不够的时候mache会从全局的mcental里取内存等等 , 想了解这部分具体细节的同学可以参考前面的文章《图解Go内存管理器的内存分配策略》 。
其实从调度器和内存分配的角度来看 , 如果运行时只使用全局变量来分配内存的话 , 势必会造成线程之间的锁竞争进而影响程序的执行效率 , 栈内存由于与线程关系比较密切 , 所以在每一个线程缓存runtime.mcache中都加入了栈缓存减少锁竞争影响 。
typemcachestruct{...alloc[numSpanClasses]*mspanstackcache[_NumStackOrders]stackfreelist...}typestackfreeliststruct{listgclinkptrsizeuintptr}编译器会为函数调用插入运行时检查runtime.morestack , 它会在几乎所有的函数调用之前检查当前goroutine的栈内存是否充足 , 如果当前栈需要扩容 , 会调用runtime.newstack创建新的栈:
funcnewstack(){......//Allocateabiggersegmentandmovethestack.oldsize:=gp.stack.hi-gp.stack.lonewsize:=oldsize*2ifnewsize>maxstacksize{print("runtime:goroutinestackexceeds",maxstacksize,"-bytelimitn")throw("stackoverflow")}//Thegoroutinemustbeexecutinginordertocallnewstack,//soitmustbeGrunning(orGscanrunning).casgstatus(gp,_Grunning,_Gcopystack)//TheconcurrentGCwillnotscanthestackwhilewearedoingthecopysince//thegpisinaGcopystackstatus.copystack(gp,newsize,true)ifstackDebug>=1{print("stackgrowdonen")}casgstatus(gp,_Gcopystack,_Grunning)}旧栈的大小是通过我们上面说的保存在goroutine中的stack信息里记录的栈区内存边界计算出来的 , 然后用旧栈两倍的大小创建新栈 , 创建前会检查是新栈的大小是否超过了单个栈的内存上限 。
oldsize:=gp.stack.hi-gp.stack.lonewsize:=oldsize*2ifnewsize>maxstacksize{print("runtime:goroutinestackexceeds",maxstacksize,"-bytelimitn")throw("stackoverflow")}如果目标栈的大小没有超出程序的限制 , 会将goroutine切换至_Gcopystack状态并调用runtime.copystack开始栈的拷贝 , 在拷贝栈的内存之前 , 运行时会先通过runtime.stackalloc函数分配新的栈空间: