最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

 更新時間:2024年01月16日 10:45:02   作者:余塵雨晨  
為了在退出函數(shù)前執(zhí)行一些資源清理的操作,例如關(guān)閉文件、釋放連接、釋放鎖資源等,會在函數(shù)里寫上多個defer語句,多個_defer 結(jié)構(gòu)體形成一個鏈表,G 結(jié)構(gòu)體中某個字段指向此鏈表,那么go中的defer鏈如何被遍歷執(zhí)行,本文將給大家詳細的介紹,感興趣的朋友可以參考下

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

為了在退出函數(shù)前執(zhí)行一些資源清理的操作,例如關(guān)閉文件、釋放連接、釋放鎖資源等,會在函數(shù)里寫上多個 defer 語句,被 defered 的函數(shù),以“先進后出”的順序,在 RET 指令前得以執(zhí)行。

在一條函數(shù)調(diào)用鏈中,多個函數(shù)中會出現(xiàn)多個 defer 語句。例如:a()→b()→c() 中,每個函數(shù)里都有 defer 語句,而這些 defer 語句會創(chuàng)建對應(yīng)個數(shù)的 _defer 結(jié)構(gòu)體,這些結(jié)構(gòu)體以鏈表的形式“掛”在 G 結(jié)構(gòu)體下??雌饋硐襁@樣,如圖 2-1 所示。

多個 _defer 結(jié)構(gòu)體形成一個鏈表,G 結(jié)構(gòu)體中某個字段指向此鏈表。在編譯器的“加持下”,defer 語句會先調(diào)用deferporc 函數(shù),new 一個_defer 結(jié)構(gòu)體,掛到 G 上。當然,調(diào)用 new 之前會優(yōu)先從當前 G 所綁定的 P 的 defer pool 里取,沒取到則會去全局的defer pool 里取,實在沒有的話才新建一個。這是 Go runtime 里非常常見的操作,即設(shè)置多級緩存,提升運行效率。

在執(zhí)行 RET 指令之前(注意不是 return 之前),調(diào)用 deferreturn 函數(shù)完成 _defer 鏈表的遍歷,執(zhí)行完這條鏈上所有被 defered 的函數(shù)(如關(guān)閉文件、釋放連接、釋放鎖資源等)。在deferreturn 函數(shù)的最后,會使用 jmpdefer 跳轉(zhuǎn)到之前被 defered 的函數(shù),這時控制權(quán)從 runtime 轉(zhuǎn)移到了用戶自定義的函數(shù)。這只是執(zhí)行了一個被 defered 的函數(shù),那這條鏈上其他的被 defered 的函數(shù),該如何得到執(zhí)行?

答案就是控制權(quán)會再次交給 runtime,并再次執(zhí)行 deferreturn 函數(shù),完成 defer 鏈表的遍歷。那這一切是如何完成的?
這就要從 Go 匯編的棧幀說起了。先看一個匯編函數(shù)的聲明:

TEXT runtime·gogo(SB), NOSPLIT, $16-8

最后兩個數(shù)字分別表示:①gogo 函數(shù)的棧幀大小為 16B,即函數(shù)的局部變量和為調(diào)用子函數(shù)準備的參數(shù)、返回值共需要 16B 的棧空間;②參數(shù)和返回值的大小加起來是 8B。實際上,gogo 函數(shù)的聲明是這樣的:

// func gogo(buf *gobuf)

參數(shù)及返回值的大小是給調(diào)用者“看”的,調(diào)用者根據(jù)這個數(shù)字可以構(gòu)造棧:準備好被調(diào)函數(shù)需要的參數(shù)及返回值。

典型的函數(shù)調(diào)用場景下參數(shù)布局圖如圖 2-2 所示:

● 圖 2-2 函數(shù)調(diào)用參數(shù)布局

圖 2-2 中左半部分,主調(diào)函數(shù)準備好調(diào)用子函數(shù)的參數(shù)及返回值,執(zhí)行 CALL 指令,將返回地址 return address 壓入棧頂,相當于執(zhí)行了 PUSH IP。之后,將 BP 寄存器的值入棧,相當于執(zhí)行了 PUSH BP,再 JMP 到被調(diào)函數(shù)。BP 指的是?;分羔?,SP 是指棧頂指針。

圖中 return address 表示子函數(shù)執(zhí)行完畢后,返回到上層函數(shù)中調(diào)用子函數(shù)語句的下一條要執(zhí)行的指令,它屬于 caller 的棧幀;而調(diào)用者的 BP 則屬于被調(diào)函數(shù)的棧幀。

