golang核心原理-協程棧

什麼是協程棧

每一個協程都須要有本身的棧空間,來存放變量,函數,寄存器等信息。因此係統須要給協程分配足夠的棧空間。java

棧分配方式

固定大小的棧

每一個協程都有相同的,固定大小的棧。c++

優勢:實現簡單;golang

缺點:每一個協程須要的棧空間不盡相同,若是一律而論,那麼有些是浪費,有些是不夠用。bash

建立時指定

由開發者在建立時指定協程棧大小。java, c++在建立線程時能夠指定其棧大小。less

優勢:實現簡單函數

缺點:對開發者要求比較高,須要根據棧變量,請求量預估。可是有些場景不太好預估,好比遞歸調用,這種狀況一般只能往大的估計。優化

Segmented stacks

分配和釋法額外的內存空間。初始分配的比較小的空間,如4k。不夠了再增長,用完即釋放。如下是一個例子: ui

當G調用H的時候,沒有足夠的棧空間來讓H運行,這時候Go運行環境就會從堆裏分配一個新的棧內存塊去讓H運行。在H返回到G以前,新分配的內存塊被釋放回堆。這種管理棧的方法通常都工做得很好。但對有些代碼,特別是遞歸調用,它會形成程序不停地分配和釋放新的內存空間。舉個例子,在一個程序裏,函數G會在一個循環裏調用不少次H函數。每次調用都會分配一塊新的內存空間。這就是熱分裂問題(hot split problem)。

優勢:動態擴展,初始成本小,能夠將協程看成廉價資源使用。this

缺點:存在熱分裂問題(hot split problem)。spa

Stack copying

動態擴展,分配更大的內存,作指針遷移。

優勢:動態擴展,初始成本小,能夠將協程看成廉價資源使用,且不存在hot split problem問題

缺點:因爲一般以2倍擴展,當請求量密集,內存敏感的狀況下,內存會消耗比較多,容易oom,固然,一般的業務量是ok的,不會有任何問題。同時100w鏈接纔要考慮優化。

golang 棧分配方式

1.3以前採用的是Segmented stacks的方式。以後採用的Stack copying,也叫continuous stack(連續棧)

棧擴容

觸發時機

運行時,發現棧不夠用了

關鍵步驟

  1. 將狀態從 _Grunning 更新至 _Gcopystack
  2. 計算出須要申請的數據大小
  3. copystack,進行棧複製,後面會詳細分析
  4. 將協程狀態恢復至_Grunning
  5. 走一遍協程調度

關鍵源碼

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

關鍵步驟

  1. 檢查協程狀態,若是已經結束,則釋放空間
  2. 肯定新空間size,目前爲原來1/2
  3. 檢查棧使用是否超過1/4,若沒有,則放棄
  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, newsize, false) //在下面會講到 } 複製代碼

copystack棧拷貝過程

原來內容上的拷貝

關鍵步驟

  1. 申請新的棧空間:new := stackalloc(uint32(newsize));
  2. 調整指針指向,將sudog,ctx等,指向新位置,計算方式爲原地址+delta(delta爲new.hi-old.hi);
  3. gentraceback,調整棧幀到新位置;
  4. memmove老棧數據到新棧;
  5. 刪除老棧。
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), 0, gp, 0, 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 ", funcname(f), " (targetpc=", targetpc, ")\n")
			throw("bad symbol table")
		}
		bv = stackmapdata(stackmap, pcdata)
		size = uintptr(bv.n) * sys.PtrSize
		if stackDebug >= 3 {
			print(" locals ", pcdata, "/", stackmap.n, " ", 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 ", funcname(f), " untyped args ", frame.argp, "+", frame.arglen, "\n")
				throw("missing stackmap")
			}
			if pcdata < 0 || pcdata >= stackmap.n {
				print("runtime: pcdata is ", pcdata, " and ", stackmap.n, " args stack map entries for ", funcname(f), " (targetpc=", targetpc, ")\n")
				throw("bad symbol table")
			}
			bv = stackmapdata(stackmap, pcdata)
		}
		if stackDebug >= 3 {
			print("args\n")
		}
		adjustpointers(unsafe.Pointer(frame.argp), &bv, adjinfo, funcInfo{})
	}
	return true
}
複製代碼
相關文章
相關標籤/搜索