深刻理解Go-runtime.SetFinalizer原理剖析

finalizer是與對象關聯的一個函數,經過runtime.SetFinalizer 來設置,它在對象被GC的時候,這個finalizer會被調用,以完成對象生命中最後一程。因爲finalizer的存在,致使了對象在三色標記中,不可能被標爲白色對象,也就是垃圾,因此,這個對象的生命也會得以延續一個GC週期。正如defer同樣,咱們也能夠經過 Finalizer 完成一些相似於資源釋放的操做segmentfault

1. 結構概覽

1.1. heap

type mspan struct {
	// 當前span上全部對象的special串成鏈表
	// special中有個offset,就是數據對象在span上的offset,經過offset,將數據對象和special關聯起來
	specials    *special   // linked list of special records sorted by offset.
}
複製代碼

1.2. special

type special struct {
	next   *special // linked list in span
	// 數據對象在span上的offset
	offset uint16   // span offset of object
	kind   byte     // kind of special
}
複製代碼

##1.3. specialfinalizer數組

type specialfinalizer struct {
	special special
	fn      *funcval // May be a heap pointer.
	// return的數據的大小
	nret    uintptr
	// 第一個參數的類型
	fint    *_type   // May be a heap pointer, but always live.
	// 與finalizer關聯的數據對象的指針類型
	ot      *ptrtype // May be a heap pointer, but always live.
}
複製代碼

1.4. finalizer

type finalizer struct {
	fn   *funcval       // function to call (may be a heap pointer)
	arg  unsafe.Pointer // ptr to object (may be a heap pointer)
	nret uintptr        // bytes of return values from fn
	fint *_type         // type of first argument of fn
	ot   *ptrtype       // type of ptr to object (may be a heap pointer)
}
複製代碼

1.5. 全局變量

var finlock mutex  // protects the following variables
// 運行finalizer的g,只有一個g,不用的時候休眠,須要的時候再喚醒
var fing *g        // goroutine that runs finalizers
// finalizer的全局隊列,這裏是已經設置的finalizer串成的鏈表
var finq *finblock // list of finalizers that are to be executed
// 已經釋放的finblock的鏈表,用finc緩存起來,之後須要使用的時候能夠直接取走,避免再走一遍內存分配了
var finc *finblock // cache of free blocks
var finptrmask [_FinBlockSize / sys.PtrSize / 8]byte
var fingwait bool  // fing的標誌位,經過 fingwait和fingwake,來肯定是否須要喚醒fing
var fingwake bool
// 全部的blocks串成的鏈表
var allfin *finblock // list of all blocks
複製代碼

2. 源碼分析

2.1. 建立finalizer

2.1.1. main

func main() {
	// i 就是後面說的 數據對象
	var i = 3
	// 這裏的func 就是後面一直說的 finalizer
	runtime.SetFinalizer(&i, func(i *int) {
		fmt.Println(i, *i, "set finalizer")
	})
	time.Sleep(time.Second * 5)
}
複製代碼

2.1.2. SetFinalizer

根據 數據對象 ,生成一個special對象,並綁定到 數據對象 所在的span,串聯到span.specials上,而且確保fing的存在緩存

func SetFinalizer(obj interface{}, finalizer interface{}) {
	if debug.sbrk != 0 {
		// debug.sbrk never frees memory, so no finalizers run
		// (and we don't have the data structures to record them).
		return
	}
	e := efaceOf(&obj)
	etyp := e._type
	// ---- 省略數據校驗的邏輯 ---
	ot := (*ptrtype)(unsafe.Pointer(etyp))

	// find the containing object
	// 在內存中找不到分配的地址時 base==0,setFinalizer 是在內存回收的時候調用,沒有分配就不會回收
	base, _, _ := findObject(uintptr(e.data), 0, 0)

	f := efaceOf(&finalizer)
	ftyp := f._type
	// 若是 finalizer type == nil,嘗試移除(沒有的話,就不須要移除了)
	if ftyp == nil {
		// switch to system stack and remove finalizer
		systemstack(func() {
			removefinalizer(e.data)
		})
		return
	}
	// --- 對finalizer參數數量及類型進行校驗 --
	if ftyp.kind&kindMask != kindFunc {
		throw("runtime.SetFinalizer: second argument is " + ftyp.string() + ", not a function")
	}
	ft := (*functype)(unsafe.Pointer(ftyp))
	if ft.dotdotdot() {
		throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string() + " because dotdotdot")
	}
	if ft.inCount != 1 {
		throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string())
	}
	fint := ft.in()[0]
	switch {
	case fint == etyp:
		// ok - same type
		goto okarg
	case fint.kind&kindMask == kindPtr:
		if (fint.uncommon() == nil || etyp.uncommon() == nil) && (*ptrtype)(unsafe.Pointer(fint)).elem == ot.elem {
			// ok - not same type, but both pointers,
			// one or the other is unnamed, and same element type, so assignable.
			goto okarg
		}
	case fint.kind&kindMask == kindInterface:
		ityp := (*interfacetype)(unsafe.Pointer(fint))
		if len(ityp.mhdr) == 0 {
			// ok - satisfies empty interface
			goto okarg
		}
		if _, ok := assertE2I2(ityp, *efaceOf(&obj)); ok {
			goto okarg
		}
	}
	throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string())
