深度解析 Golang Panic 的細節
一、介紹
當數組越界、訪問非法空間或者主動調用 panic 時,panic 會停掉當前正在執行的程序,包括所有協程,比起 exit 直接退出,panic 的退出更有秩序,它會先處理完當前 goroutine 已經 defer 掛上去的任務,執行完畢後再退出整個程序。
二、執行 panic 底層到底發生什麼?
接下來我們通過一個案例彙編執行得出 panic 底層執行哪些操作,如下:
func main() {
panic("hello panic")
}
執行輸出:
➜ go tool compile -S main.go
"".main STEXT size=66 args=0x0 locals=0x18
0x0000 00000 (main.go:108) TEXT "".main(SB), ABIInternal, $24-0
0x0000 00000 (main.go:108) MOVQ (TLS), CX
0x0009 00009 (main.go:108) CMPQ SP, 16(CX)
...
0x002f 00047 (main.go:120) PCDATA $2, $0
0x002f 00047 (main.go:120) MOVQ AX, 8(SP)
0x0034 00052 (main.go:120) CALL runtime.gopanic(SB)
0x0039 00057 (main.go:120)
...
0x0024 00036 (main.go:111) LEAQ "".main.func1·f(SB), AX
0x002b 00043 (main.go:111) PCDATA $2, $0
0x002b 00043 (main.go:111) MOVQ AX, 8(SP)
0x0030 00048 (main.go:111) CALL runtime.deferproc(SB)
0x0035 00053 (main.go:111) TESTL AX, AX
...
0x004b 00075 (main.go:116) MOVQ AX, 8(SP)
0x0050 00080 (main.go:116) CALL runtime.gopanic(SB)
0x0055 00085 (main.go:116) UNDEF
0x0057 00087 (main.go:111) XCHGL AX, AX
0x0058 00088 (main.go:111) CALL runtime.deferreturn(SB)
0x005d 00093 (main.go:111) MOVQ 16(SP), BP
...
0x0026 00038 (main.go:112) MOVQ AX, (SP)
0x002a 00042 (main.go:112) CALL runtime.gorecover(SB)
...
0x0072 00114 ($GOROOT/src/fmt/print.go:208) LEAQ go.string."recovered:%v\n"(SB), AX
...
0x00a3 00163 ($GOROOT/src/fmt/print.go:208) CALL fmt.Fprintf(SB)
2.1 panic 結構
type _panic struct {
argp unsafe.Pointer //一個指針,指向defer調用的參數的指針
arg interface{} //panic傳入的參數
link *_panic //是一個鏈表結構,指向上一個調用的_panic
recovered bool //是否已被recover 恢復
aborted bool //panic是否被強行中止
}
說明:
-
link 字段:一個指向 _panic 結構體的指針,表明 _panic 和 _defer 類似,_panic 可以是一個單向鏈表,就跟 _defer 鏈表一樣;
-
recovered 字段:重點來了,所謂的 _panic 是否恢復其實就是看這個字段是否爲 true,recover( ) 其實就是修改這個字段;
再看一下 goroutine 的兩個重要字段:
type g struct {
// ...
_panic *_panic // panic 鏈表,這是最裏的一個
_defer *_defer // defer 鏈表,這是最裏的一個;
// ...
}
從這裏我們看出:_defer 和 _panic 鏈表都是掛在 goroutine 之上的
通過彙編得知 panic 的底層實現是 gopanic 的函數,位於 runtime/panic.go 文件。panic 機制最重要的就是 gopanic 函數了,所有的 panic 細節盡在此。爲什麼 panic 會顯得晦澀,主要有兩個點:
-
嵌套 panic 的時候,gopanic 會有遞歸執行的場景
-
程序指令跳轉並不是常規的函數壓棧,彈棧,在 recovery 的時候,是直接修改指令寄存器的結構體,從而直接越過了 gopanic 後面的邏輯,甚至是多層 gopanic 遞歸的邏輯
2.2 gopanic 實現
// The implementation of the predeclared function panic.
func gopanic(e interface{}) {
gp := getg() //獲取指向當前goroutine的指針
...
//初始化一個panic的基本單元_panic
var p _panic
p.arg = e
// 把當前最新的 _panic 掛到鏈表最前面
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
//遍歷當前goroutinue的defer鏈表
for {
//獲取當前goroutinue上掛的defer
d := gp._defer
if d == nil {
break
}
//下面是一些對此defer的判斷和處理,是否開放,是否可執行等
...
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
if !d.openDefer {
// For open-coded defers, we need to process the
// defer again, in case there are any other defers
// to call in the frame (not including the defer
// call that caused the panic).
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
}
...
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
//當前goroutinue若存在有效的defer調用,
//就會調下面的reflectcall方法來處理defer後面的操作,
//當然,若defer裏面進行了recover處理,則會調用gorecover函數,
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
}
p.argp = nil
//在pc、sp中記錄當前defer的pc、sp
pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
...
//已經有recover被調用
if p.recovered {
// 摘掉當前的 _panic
gp._panic = p.link
// 如果前面還有 panic,並且是標記了 aborted 的,那麼也摘掉;
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
// panic 的流程到此爲止,恢復到業務函數堆棧上執行代碼;
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 注意:恢復的時候 panic 函數將從此處跳出,本 gopanic 調用結束,後面的代碼永遠都不會執行。
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
//準備panic程序退出時要打印的參數
preprintpanics(gp._panic)
//遞歸打印所有panic中的參數信息,並執行exit(2)來退出程序
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
上面邏輯可以拆分爲循環內和循環外兩部分去理解:
- 循環內:程序執行 defer,是否恢復正常的指令執行,一切都在循環內決定。循環內的事情拆解成:
-
遍歷 goroutine 的 defer 鏈表,獲取到一個 _defer 延遲函數;
-
獲取到 _defer 延遲函數,設置標識 d.started,綁定當前 d._panic(用以在遞歸的時候判斷);
-
執行 _defer 延遲函數;
-
摘掉執行完的 _defer 函數;
-
判斷 _panic.recovered 是否設置爲 true,進行相應操作;
-
如果是 true 那麼重置 pc,sp 寄存器(一般從 deferreturn 指令前開始執行),goroutine 投遞到調度隊列,等待執行;
-
重複以上步驟;
- 循環外:一旦走到循環外,說明 _panic 沒人處理,認命吧,程序即將退出
調用 gopanic 函數,接着遍歷當前 goroutinue 上掛載的 defer 鏈表,在執行 reflectcall 時碰到 recover,就執行 gorecover 函數處理,接下來看下 gorecover。
2.3 gorecover 實現以及如何恢復 panic
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
通過如上代碼,得知 gorecover 得知當前 goroutinue 是否在 panic 的流程中,在的話就將 panic 的 recovered 字段置微爲 true,否則直接返回 nil,所以說 recover 在普通的流程被調用時時沒有任何作用的。
gorecover 函數僅僅改了 recovered 標記,那麼 gouroutine 是怎麼從 panic 返回的呢?
-
在 gopanic 函數中,執行 defer 鏈中所有的延遲函數,執行時遇到 recover 會使得_panic.recovered=true
-
調用 recovery 通過_defer.sp 和_defer.pc 跳轉到 deferproc 的上下文中來執行,同時將返回值設置爲 1
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// d's arguments need to be in the stack.
if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
throw("bad recovery")
}
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
//切換上下文
gogo(&gp.sched)
}
- 最後根據 deferproc 的返回值,若爲 0 的話,說明 deferproc 成功,可以繼續處理後面的邏輯;當返回值爲非 0 的情況時(就是 panic 恢復時從 recovery 函數跳轉過來的),此時就會跳轉到函數的最後,return 之前,繼續執行 deferreturn。由於調用 recover 的 defer 已經從 defer 鏈表上摘掉了,所以可以繼續執行之前沒完成的 defer,並最終返回當前函數的調用者。
func deferproc(siz int32, fn *funcval) {
...
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
//newdefer從_defer池中找是否有可複用的_defer基本單元,若沒有,就會malloc一個新的
//並且將當前goroutinue的defer指向這個d, 新的defer總是會出現在最前面
//這就是defer先入後出屬性的原因所在,
d := newdefer(siz)
...
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
三、Q&A
3.1 爲什麼 recover 一定要放在 defer 才生效?
因爲 _panic.recovered 字段的的修改時機,只能在 for 循環內的第三步執行 _defer 延遲函數。
3.2 爲什麼 recover 只能當前協程有效?
上面我們提到 gopanic 函數,裏面 for 循環有如下代碼:
//獲取當前goroutinue上掛的defer
d := gp._defer
// 步驟:執行 defer 函數
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
// 步驟:執行完成,把這個 defer 從鏈表裏摘掉;
gp._defer = d.link
在 gopanic 裏,只遍歷執行當前 goroutine 上的 _defer 函數鏈條。所以,如果掛在其他 goroutine 的 defer 函數做了 recover ,那麼沒有絲毫用途。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/byouCtNulnjYDluDaO9wIw