詳解go中的defer鏈如何被遍歷執(zhí)行
go中的defer 鏈如何被遍歷執(zhí)行
為了在退出函數(shù)前執(zhí)行一些資源清理的操作,例如關閉文件、釋放連接、釋放鎖資源等,會在函數(shù)里寫上多個 defer 語句,被 defered 的函數(shù),以“先進后出”的順序,在 RET 指令前得以執(zhí)行。
在一條函數(shù)調用鏈中,多個函數(shù)中會出現(xiàn)多個 defer 語句。例如:a()→b()→c() 中,每個函數(shù)里都有 defer 語句,而這些 defer 語句會創(chuàng)建對應個數(shù)的 _defer 結構體,這些結構體以鏈表的形式“掛”在 G 結構體下??雌饋硐襁@樣,如圖 2-1 所示。
多個 _defer 結構體形成一個鏈表,G 結構體中某個字段指向此鏈表。在編譯器的“加持下”,defer 語句會先調用deferporc 函數(shù),new 一個_defer 結構體,掛到 G 上。當然,調用 new 之前會優(yōu)先從當前 G 所綁定的 P 的 defer pool 里取,沒取到則會去全局的defer pool 里取,實在沒有的話才新建一個。這是 Go runtime 里非常常見的操作,即設置多級緩存,提升運行效率。
在執(zhí)行 RET 指令之前(注意不是 return 之前),調用 deferreturn 函數(shù)完成 _defer 鏈表的遍歷,執(zhí)行完這條鏈上所有被 defered 的函數(shù)(如關閉文件、釋放連接、釋放鎖資源等)。在deferreturn 函數(shù)的最后,會使用 jmpdefer 跳轉到之前被 defered 的函數(shù),這時控制權從 runtime 轉移到了用戶自定義的函數(shù)。這只是執(zhí)行了一個被 defered 的函數(shù),那這條鏈上其他的被 defered 的函數(shù),該如何得到執(zhí)行?
答案就是控制權會再次交給 runtime,并再次執(zhí)行 deferreturn 函數(shù),完成 defer 鏈表的遍歷。那這一切是如何完成的?
這就要從 Go 匯編的棧幀說起了。先看一個匯編函數(shù)的聲明:
TEXT runtime·gogo(SB), NOSPLIT, $16-8
最后兩個數(shù)字分別表示:①gogo 函數(shù)的棧幀大小為 16B,即函數(shù)的局部變量和為調用子函數(shù)準備的參數(shù)、返回值共需要 16B 的??臻g;②參數(shù)和返回值的大小加起來是 8B。實際上,gogo 函數(shù)的聲明是這樣的:
// func gogo(buf *gobuf)
參數(shù)及返回值的大小是給調用者“看”的,調用者根據(jù)這個數(shù)字可以構造棧:準備好被調函數(shù)需要的參數(shù)及返回值。
典型的函數(shù)調用場景下參數(shù)布局圖如圖 2-2 所示:
● 圖 2-2 函數(shù)調用參數(shù)布局
圖 2-2 中左半部分,主調函數(shù)準備好調用子函數(shù)的參數(shù)及返回值,執(zhí)行 CALL 指令,將返回地址 return address 壓入棧頂,相當于執(zhí)行了 PUSH IP。之后,將 BP 寄存器的值入棧,相當于執(zhí)行了 PUSH BP,再 JMP 到被調函數(shù)。BP 指的是棧基址指針,SP 是指棧頂指針。
圖中 return address 表示子函數(shù)執(zhí)行完畢后,返回到上層函數(shù)中調用子函數(shù)語句的下一條要執(zhí)行的指令,它屬于 caller 的棧幀;而調用者的 BP 則屬于被調函數(shù)的棧幀。
子函數(shù)執(zhí)行完畢后,執(zhí)行 RET 指令:首先將子函數(shù)棧底部的值(圖 2-2 中的調用者的 BP)賦到 CPU 的 BP 寄存器中,于是 BP 指向上層函數(shù)的 BP;再將 return address 賦到 IP 寄存器中,這時 SP 回到左圖所示的位置。相當于還原了整個調用子函數(shù)的現(xiàn)場,好像一切都沒發(fā)生過,而實際上“調用子函數(shù)的返回值”已經(jīng)被填充上了正確的值,例如兩個數(shù)相加的結果;接著,CPU 繼續(xù)執(zhí)行 IP 寄存器里的下一條指令。
再回到 defer 上來,其實在構造 _defer 結構體的時候,需要將當前函數(shù)的 SP、被 defered 的函數(shù)指針保存到 _defer 結構體中。并且會將被 defered 的函數(shù)所需要的參數(shù)復制到和 _defer 結構體相鄰的位置。最終在調用被 defered 的函數(shù)的時候,用的就是這時被復制的值,相當于使用了它的一個“快照”,如果此參數(shù)不是指針或引用類型的話,會產(chǎn)生一些意料之外的 bug。
最后,在 deferreturn 函數(shù)里,遍歷 _defer 鏈表,這些被 defered 的函數(shù)得以執(zhí)行,_defer 鏈表也會被逐漸“消耗”完。
來看一個例子:
package main import "fmt" func sum(a, b int) { c := a + b fmt.Println("sum:", c) } func f(a, b int) { defer sum(a, b) fmt.Printf("a: %d, b: %d\n", a, b) } func main() { a, b := 1, 2 f(a, b) }
執(zhí)行完 f 函數(shù)時,最終會進入 deferreturn 函數(shù):
// src/runtime/panic.go func deferreturn(arg0 uintptr) { gp := getg() d := gp._defer if d == nil { return } ...... switch d.siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d)) default: memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) // 移動參數(shù) } fn := d.fn d.fn = nil gp._defer = d.link freedefer(d) _ = fn.fn jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) }
因為是在遍歷 _defer 鏈表,所以得加入一個終止的條件:
d := gp._defer if d == nil { return }
當 _defer 鏈表為空的時候,終止遍歷。在后面的代碼里會看到,每執(zhí)行完一個被 defered 的函數(shù)后,都會將 _defer 結構體從鏈表中刪除并回收,所以 _defer 鏈表會越來越短,直至為空。
函數(shù)中 switch 語句里要做的就是準備好被 defered 的函數(shù)(例子中就是 sum 函數(shù))所需要的a、b 兩個 int 型參數(shù)。參數(shù)從哪來?從 _defer 結構體相鄰的位置,還記得嗎,這是在 deferproc 函數(shù)里復制過去的。deferArgs(d) 返回的就是當時復制的目的地址。那要復制到哪去?答案是:unsafe.Pointer(&arg0)。因為,arg0 是 deferreturn 函數(shù)的參數(shù),而在 Go 匯編中,一個函數(shù)的參數(shù)是由它的主調函數(shù)準備的。因此 arg0 的地址實際上就是它的上層函數(shù)(在這里就是 f 函數(shù))的棧上放調用子函數(shù)參數(shù)的位置,回憶一下前面講函數(shù)棧幀的圖。函數(shù)的最后,通過 jmpdefer 跳轉到被 defered 的 sum 函數(shù):
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
這里 fn 就是 sum 函數(shù)。核心在于 jmpdefer 所做的事:
// src/runtime/asm_amd64.s TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16 MOVQ fv+0(FP), DX // fn,defer 的函數(shù)的地址 MOVQ argp+8(FP), BX LEAQ -8(BX), SP // caller sp after CALL MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use) SUBQ $5, (SP) // return to CALL again MOVQ 0(DX), BX JMP BX // but first run the deferred function
首先將 sum 函數(shù)的地址放到 DX 寄存器中,最后通過 JMP 指令去執(zhí)行。
MOVQ argp+8(FP), BX LEAQ -8(BX), SP // 執(zhí)行 CALL 指令后 f 函數(shù)的棧頂
這兩行實際上是調整了下當前 SP 寄存器的值,因為 argp+8(FP) 實際上是 jmpdefer 的第二個參數(shù)(它在 deferreturn 函數(shù)中),它指向 f 函數(shù)棧幀中的剛被復制過來的 sum 函數(shù)的參數(shù)。而 -8(BX) 就代表了 f 函數(shù)調用 deferreturn 的返回地址,實際上就是 f 調用 deferreturn 函數(shù)的下一條指令地址。
接著,MOVQ -8(SP), BP 這條指令重置了 BP 寄存器,使它指向了 f 棧幀 的 BP。這樣,SP、BP 寄存器回到了 f 函數(shù)調用 deferreturn 之前的狀態(tài):f 剛準備好調用 deferreturn 的參數(shù),并且把返回值壓棧了。也就相當于拋棄了 deferreturn 函數(shù)的棧幀。
接著 SUBQ $5, (SP) 把返回地址減少了 5B,剛好是一個 CALL 指令的長度。什么意思?當執(zhí)行完 deferreturn 函數(shù)之后,執(zhí)行流程會返回到 CALL deferreturn 的下一條指令,將這個值減少 5B,也就又回到了 CALL deferreturn 指令,從而實現(xiàn)了“遞歸地”調用 deferreturn 函數(shù)的效果。當然,棧卻不會再增長。
上面所描述的整個過程,結合圖 2-3 會更容易理解:
jmpdefer 函數(shù)的最后會執(zhí)行 sum 函數(shù),看起來就像是 f 函數(shù)直接調用 sum 函數(shù)一樣,參數(shù)、返回值都是就緒的。
● 圖 2-3 執(zhí)行 jmpdefer
等到 sum 函數(shù)執(zhí)行完,執(zhí)行流程就會跳轉到 call deferreturn 指令處重新進入 deferreturn 函數(shù),遍歷完所有的_defer 結構體,執(zhí)行完所有的被 defered 的函數(shù),才真正執(zhí)行完 deferretrun 函數(shù),如圖 2-4 所示。
● 圖 2-4 重新調用 deferreturn
綜上所述,實現(xiàn)遍歷 defer 鏈表的關鍵就是 jmpdefer 函數(shù)所做的一些“見不得人”的工作,將調用 deferreturn 函數(shù)的返回地址減少了 5 個字節(jié),使得被 defered 的函數(shù)執(zhí)行完后,又回到CALL deferreturn 指令處,從而實現(xiàn)“遞歸地”調用 deferreturn 函數(shù),完成 _defer 鏈表的遍歷。
以上就是詳解go中的defer鏈如何被遍歷執(zhí)行的詳細內容,更多關于go defer鏈遍歷執(zhí)行的資料請關注腳本之家其它相關文章!
相關文章
Go語言之io.ReadAtLeast函數(shù)的基本使用和原理解析
io.ReadAtLeast函數(shù)是Go語言標準庫提供的一個工具函數(shù),能夠從數(shù)據(jù)源讀取至少指定數(shù)量的字節(jié)數(shù)據(jù)到緩沖區(qū)中,這篇文章主要介紹了io.ReadAtLeast函數(shù)的相關知識,需要的朋友可以參考下2023-07-07