okarg:
	// compute size needed for return parameters
	// 計算返回參數的大小並進行對齊
	nret := uintptr(0)
	for _, t := range ft.out() {
		nret = round(nret, uintptr(t.align)) + uintptr(t.size)
	}
	nret = round(nret, sys.PtrSize)

	// make sure we have a finalizer goroutine
	// 確保 finalizer 有一個 goroutine
	createfing()

	systemstack(func() {
		// 卻換到g0,添加finalizer,而且不能重複設置
		if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {
			throw("runtime.SetFinalizer: finalizer already set")
		}
	})
}
複製代碼

這裏邏輯沒什麼複雜的,只是在參數、類型的判斷等上面,比較的麻煩markdown

2.1.3. removefinalizer

經過removespecial,找到數據對象p所對應的special對象,若是找到的話,釋放mheap上對應的內存函數

func removefinalizer(p unsafe.Pointer) {
	// 根據數據p找到對應的special對象
	s := (*specialfinalizer)(unsafe.Pointer(removespecial(p, _KindSpecialFinalizer)))
	if s == nil {
		return // there wasn't a finalizer to remove
	}
	lock(&mheap_.speciallock)
	// 釋放找到的special所對應的內存
	mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
	unlock(&mheap_.speciallock)
}
複製代碼

這裏的函數,雖然叫removefinalizer, 可是這裏暫時跟finalizer結構體沒有關係,都是在跟special結構體打交道,後面的addfinalizer也是同樣的oop

2.1.4. removespecial

遍歷數據所在的span的specials,若是找到了指定數據p的special的話,就從specials中移除,並返回源碼分析

func removespecial(p unsafe.Pointer, kind uint8) *special {
	// 找到數據p所在的span
	span := spanOfHeap(uintptr(p))
	if span == nil {
		throw("removespecial on invalid pointer")
	}

	// Ensure that the span is swept.
	// Sweeping accesses the specials list w/o locks, so we have
	// to synchronize with it. And it's just much safer.
	mp := acquirem()
	// 保證span被清掃過了
	span.ensureSwept()
	// 獲取數據p的偏移量,根據偏移量去尋找p對應的special
	offset := uintptr(p) - span.base()

	lock(&span.speciallock)
	t := &span.specials
	// 遍歷span.specials這個鏈表
	for {
		s := *t
		if s == nil {
			break
		}
		// This function is used for finalizers only, so we don't check for
		// "interior" specials (p must be exactly equal to s->offset).
		if offset == uintptr(s.offset) && kind == s.kind {
			// 找到了,修改指針,將當前找到的special移除
			*t = s.next
			unlock(&span.speciallock)
			releasem(mp)
			return s
		}
		t = &s.next
	}
	unlock(&span.speciallock)
	releasem(mp)
	// 沒有找到,就返回nil
	return nil
}
複製代碼

2.1.5. addfinalizer

正好跟removefinalizer相反,這個就是根據數據對象p,建立對應的special,而後添加到span.specials鏈表上面學習

func addfinalizer(p unsafe.Pointer, f *funcval, nret uintptr, fint *_type, ot *ptrtype) bool {
	lock(&mheap_.speciallock)
	// 分配出來一塊內存供finalizer使用
	s := (*specialfinalizer)(mheap_.specialfinalizeralloc.alloc())
	unlock(&mheap_.speciallock)
	s.special.kind = _KindSpecialFinalizer
	s.fn = f
	s.nret = nret
	s.fint = fint
	s.ot = ot
	if addspecial(p, &s.special) {

		return true
	}

	// There was an old finalizer
	// 沒有添加成功,是由於p已經有了一個special對象了
	lock(&mheap_.speciallock)
	mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
	unlock(&mheap_.speciallock)
	return false
}
複製代碼

2.1.6. addspecial

這裏是添加special的主邏輯ui

