golang核心原理-協程棧
阿新 • • 發佈:2019-12-31
什麼是協程棧
每個協程都需要有自己的棧空間,來存放變數,函式,暫存器等資訊。所以系統需要給協程分配足夠的棧空間。
棧分配方式
固定大小的棧
每個協程都有相同的,固定大小的棧。
優點:實現簡單;
缺點:每個協程需要的棧空間不盡相同,如果一概而論,那麼有些是浪費,有些是不夠用。
建立時指定
由開發者在建立時指定協程棧大小。java,c++在建立執行緒時可以指定其棧大小。
優點:實現簡單
缺點:對開發者要求比較高,需要根據棧變數,請求量預估。但是有些場景不太好預估,比如遞迴呼叫,這種情況通常只能往大的估計。
Segmented stacks
分配和釋法額外的記憶體空間。初始分配的比較小的空間,如4k。不夠了再增加,用完即釋放。以下是一個例子:
優點:動態擴充套件,初始成本小,可以將協程當作廉價資源使用。
缺點:存在熱分裂問題(hot split problem)。
Stack copying
動態擴充套件,分配更大的記憶體,做指標遷移。
缺點:由於通常以2倍擴充套件,當請求量密集,記憶體敏感的情況下,記憶體會消耗比較多,容易oom,當然,通常的業務量是ok的,不會有任何問題。同時100w連線才要考慮優化。
golang 棧分配方式
1.3之前採用的是Segmented stacks的方式。之後採用的Stack copying,也叫continuous stack(連續棧)
棧擴容
觸發時機
執行時,發現棧不夠用了
關鍵步驟
- 將狀態從 _Grunning 更新至 _Gcopystack
- 計算出需要申請的資料大小
- copystack,進行棧複製,後面會詳細分析
- 將協程狀態恢復至_Grunning
- 走一遍協程排程
關鍵原始碼
func newstack() {
thisg := getg()
......
gp := thisg.m.curg
......
// Allocate a bigger segment and move the stack.
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize * 2 // 比原來大一倍
......
// The goroutine must be executing in order to call newstack,// so it must be Grunning (or Gscanrunning).
casgstatus(gp,_Grunning,_Gcopystack) //修改協程狀態
// The concurrent GC will not scan the stack while we are doing
// the copy since the gp is in a Gcopystack status.
copystack(gp,newsize,true) //在下面會講到
......
casgstatus(gp,_Gcopystack,_Grunning)
gogo(&gp.sched)
}
複製程式碼
棧縮容
觸發時機
gc進行時,非執行中協程,棧使用不超過1/4的,會縮容為原來1/2
關鍵步驟
- 檢查協程狀態,如果已經結束,則釋放空間
- 確定新空間size,目前為原來1/2
- 檢查棧使用是否超過1/4,若沒有,則放棄
- copystack,進行棧複製,後面會詳細分析
關鍵原始碼
func shrinkstack(gp *g) {
gstatus := readgstatus(gp)
if gstatus&^_Gscan == _Gdead {
if gp.stack.lo != 0 {
// Free whole stack - it will get reallocated
// if G is used again.
stackfree(gp.stack)
gp.stack.lo = 0
gp.stack.hi = 0
}
return
}
......
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize / 2 // 比原來小1倍
if newsize < _FixedStack {
return
}
// Compute how much of the stack is currently in use and only
// shrink the stack if gp is using less than a quarter of its
// current stack. The currently used stack includes everything
// down to the SP plus the stack guard space that ensures
// there's room for nosplit functions.
avail := gp.stack.hi - gp.stack.lo
//當已使用的棧佔不到總棧的1/4 進行縮容
if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 {
return
}
copystack(gp,false) //在下面會講到
}
複製程式碼
copystack棧拷貝過程
原來內容上的拷貝
關鍵步驟
- 申請新的棧空間:new := stackalloc(uint32(newsize));
- 調整指標指向,將sudog,ctx等,指向新位置,計算方式為原地址+delta(delta為new.hi-old.hi);
- gentraceback,調整棧幀到新位置;
- memmove老棧資料到新棧;
- 刪除老棧。
func copystack(gp *g,newsize uintptr,sync bool) {
......
old := gp.stack
......
used := old.hi - gp.sched.sp
// allocate new stack
new := stackalloc(uint32(newsize))
......
// Compute adjustment.
var adjinfo adjustinfo
adjinfo.old = old
adjinfo.delta = new.hi - old.hi //用於舊棧指標的調整
//後面有機會和 select / chan 一起分析
// Adjust sudogs,synchronizing with channel ops if necessary.
ncopy := used
if sync {
adjustsudogs(gp,&adjinfo)
} else {
......
adjinfo.sghi = findsghi(gp,old)
// Synchronize with channel ops and copy the part of
// the stack they may interact with.
ncopy -= syncadjustsudogs(gp,used,&adjinfo)
}
//把舊棧資料複製到新棧
// Copy the stack (or the rest of it) to the new location
memmove(unsafe.Pointer(new.hi-ncopy),unsafe.Pointer(old.hi-ncopy),ncopy)
// Adjust remaining structures that have pointers into stacks.
// We have to do most of these before we traceback the new
// stack because gentraceback uses them.
adjustctxt(gp,&adjinfo)
adjustdefers(gp,&adjinfo)
adjustpanics(gp,&adjinfo)
......
// Swap out old stack for new one
gp.stack = new
gp.stackguard0 = new.lo + _StackGuard // NOTE: might clobber a preempt request
gp.sched.sp = new.hi - used
gp.stktopsp += adjinfo.delta
// Adjust pointers in the new stack.
gentraceback(^uintptr(0),^uintptr(0),gp,nil,0x7fffffff,adjustframe,noescape(unsafe.Pointer(&adjinfo)),0)
......
//釋放舊棧
stackfree(old)
}
複製程式碼
棧幀調整
golang棧幀
package main
func myFunction(a,b int) (int,int) {
return a + b,a - b
}
func main() {
myFunction(66,77)
}
複製程式碼
棧幀調整
gentraceback裡回撥了adjustframe函式,我們所需要了解的即golang的棧空間中,有存放函式引數,返回值,函式返回地址等資訊,這些地址都需要調節,該函式就是針對原來的棧指標進行的調節。程式碼如下:
// Note: the argument/return area is adjusted by the callee.
func adjustframe(frame *stkframe,arg unsafe.Pointer) bool {
adjinfo := (*adjustinfo)(arg)
targetpc := frame.continpc
if targetpc == 0 {
// Frame is dead.
return true
}
f := frame.fn
.........
pcdata := pcdatavalue(f,_PCDATA_StackMapIndex,targetpc,&adjinfo.cache)
if pcdata == -1 {
pcdata = 0 // in prologue
}
// Adjust local variables if stack frame has been allocated.
size := frame.varp - frame.sp
var minsize uintptr
switch sys.ArchFamily {
case sys.ARM64:
minsize = sys.SpAlign
default:
minsize = sys.MinFrameSize
}
if size > minsize {
var bv bitvector
stackmap := (*stackmap)(funcdata(f,_FUNCDATA_LocalsPointerMaps))
if stackmap == nil || stackmap.n <= 0 {
print("runtime: frame ",funcname(f)," untyped locals ",hex(frame.varp-size),"+",hex(size),"\n")
throw("missing stackmap")
}
// Locals bitmap information,scan just the pointers in locals.
if pcdata < 0 || pcdata >= stackmap.n {
print("runtime: pcdata is ",pcdata," and ",stackmap.n," locals stack map entries for "," (targetpc=",")\n")
throw("bad symbol table")
}
bv = stackmapdata(stackmap,pcdata)
size = uintptr(bv.n) * sys.PtrSize
if stackDebug >= 3 {
print(" locals ","/"," ",size/sys.PtrSize," words ",bv.bytedata,"\n")
}
adjustpointers(unsafe.Pointer(frame.varp-size),&bv,adjinfo,f)
}
// Adjust saved base pointer if there is one.
if sys.ArchFamily == sys.AMD64 && frame.argp-frame.varp == 2*sys.RegSize {
if !framepointer_enabled {
print("runtime: found space for saved base pointer,but no framepointer experiment\n")
print("argp=",hex(frame.argp)," varp=",hex(frame.varp),"\n")
throw("bad frame layout")
}
if stackDebug >= 3 {
print(" saved bp\n")
}
if debugCheckBP {
// Frame pointers should always point to the next higher frame on
// the Go stack (or be nil,for the top frame on the stack).
bp := *(*uintptr)(unsafe.Pointer(frame.varp))
if bp != 0 && (bp < adjinfo.old.lo || bp >= adjinfo.old.hi) {
println("runtime: found invalid frame pointer")
print("bp=",hex(bp)," min=",hex(adjinfo.old.lo)," max=",hex(adjinfo.old.hi),"\n")
throw("bad frame pointer")
}
}
adjustpointer(adjinfo,unsafe.Pointer(frame.varp))
}
// Adjust arguments.
if frame.arglen > 0 {
var bv bitvector
if frame.argmap != nil {
bv = *frame.argmap
} else {
stackmap := (*stackmap)(funcdata(f,_FUNCDATA_ArgsPointerMaps))
if stackmap == nil || stackmap.n <= 0 {
print("runtime: frame "," untyped args ",frame.argp,frame.arglen,"\n")
throw("missing stackmap")
}
if pcdata < 0 || pcdata >= stackmap.n {
print("runtime: pcdata is "," args stack map entries for ",")\n")
throw("bad symbol table")
}
bv = stackmapdata(stackmap,pcdata)
}
if stackDebug >= 3 {
print("args\n")
}
adjustpointers(unsafe.Pointer(frame.argp),funcInfo{})
}
return true
}
複製程式碼