子函數(shù)執(zhí)行完畢后,執(zhí)行 RET 指令:首先將子函數(shù)棧底部的值(圖 2-2 中的調(diào)用者的 BP)賦到 CPU 的 BP 寄存器中,于是 BP 指向上層函數(shù)的 BP;再將 return address 賦到 IP 寄存器中,這時 SP 回到左圖所示的位置。相當于還原了整個調(diào)用子函數(shù)的現(xiàn)場,好像一切都沒發(fā)生過,而實際上“調(diào)用子函數(shù)的返回值”已經(jīng)被填充上了正確的值,例如兩個數(shù)相加的結(jié)果;接著,CPU 繼續(xù)執(zhí)行 IP 寄存器里的下一條指令。

再回到 defer 上來,其實在構(gòu)造 _defer 結(jié)構(gòu)體的時候,需要將當前函數(shù)的 SP、被 defered 的函數(shù)指針保存到 _defer 結(jié)構(gòu)體中。并且會將被 defered 的函數(shù)所需要的參數(shù)復(fù)制到和 _defer 結(jié)構(gòu)體相鄰的位置。最終在調(diào)用被 defered 的函數(shù)的時候,用的就是這時被復(fù)制的值,相當于使用了它的一個“快照”,如果此參數(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 結(jié)構(gòu)體從鏈表中刪除并回收,所以 _defer 鏈表會越來越短,直至為空。

函數(shù)中 switch 語句里要做的就是準備好被 defered 的函數(shù)(例子中就是 sum 函數(shù))所需要的a、b 兩個 int 型參數(shù)。參數(shù)從哪來?從 _defer 結(jié)構(gòu)體相鄰的位置,還記得嗎,這是在 deferproc 函數(shù)里復(fù)制過去的。deferArgs(d) 返回的就是當時復(fù)制的目的地址。那要復(fù)制到哪去?答案是:unsafe.Pointer(&arg0)。因為,arg0 是 deferreturn 函數(shù)的參數(shù),而在 Go 匯編中,一個函數(shù)的參數(shù)是由它的主調(diào)函數(shù)準備的。因此 arg0 的地址實際上就是它的上層函數(shù)(在這里就是 f 函數(shù))的棧上放調(diào)用子函數(shù)參數(shù)的位置,回憶一下前面講函數(shù)棧幀的圖。函數(shù)的最后,通過 jmpdefer 跳轉(zhuǎn)到被 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ù)的棧頂

這兩行實際上是調(diào)整了下當前 SP 寄存器的值,因為 argp+8(FP) 實際上是 jmpdefer 的第二個參數(shù)(它在 deferreturn 函數(shù)中),它指向 f 函數(shù)棧幀中的剛被復(fù)制過來的 sum 函數(shù)的參數(shù)。而 -8(BX) 就代表了 f 函數(shù)調(diào)用 deferreturn 的返回地址,實際上就是 f 調(diào)用 deferreturn 函數(shù)的下一條指令地址。

接著,MOVQ -8(SP), BP 這條指令重置了 BP 寄存器,使它指向了 f 棧幀 的 BP。這樣,SP、BP 寄存器回到了 f 函數(shù)調(diào)用 deferreturn 之前的狀態(tài):f 剛準備好調(diào)用 deferreturn 的參數(shù),并且把返回值壓棧了。也就相當于拋棄了 deferreturn 函數(shù)的棧幀。

接著 SUBQ $5, (SP) 把返回地址減少了 5B,剛好是一個 CALL 指令的長度。什么意思?當執(zhí)行完 deferreturn 函數(shù)之后,執(zhí)行流程會返回到 CALL deferreturn 的下一條指令,將這個值減少 5B,也就又回到了 CALL deferreturn 指令,從而實現(xiàn)了“遞歸地”調(diào)用 deferreturn 函數(shù)的效果。當然,棧卻不會再增長。

上面所描述的整個過程,結(jié)合圖 2-3 會更容易理解: 

jmpdefer 函數(shù)的最后會執(zhí)行 sum 函數(shù),看起來就像是 f 函數(shù)直接調(diào)用 sum 函數(shù)一樣,參數(shù)、返回值都是就緒的。

● 圖 2-3 執(zhí)行 jmpdefer

等到 sum 函數(shù)執(zhí)行完,執(zhí)行流程就會跳轉(zhuǎn)到 call deferreturn 指令處重新進入 deferreturn 函數(shù),遍歷完所有的_defer 結(jié)構(gòu)體,執(zhí)行完所有的被 defered 的函數(shù),才真正執(zhí)行完 deferretrun 函數(shù),如圖 2-4 所示。