func addspecial(p unsafe.Pointer, s *special) bool {
	span := spanOfHeap(uintptr(p))
	if span == nil {
		throw("addspecial on invalid pointer")
	}
	// 同 removerspecial同樣,確保這個span已經清掃過了
	mp := acquirem()
	span.ensureSwept()

	offset := uintptr(p) - span.base()
	kind := s.kind

	lock(&span.speciallock)

	// Find splice point, check for existing record.
	t := &span.specials
	for {
		x := *t
		if x == nil {
			break
		}
		if offset == uintptr(x.offset) && kind == x.kind {
			// 已經存在了,不能在增長了,一個數據對象,只能綁定一個finalizer
			unlock(&span.speciallock)
			releasem(mp)
			return false // already exists
		}
		if offset < uintptr(x.offset) || (offset == uintptr(x.offset) && kind < x.kind) {
			break
		}
		t = &x.next
	}

	// Splice in record, fill in offset.
	// 添加到 specials 隊列尾
	s.offset = uint16(offset)
	s.next = *t
	*t = s
	unlock(&span.speciallock)
	releasem(mp)

	return true
}
複製代碼

2.1.7. createfing

這個函數是保證,建立了finalizer以後,有一個goroutine去運行,這裏只運行一次,這個goroutine會由全局變量 fing 記錄this

func createfing() {
	// start the finalizer goroutine exactly once
	// 進建立一個goroutine,進行時刻監控運行
	if fingCreate == 0 && atomic.Cas(&fingCreate, 0, 1) {
		// 開啓一個goroutine運行
		go runfinq()
	}
}
複製代碼

2.2. 執行finalizer

在上面的 createfing 的會嘗試建立一個goroutine去執行,接下來就分析一下執行流程吧

func runfinq() {
	var (
		frame    unsafe.Pointer
		framecap uintptr
	)

	for {
		lock(&finlock)
		// 獲取finq 全局隊列,並清空全局隊列
		fb := finq
		finq = nil
		if fb == nil {
			// 若是全局隊列爲空,休眠當前g,等待被喚醒
			gp := getg()
			fing = gp
			// 設置fing的狀態標誌位
			fingwait = true
			goparkunlock(&finlock, waitReasonFinalizerWait, traceEvGoBlock, 1)
			continue
		}
		unlock(&finlock)
		// 循環執行runq鏈表裏的fin數組
		for fb != nil {
			for i := fb.cnt; i > 0; i-- {
				f := &fb.fin[i-1]
				// 獲取存儲當前finalizer的返回數據的大小,若是比以前大,則分配
				framesz := unsafe.Sizeof((interface{})(nil)) + f.nret
				if framecap < framesz {
					// The frame does not contain pointers interesting for GC,
					// all not yet finalized objects are stored in finq.
					// If we do not mark it as FlagNoScan,
					// the last finalized object is not collected.
					frame = mallocgc(framesz, nil, true)
					framecap = framesz
				}

				if f.fint == nil {
					throw("missing type in runfinq")
				}
				// frame is effectively uninitialized
				// memory. That means we have to clear
				// it before writing to it to avoid
				// confusing the write barrier.
				// 清空frame內存存儲
				*(*[2]uintptr)(frame) = [2]uintptr{}
				switch f.fint.kind & kindMask {
				case kindPtr:
					// direct use of pointer
					*(*unsafe.Pointer)(frame) = f.arg
				case kindInterface:
					ityp := (*interfacetype)(unsafe.Pointer(f.fint))
					// set up with empty interface
					(*eface)(frame)._type = &f.ot.typ
					(*eface)(frame).data = f.arg
					if len(ityp.mhdr) != 0 {
						// convert to interface with methods
						// this conversion is guaranteed to succeed - we checked in SetFinalizer
						*(*iface)(frame) = assertE2I(ityp, *(*eface)(frame))
					}
				default:
					throw("bad kind in runfinq")
				}
				// 調用finalizer函數
				fingRunning = true
				reflectcall(nil, unsafe.Pointer(f.fn), frame, uint32(framesz), uint32(framesz))
				fingRunning = false

				// Drop finalizer queue heap references
				// before hiding them from markroot.
				// This also ensures these will be
				// clear if we reuse the finalizer.
				// 清空finalizer的屬性
				f.fn = nil
				f.arg = nil
				f.ot = nil
				atomic.Store(&fb.cnt, i-1)
			}
			// 將已經完成的finalizer放入finc以做緩存,避免再次分配內存
			next := fb.next
			lock(&finlock)
			fb.next = finc
			finc = fb
			unlock(&finlock)
			fb = next
		}
	}
}
複製代碼

看完上面的流程的時候,忽然發現有點懵逼

  1. 全局隊列finq中是何時被插入數據 finalizer的?
  2. g若是休眠了,那怎麼被喚醒呢?

先針對第一個問題分析:

插入隊列的操做,要追溯到咱們以前分析的GC 深刻理解Go-垃圾回收機制 了,在sweep 中有下面一段函數

2.2.1. sweep

