前言
defer
這個(gè)關(guān)鍵字在開(kāi)發(fā)過(guò)程中上場(chǎng)率可不低慨代,初學(xué)者只會(huì)知道在當(dāng)前函數(shù)中聲明一個(gè)defer
函數(shù)豹绪,那么會(huì)在當(dāng)前函數(shù)return
時(shí)再去執(zhí)行defer
定義的函數(shù),但具體原因是什么呢玫膀?假如在當(dāng)前函數(shù)中同時(shí)聲明多個(gè)defer
函數(shù)尘盼,為何先聲明的后執(zhí)行呢?要想弄懂這些問(wèn)題兼吓,可以從本文中得到答案臂港。
Example
簡(jiǎn)單的看一段程序,代碼很簡(jiǎn)單视搏,咱們主要來(lái)分析一下defer
具體的執(zhí)行流程审孽。一個(gè)簡(jiǎn)單的defer
關(guān)鍵字在經(jīng)過(guò)編譯之后究竟經(jīng)過(guò)了哪些方法的調(diào)用。
func main() {
f()
}
func f() {
defer sum(1, 2)
}
func sum(a, b int) int {
return a + b
}
咱們看一下匯編指令浑娜,重點(diǎn)部分都加有注釋佑力。(對(duì)函數(shù)調(diào)用過(guò)程這一塊不太了解的可以去看一下這篇文章)
(gdb) disass main.f
Dump of assembler code for function main.f:
0x000000000044f960 <0>: mov %fs:0xfffffffffffffff8,%rcx
0x000000000044f969 <+9>: cmp 0x10(%rcx),%rsp
0x000000000044f96d <+13>: jbe 0x44f9cb <main.f+107>
0x000000000044f96f <+15>: sub $0x30,%rsp # 分配函數(shù)f的棧空間
0x000000000044f973 <+19>: mov %rbp,0x28(%rsp) # 保存main函數(shù)的rbp到0x28(%rsp)中
0x000000000044f978 <+24>: lea 0x28(%rsp),%rbp # 將0x28(%rsp)位置作為函數(shù)f的rbp
0x000000000044f97d <+29>: movl $0x18,(%rsp) # rsp(0)位置為24筋遭,代表參數(shù)長(zhǎng)度
0x000000000044f984 <+36>: lea 0x23c85(%rip),%rax # 0x473610 這里是sum函數(shù)的地址
0x000000000044f98b <+43>: mov %rax,0x8(%rsp) # rsp(8)位置為sum函數(shù)的地址
0x000000000044f990 <+48>: movq $0x1,0x10(%rsp) # rsp(16)位置為第一個(gè)參數(shù)值1
0x000000000044f999 <+57>: movq $0x2,0x18(%rsp) # rsp(24)位置為第二個(gè)參數(shù)值2
0x000000000044f9a2 <+66>: callq 0x4221f0 <runtime.deferproc># 關(guān)注點(diǎn)1
# deferproc后自動(dòng)插入的一條指令打颤。正常情況下eax寄存器中返回的值是0,執(zhí)行接下來(lái)的業(yè)務(wù)邏輯漓滔。
# 但是當(dāng)業(yè)務(wù)邏輯panic后编饺,并且有recover的情況下,eax寄存器中會(huì)被填入1响驴,
# 則經(jīng)過(guò)該指令對(duì)比后直接跳轉(zhuǎn)到0x44f9bb處的deferreturn
0x000000000044f9a7 <+71>: test %eax,%eax
0x000000000044f9a9 <+73>: jne 0x44f9bb <main.f+91>
0x000000000044f9ab <+75>: nop
0x000000000044f9ac <+76>: callq 0x422a80 <runtime.deferreturn> # 關(guān)注點(diǎn)2
0x000000000044f9b1 <+81>: mov 0x28(%rsp),%rbp # 恢復(fù)成main函數(shù)的rbp
0x000000000044f9b6 <+86>: add $0x30,%rsp # 將椡盖遥空間還原
0x000000000044f9ba <+90>: retq
0x000000000044f9bb <+91>: nop
0x000000000044f9bc <+92>: callq 0x422a80 <runtime.deferreturn>
0x000000000044f9c1 <+97>: mov 0x28(%rsp),%rbp
0x000000000044f9c6 <+102>: add $0x30,%rsp
0x000000000044f9ca <+106>: retq
0x000000000044f9cb <+107>: callq 0x447840 <runtime.morestack_noctxt>
0x000000000044f9d0 <+112>: jmp 0x44f960 <main.f>
上面有兩個(gè)關(guān)注點(diǎn),runtime.deferproc
與runtime.deferreturn
這兩個(gè)方法豁鲤。在了解這兩個(gè)方法之前秽誊,先看一下defer
的結(jié)構(gòu)。
// A _defer holds an entry on the list of deferred calls.
// If you add a field here, add code to clear it in freedefer.
type _defer struct {
siz int32 // 參數(shù)的長(zhǎng)度琳骡,函數(shù)fn的參數(shù)長(zhǎng)度
started bool
sp uintptr // sp at time of defer
pc uintptr // defer語(yǔ)句下一條語(yǔ)句的地址
fn *funcval // 按上面的例子是&funcval{fn:&sum}
_panic *_panic // panic that is running defer
link *_defer // 同一個(gè)goroutine所有被延遲執(zhí)行的函數(shù)通過(guò)該成員鏈在一起形成一個(gè)鏈表
}
deferproc
// siz=fn.fn的參數(shù)長(zhǎng)度
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
if getg().m.curg != getg() {
// go code on the system stack can't defer
throw("defer on system stack")
}
// 獲取調(diào)用者的sp值(棧頂值)
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
// 獲取調(diào)用者的pc
callerpc := getcallerpc()
// 從緩沖區(qū)獲取或者重新創(chuàng)建一個(gè)
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc // 在panic-recover后锅论,用來(lái)跳轉(zhuǎn)的指令
d.sp = sp
switch siz {
case 0:
// 表示參數(shù)長(zhǎng)度是0,無(wú)需進(jìn)行任何操作
case sys.PtrSize:
// 表示是指針參數(shù)楣号,直接拷貝地址值即可
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
// 相當(dāng)于復(fù)制值到defer之后
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
// 正常情況下是返回0最易,然后執(zhí)行defer后面的邏輯,最后在f中執(zhí)行return時(shí)調(diào)用deferreturn
// 異常情況下(panic-recover)返回1竖席,直接執(zhí)行deferreturn
return0()
}
首先耘纱,deferproc
需要兩個(gè)參數(shù),第一個(gè)是defer函數(shù)的參數(shù)的長(zhǎng)度(以字節(jié)為單位的)毕荐,第二個(gè)參數(shù) funcval 是一個(gè)變長(zhǎng)結(jié)構(gòu)體束析。如下所示:
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
fn
的參數(shù)部分緊隨著fn
,按本文的例子憎亚,這里的fn=&sum
员寇,參數(shù)a,b以及返回參數(shù)緊跟著fn
排列弄慰。由于go中函數(shù)調(diào)用參數(shù)通過(guò)棧來(lái)傳遞,所以此時(shí)堆棧結(jié)構(gòu)是:
在函數(shù)deferproc
中蝶锋,
- 會(huì)先通過(guò)
newdefer(siz)
獲取一個(gè)defer
結(jié)構(gòu)體對(duì)應(yīng)的對(duì)象陆爽。具體是從緩存中獲取還是新分配一個(gè),下面會(huì)進(jìn)行詳細(xì)的解釋扳缕。 - 并給其sp屬性附上當(dāng)前調(diào)用者f的棧頂sp的值慌闭,后面進(jìn)行
deferreturn
時(shí)會(huì)通過(guò)這個(gè)值去進(jìn)行判斷要執(zhí)行的defer
是否屬于當(dāng)前調(diào)用者。 - 然后會(huì)將參數(shù)部分拷貝到緊挨著
defer
對(duì)象后面的地址:deferArgs(d)=unsafe.Pointer(d)+unsafe.Sizeof(*d)
躯舔。 - 執(zhí)行
return0
函數(shù)驴剔,正常情況下返回0,經(jīng)過(guò)test %eax,%eax
檢測(cè)后繼續(xù)執(zhí)行業(yè)務(wù)邏輯粥庄。異常情況下會(huì)返回1丧失,并且直接跳轉(zhuǎn)到deferreturn
。
newdefer
//go:nosplit
func newdefer(siz int32) *_defer {
var d *_defer
// 計(jì)算出sc惜互,方便從p的緩存池或者sched全局緩沖池中獲取defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
// 當(dāng)前p上緩存用完了布讹,則需要從全局區(qū)拷貝幾個(gè)出來(lái),
// 直到拷貝到deferpool容量的一半
systemstack(func() {
// 加上鎖训堆,有可能多個(gè)p同時(shí)去拉取
lock(&sched.deferlock)
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
unlock(&sched.deferlock)
})
}
if n := len(pp.deferpool[sc]); n > 0 {
d = pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
}
}
if d == nil {
// Allocate new defer+args.
// 當(dāng)前p和全局緩沖池中都沒(méi)有或者需要的參數(shù)過(guò)長(zhǎng)描验,則需要新分配一個(gè)
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
if debugCachedWork {
// Duplicate the tail below so if there's a
// crash in checkPut we can tell if d was just
// allocated or came from the pool.
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
}
d.siz = siz
// 與之前綁定在g上的defer形成一個(gè)鏈表
// 例如之前g上綁定1號(hào)defer,新加進(jìn)來(lái)一個(gè)2號(hào)defer
// 那么現(xiàn)在g上defer的順序是 2-->1,這也變向的驗(yàn)證了先聲明的defer后執(zhí)行
d.link = gp._defer
gp._defer = d
return d
}
分配defer
的流程大概分為三步走:
- 由于緩沖區(qū)中是根據(jù)參數(shù)的長(zhǎng)度進(jìn)行不同級(jí)別的
defer
來(lái)緩存的,所以得先計(jì)算出相應(yīng)的sc
值坑鱼。 - 判斷
sc
的值是否在緩沖區(qū)允許的范圍內(nèi)挠乳。- 在范圍內(nèi),即sc<5姑躲,則先檢查當(dāng)前p的緩沖區(qū)中是否還有可用的
defer
。- 沒(méi)有可用盟蚣,則去全局緩沖區(qū)
sched
中批量拉取直至達(dá)到當(dāng)前p緩沖區(qū)容量的一半黍析。再?gòu)漠?dāng)前p中去獲取。 - 若有可用屎开,則直接取出當(dāng)前p緩沖區(qū)對(duì)應(yīng)尺寸的
[]defer
最后一個(gè)元素即可阐枣。
- 沒(méi)有可用盟蚣,則去全局緩沖區(qū)
- 在范圍內(nèi),即sc<5姑躲,則先檢查當(dāng)前p的緩沖區(qū)中是否還有可用的
- 無(wú)法從緩沖區(qū)中獲取或者是參數(shù)過(guò)大(sc不在緩沖區(qū)范圍內(nèi)),則直接新分配一個(gè)奄抽。
- 將獲取的
defer
綁定到當(dāng)前goroutine上蔼两,并與之前綁定的defer
形成鏈表。
deferreturn
func deferreturn(arg0 uintptr) {
gp := getg()
// 獲取g上綁定的第一個(gè)defer
d := gp._defer
if d == nil {
// 由于是遞歸調(diào)用逞度,這里是一個(gè)循環(huán)終止條件额划,d上已經(jīng)沒(méi)有綁定的defer了
return
}
// 獲取當(dāng)前調(diào)用者的sp
sp := getcallersp()
if d.sp != sp {
// 判斷當(dāng)前調(diào)用者棧是否和defer中保存的一致
// 舉個(gè)例子,a()中聲明一個(gè)defer1档泽,并調(diào)用b(),b中也聲明一個(gè)defer2
// 然后defer1和defer2都綁定在同一個(gè)g上
// 那么在b()執(zhí)行return時(shí)俊戳,只會(huì)執(zhí)行defer2揖赴,因?yàn)閐efer2上綁定的才是b()的sp
return
}
// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
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))
}
fn := d.fn
d.fn = nil
// g中的defer指向下一個(gè)defer
gp._defer = d.link
// 進(jìn)行釋放,歸還到相應(yīng)的緩沖區(qū)或者讓gc回收
freedefer(d)
// 執(zhí)行defer中綁定的func
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
- 判斷當(dāng)前goroutine上是否還有綁定的
defer
抑胎,若沒(méi)有燥滑,直接return。 - 獲取goroutine綁定的
defer
鏈表頭部的defer
阿逃。 - 判斷當(dāng)前
defer
中存儲(chǔ)的sp是否和調(diào)用者的sp一致铭拧,若不一致,也直接return恃锉,證明當(dāng)前defer不是在此調(diào)用函數(shù)中聲明的搀菩。 - 進(jìn)行參數(shù)的拷貝。
- 釋放當(dāng)前要執(zhí)行fn關(guān)聯(lián)的defer淡喜。
- 執(zhí)行
jmpdefer
函數(shù)秕磷,這里會(huì)執(zhí)行完fn的邏輯后遞歸調(diào)用deferreturn
函數(shù)。
接著看一下jmpdefer
函數(shù)炼团,咱們拆開(kāi)一句句的去看澎嚣。
runtime/asm_amd64.s:566
// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // fn
MOVQ argp+8(FP), BX // caller sp
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
在執(zhí)行deferreturn
調(diào)用時(shí),棧幀內(nèi)部結(jié)構(gòu)大概如上圖所示瘟芝,咱們簡(jiǎn)單的梳理一下易桃。deferreturn
只有一個(gè)參數(shù)arg0
,所以在棧中的具體位置肯定是靠近其返回地址的锌俱。返回地址對(duì)應(yīng)的sp實(shí)際上是函數(shù)f的sp晤郑。
這里咱們將上面的匯編代碼拆解進(jìn)行一一分析:
MOVQ fv+0(FP), DX
將fv
中的第一個(gè)參數(shù)fn
放到DX寄存器中。
MOVQ argp+8(FP), BX
將argp
的地址放到BX寄存器中贸宏,實(shí)際上就是上圖中的&arg0
造寝。
LEAQ -8(BX), SP
將BX地址減去8,并將對(duì)應(yīng)的地址放到寄存器SP中吭练。實(shí)際上&arg0
地址減去8指向的是deferreturn
的返回地址诫龙,也就是callq 0x422a80 <runtime.deferreturn>
的下一條指令mov 0x28(%rsp),%rbp
,所以這里實(shí)際上將SP寄存器指向函數(shù)f的棧頂鲫咽。
MOVQ -8(SP), BP
SP-8的位置恰好存放函數(shù)f的bp签赃,這里將其值存放到BP寄存器中。經(jīng)過(guò)了上面四句指令分尸,已經(jīng)成功將函數(shù)棧從deferreturn
切換到了f
中锦聊。
SUBQ $5, (SP)
將SP向上移動(dòng)5位,一般人可能會(huì)在這里有疑惑箩绍,咱們先看一張圖片
咱們上面有說(shuō)到孔庭,sp指向的位置是deferreturn
的返回地址,也就是callq 0x422a80 <runtime.deferreturn>
的下一條指令mov 0x28(%rsp),%rbp
伶选。很神奇的就是SP-5后史飞,咱們可以發(fā)現(xiàn)SP又指向了callq 0x422a80 <runtime.deferreturn>
的位置尖昏,相當(dāng)于該位置成為了棧頂。這樣在執(zhí)行完函數(shù)fn
之后构资,又會(huì)繼續(xù)執(zhí)行deferreturn
函數(shù)抽诉,相當(dāng)于一個(gè)遞歸調(diào)用。
MOVQ 0(DX), BX #將fn地址放到BX寄存器中
JMP BX
跳轉(zhuǎn)到指定的fn函數(shù)去執(zhí)行相關(guān)邏輯吐绵,執(zhí)行完成后跳轉(zhuǎn)到deferreturn
函數(shù)迹淌。
freedefer
//go:nosplit
func freedefer(d *_defer) {
if d._panic != nil {
freedeferpanic()
}
if d.fn != nil {
freedeferfn()
}
sc := deferclass(uintptr(d.siz))
if sc >= uintptr(len(p{}.deferpool)) {
// 參數(shù)過(guò)大的不進(jìn)行緩存,等gc進(jìn)行回收
return
}
pp := getg().m.p.ptr()
if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
// 當(dāng)前p中緩沖區(qū)已滿己单,則遷移一半的defer到全局緩沖區(qū)
systemstack(func() {
var first, last *_defer
for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
n := len(pp.deferpool[sc])
d := pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
if first == nil {
first = d
} else {
last.link = d
}
last = d
}
lock(&sched.deferlock)
last.link = sched.deferpool[sc]
sched.deferpool[sc] = first
unlock(&sched.deferlock)
})
}
// These lines used to be simply `*d = _defer{}` but that
// started causing a nosplit stack overflow via typedmemmove.
d.siz = 0
d.started = false
d.sp = 0
d.pc = 0
// d._panic and d.fn must be nil already.
// If not, we would have called freedeferpanic or freedeferfn above,
// both of which throw.
d.link = nil
// 緩存到當(dāng)前p的緩沖區(qū)
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
釋放已經(jīng)用過(guò)的defer
:
- 首先判斷參數(shù)長(zhǎng)度唉窃,參數(shù)長(zhǎng)度過(guò)長(zhǎng)的直接讓gc進(jìn)行回收即可,無(wú)需歸還到緩沖區(qū)中纹笼。
- 若當(dāng)前p的緩沖區(qū)已經(jīng)滿了纹份,則需要進(jìn)行遷移操作,這里會(huì)將當(dāng)前p容量一半的
defer
歸還到全局緩沖區(qū)廷痘,供給其他的p使用蔓涧。操作的時(shí)候需要加上鎖,防止多個(gè)p出現(xiàn)并發(fā)操作笋额。 - 將其屬性置空元暴,并追加到
pp.deferpool[sc]
數(shù)組中。
總結(jié)
本文主要涉及到defer
這個(gè)關(guān)鍵字在編譯之后究竟是怎樣嵌入我們的代碼的兄猩,以及defer
后的函數(shù)是何時(shí)調(diào)用的茉盏,具體流程會(huì)先調(diào)用deferproc
在goroutine上綁定defer
鏈表,然后執(zhí)行deferreturn
時(shí)依次遍歷鏈表執(zhí)行defer
中的函數(shù)枢冤,正向插入鸠姨,反向遍歷,這樣也能看出先定義的defer
后執(zhí)行淹真。當(dāng)然還有部分點(diǎn)未做涉及享怀,例如panic后當(dāng)前goroutinedefer
鏈表中函數(shù)的調(diào)用,以及recover
是如何實(shí)現(xiàn)的趟咆。下一篇文章會(huì)對(duì)這一塊進(jìn)行分析。