欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解go中的defer鏈如何被遍歷執(zhí)行

 更新時間:2024年01月16日 10:45:02   作者:余塵雨晨  
為了在退出函數(shù)前執(zhí)行一些資源清理的操作,例如關閉文件、釋放連接、釋放鎖資源等,會在函數(shù)里寫上多個defer語句,多個_defer 結構體形成一個鏈表,G 結構體中某個字段指向此鏈表,那么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語言make和new關鍵字的區(qū)別

    GO語言make和new關鍵字的區(qū)別

    本篇文章來介紹一道非常常見的面試題,到底有多常見呢?可能很多面試的開場白就是由此開始的。那就是?new?和?make?這兩個內置函數(shù)的區(qū)別,希望對大家有所幫助
    2023-04-04
  • Go初學者踩坑之go?mod?init與自定義包的使用

    Go初學者踩坑之go?mod?init與自定義包的使用

    go?mod是go的一個模塊管理工具,用來代替?zhèn)鹘y(tǒng)的GOPATH方案,下面這篇文章主要給大家介紹了關于Go初學者踩坑之go?mod?init與自定義包的使用,需要的朋友可以參考下
    2022-10-10
  • Go語言中的自定義函數(shù)類型的實現(xiàn)

    Go語言中的自定義函數(shù)類型的實現(xiàn)

    在Go語言中,函數(shù)類型是一種將函數(shù)作為值的數(shù)據(jù)類型,本文主要介紹了Go語言中的自定義函數(shù)類型,具有一定的參考價值,感興趣的可以了解一下
    2023-09-09
  • Go語言大揭秘:適用于哪些類型的項目開發(fā)?

    Go語言大揭秘:適用于哪些類型的項目開發(fā)?

    想知道Go編程語言適合開發(fā)哪些類型的項目嗎?無論是網(wǎng)絡服務、分布式系統(tǒng)還是嵌入式設備,Go都能輕松應對,本文將帶你了解Go在各種場景下的應用,讓你更好地選擇和使用Go進行開發(fā),需要的朋友可以參考下
    2024-01-01
  • 詳解go語言中sort如何排序

    詳解go語言中sort如何排序

    我們的代碼業(yè)務中很多地方需要我們自己進行排序操作,本文主要介紹了詳解go語言中sort如何排序,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • 解決vscode中golang插件依賴安裝失敗問題

    解決vscode中golang插件依賴安裝失敗問題

    這篇文章主要介紹了解決vscode中golang插件依賴安裝失敗問題,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-08-08
  • 使用Go實現(xiàn)一個百行聊天服務器的示例代碼

    使用Go實現(xiàn)一個百行聊天服務器的示例代碼

    前段時間, redis作者整了個c語言版本的聊天服務器,代碼量攏共不過百行,于是, 心血來潮下, 我也整了個Go語言版本, 簡單來說就是實現(xiàn)了一個聊天室的功能,文中通過代碼示例給大家介紹的非常詳細,需要的朋友可以參考下
    2023-12-12
  • Golang中類型轉換利器cast庫的用法詳解

    Golang中類型轉換利器cast庫的用法詳解

    cast庫是一個簡潔而強大的第三方庫,它的主要功能是實現(xiàn)類型之間的安全轉換,而在Golang開發(fā)中,類型轉換是一個常見且不可避免的過程,下面我們就來看看cast庫在Golang中的具體應用吧
    2024-11-11
  • Go語言之io.ReadAtLeast函數(shù)的基本使用和原理解析

    Go語言之io.ReadAtLeast函數(shù)的基本使用和原理解析

    io.ReadAtLeast函數(shù)是Go語言標準庫提供的一個工具函數(shù),能夠從數(shù)據(jù)源讀取至少指定數(shù)量的字節(jié)數(shù)據(jù)到緩沖區(qū)中,這篇文章主要介紹了io.ReadAtLeast函數(shù)的相關知識,需要的朋友可以參考下
    2023-07-07
  • GoLang OS包以及File類型詳細講解

    GoLang OS包以及File類型詳細講解

    go中對文件和目錄的操作主要集中在os包中,下面對go中用到的對文件和目錄的操作,做一個總結筆記。在go中的文件和目錄涉及到兩種類型,一個是type File struct,另一個是type Fileinfo interface
    2023-03-03

最新評論