func (s *mspan) sweep(preserve bool) bool {
	....
	specialp := &s.specials
	special := *specialp
	for special != nil {
		....
		if special.kind == _KindSpecialFinalizer || !hasFin {
			// Splice out special record.
			y := special
			special = special.next
			*specialp = special
			// 加入全局finq隊列的入口就在這裏了
			freespecial(y, unsafe.Pointer(p), size)
		}
		....
	}
	....
}
複製代碼

2.2.2. freespecial

在gc的時候,不只要把special對應的內存釋放掉,並且把specials整理建立對應dinalizer對象,並插入到 finq隊列裏面

func freespecial(s *special, p unsafe.Pointer, size uintptr) {
	switch s.kind {
	case _KindSpecialFinalizer:
		// 把這個finalizer加入到全局隊列
		sf := (*specialfinalizer)(unsafe.Pointer(s))
		queuefinalizer(p, sf.fn, sf.nret, sf.fint, sf.ot)
		lock(&mheap_.speciallock)
		mheap_.specialfinalizeralloc.free(unsafe.Pointer(sf))
		unlock(&mheap_.speciallock)
	// 下面兩種狀況不在分析範圍內,省略
	case _KindSpecialProfile:
		sp := (*specialprofile)(unsafe.Pointer(s))
		mProf_Free(sp.b, size)
		lock(&mheap_.speciallock)
		mheap_.specialprofilealloc.free(unsafe.Pointer(sp))
		unlock(&mheap_.speciallock)
	default:
		throw("bad special kind")
		panic("not reached")
	}
}
複製代碼

2.2.3. queuefinalizer

func queuefinalizer(p unsafe.Pointer, fn *funcval, nret uintptr, fint *_type, ot *ptrtype) {
	lock(&finlock)
	// 若是finq爲空或finq的內部數組已經滿了,則從finc或從新分配 來獲取block並插入到finq的鏈表頭
	if finq == nil || finq.cnt == uint32(len(finq.fin)) {
		if finc == nil {
			finc = (*finblock)(persistentalloc(_FinBlockSize, 0, &memstats.gc_sys))
			finc.alllink = allfin
			allfin = finc
			if finptrmask[0] == 0 {
				// Build pointer mask for Finalizer array in block.
				// Check assumptions made in finalizer1 array above.
				if (unsafe.Sizeof(finalizer{}) != 5*sys.PtrSize ||
					unsafe.Offsetof(finalizer{}.fn) != 0 ||
					unsafe.Offsetof(finalizer{}.arg) != sys.PtrSize ||
					unsafe.Offsetof(finalizer{}.nret) != 2*sys.PtrSize ||
					unsafe.Offsetof(finalizer{}.fint) != 3*sys.PtrSize ||
					unsafe.Offsetof(finalizer{}.ot) != 4*sys.PtrSize) {
					throw("finalizer out of sync")
				}
				for i := range finptrmask {
					finptrmask[i] = finalizer1[i%len(finalizer1)]
				}
			}
		}
		// 從finc中移除並獲取鏈表頭
		block := finc
		finc = block.next
		// 將從finc獲取到的鏈表掛載到finq的隊列頭,finq指向新的block
		block.next = finq
		finq = block
	}
	// 根據finq.cnt獲取索引對應的block
	f := &finq.fin[finq.cnt]
	atomic.Xadd(&finq.cnt, +1) // Sync with markroots
	// 設置相關屬性
	f.fn = fn
	f.nret = nret
	f.fint = fint
	f.ot = ot
	f.arg = p
	// 設置喚醒標誌
	fingwake = true
	unlock(&finlock)
}
複製代碼

至此,也就明白了,runq全局隊列是怎麼被填充的了

那麼,第二個問題,當fing被休眠後,怎麼被喚醒呢?

這裏就須要追溯到,深刻理解Go-goroutine的實現及Scheduler分析 這篇文章了

2.2.4. findrunnable

在 findrunnable 中有一段代碼以下:

func findrunnable() (gp *g, inheritTime bool) {
	// 經過狀態位判斷是否須要喚醒 fing, 經過wakefing來判斷並返回fing
	if fingwait && fingwake {
		if gp := wakefing(); gp != nil {
			// 喚醒g,並從休眠出繼續執行
			ready(gp, 0, true)
		}
	}
}
複製代碼

2.2.5. wakefing

這裏不只會對狀態位 fingwait fingwake作二次判斷,並且,若是狀態位符合喚醒要求的話,須要重置兩個狀態位

func wakefing() *g {
	var res *g
	lock(&finlock)
	if fingwait && fingwake {
		fingwait = false
		fingwake = false
		res = fing
	}
	unlock(&finlock)
	return res
}
複製代碼

3. 參考文檔

  • 《Go語言學習筆記》--雨痕
相關文章
相關標籤/搜索