● 圖 2-4 重新調(diào)用 deferreturn

綜上所述,實現(xiàn)遍歷 defer 鏈表的關(guān)鍵就是 jmpdefer 函數(shù)所做的一些“見不得人”的工作,將調(diào)用 deferreturn 函數(shù)的返回地址減少了 5 個字節(jié),使得被 defered 的函數(shù)執(zhí)行完后,又回到CALL deferreturn 指令處,從而實現(xiàn)“遞歸地”調(diào)用 deferreturn 函數(shù),完成 _defer 鏈表的遍歷。

以上就是詳解go中的defer鏈如何被遍歷執(zhí)行的詳細內(nèi)容,更多關(guān)于go defer鏈遍歷執(zhí)行的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • go nil處理如何正確返回nil的error

    go nil處理如何正確返回nil的error

    這篇文章主要為大家介紹了go中的nil處理,如何正確返回nil的error實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-10-10
  • golang結(jié)構(gòu)化日志slog的用法簡介

    golang結(jié)構(gòu)化日志slog的用法簡介

    日志是任何軟件的重要組成部分,Go?提供了一個內(nèi)置日志包(slog),在本文中,小編將簡單介紹一下slog包的功能以及如何在?Go?應(yīng)用程序中使用它,感興趣的可以了解下
    2023-09-09
  • Go語言Select chan用法小結(jié)

    Go語言Select chan用法小結(jié)

    select語句是Go語言中用于處理多個通道操作的關(guān)鍵字,它允許你在多個通道上進行非阻塞的選擇操作,本文就詳細介紹一下如何使用,感興趣的可以了解一下
    2023-09-09
  • Go語言空結(jié)構(gòu)體詳解

    Go語言空結(jié)構(gòu)體詳解

    本文主要介紹了Go語言空結(jié)構(gòu)體詳解,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Go語言操作redis數(shù)據(jù)庫的方法

    Go語言操作redis數(shù)據(jù)庫的方法

    這篇文章主要介紹了Go語言操作redis數(shù)據(jù)庫的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-07-07
  • Go語言中的指針運算實例分析

    Go語言中的指針運算實例分析

    這篇文章主要介紹了Go語言中的指針運算技巧,實例分析了Go語言指針運算的實現(xiàn)方法,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-02-02
  • Go?Web編程添加服務(wù)器錯誤和訪問日志

    Go?Web編程添加服務(wù)器錯誤和訪問日志

    這篇文章主要為大家介紹了Go?Web編程添加服務(wù)器錯誤日志和訪問日志的示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06
  • Golang兩行代碼實現(xiàn)發(fā)送釘釘機器人消息

    Golang兩行代碼實現(xiàn)發(fā)送釘釘機器人消息

    創(chuàng)建一個釘釘機器人必須使用加簽,本文通過Golang兩行代碼實現(xiàn)發(fā)送釘釘機器人消息,本文給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧
    2021-12-12
  • Golang初始化MySQL數(shù)據(jù)庫方法淺析

    Golang初始化MySQL數(shù)據(jù)庫方法淺析

    這篇文章主要介紹了Golang初始化MySQL數(shù)據(jù)庫的方法,數(shù)據(jù)庫的建立第一步即要初始化,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧
    2023-05-05
  • Golang中的參數(shù)傳遞示例詳解

    Golang中的參數(shù)傳遞示例詳解

    參數(shù)傳遞是指在程序的傳遞過程中,實際參數(shù)就會將參數(shù)值傳遞給相應(yīng)的形式參數(shù),然后在函數(shù)中實現(xiàn)對數(shù)據(jù)處理和返回的過程,下面這篇文章主要給大家介紹了關(guān)于Golang中參數(shù)傳遞的相關(guān)資料,需要的朋友可以參考下。
    2017-09-09

最新評論

阜城县| 嘉义县| 香港 | 交城县| 衡东县| 武清区| 津南区| 文化| 尼勒克县| 县级市| 博客| 盐津县| 临沧市| 巴塘县| 洪雅县| 内黄县| 桓台县| 伊宁市| 景宁| 柯坪县| 喜德县| 青田县| 青铜峡市| 贵港市| 黎城县| 嘉义县| 罗源县| 屏南县| 盈江县| 无极县| 扶沟县| 绥棱县| 汉川市| 黑龙江省| 剑川县| 大安市| 游戏| 邵阳市| 米脂县| 广灵县| 牙克石市|