曹大带我学 Go(5)—— 哪里来的 goexit
你好,我是小X。
曹大最近開 Go 課程了,小X 正在和曹大學(xué) Go。
這個系列會講一些從課程中學(xué)到的讓人醍醐灌頂?shù)臇|西,撥云見日,帶你重新認識 Go。
在學(xué)員群里,有同學(xué)在用 dlv 調(diào)試時看到了令人不解的 goexit:goexit 函數(shù)是啥,為啥 go fun(){}() 的上層是它?看著像是一個“退出”函數(shù),為什么會出現(xiàn)在最上層?
其實如果看過 pprof 的火焰圖,也會經(jīng)常看到 goexit 這個函數(shù)。
我們來個例子重現(xiàn)一下:
package?mainimport?"time"func?main()?{go?func?()??{println("hello?world")}()time.Sleep(10*time.Minute) }啟動 dlv 調(diào)試,并分別在不同的地方打上斷點:
(dlv)?b?a.go:5? Breakpoint?1?(enabled)?set?at?0x106d12f?for?main.main()?./a.go:5 (dlv)?b?a.go:6 Breakpoint?2?(enabled)?set?at?0x106d13d?for?main.main()?./a.go:6 (dlv)?b?a.go:7 Breakpoint?3?(enabled)?set?at?0x106d1a0?for?main.main.func1()?./a.go:7執(zhí)行命令 c 運行到斷點處,再執(zhí)行 bt 命令得到 main 函數(shù)的調(diào)用棧:
(dlv)?bt 0??0x000000000106d12f?in?main.mainat?./a.go:5 1??0x0000000001035c0f?in?runtime.mainat?/usr/local/go/src/runtime/proc.go:204 2??0x0000000001064961?in?runtime.goexitat?/usr/local/go/src/runtime/asm_amd64.s:1374它的上一層是 runtime.main,找到原代碼位置,位于 src/runtime/proc.go 里的 main 函數(shù),它是 Go 進程的 main goroutine,這里會執(zhí)行一些 init 操作、開啟 GC、執(zhí)行用戶 main 函數(shù)……
fn?:=?main_main?//?proc.go:203 fn()?//?proc.go:204其中 fn 是 main_main 函數(shù),表示用戶的 main 函數(shù),執(zhí)行到了這里,才真正將權(quán)力交給用戶。
繼續(xù)執(zhí)行 c 命令和 bt 命令,得到 go 這一行的調(diào)用棧:
0??0x000000000106d13d?in?main.mainat?./a.go:6 1??0x0000000001035c0f?in?runtime.mainat?/usr/local/go/src/runtime/proc.go:204 2??0x0000000001064961?in?runtime.goexitat?/usr/local/go/src/runtime/asm_amd64.s:1374以及 println 這一句的調(diào)用棧:
0??0x000000000106d1a0?in?main.main.func1at?./a.go:7 1??0x0000000001064961?in?runtime.goexitat?/usr/local/go/src/runtime/asm_amd64.s:1374可以看到,調(diào)用棧的最上層都是 runtime.goexit,我們跟著注明了的代碼行數(shù),順藤摸瓜,找到 goexit 代碼:
// The top-most function running on a goroutine // returns to goexit+PCQuantum. TEXT runtime·goexit(SB),NOSPLIT,$0-0BYTE $0x90 // NOPCALL runtime·goexit1(SB) // does not return// traceback from goexit1 must hit code range of goexitBYTE $0x90 // NOP這還是個匯編函數(shù),它接著調(diào)用 goexit1 函數(shù)、goexit0 函數(shù),主要的功能就是將 goroutine 的各個字段清零,放入 gFree 隊列里,等待將來進行復(fù)用。
另一方面,goexit 函數(shù)的地址是在創(chuàng)建 goroutine 的過程中,塞到棧上的。讓 CPU “誤以為”:func() 是由 goexit 函數(shù)調(diào)用的。這樣一來,當 func() 執(zhí)行完畢時,會返回到 goexit 函數(shù)做一些清理工作。
下面這張圖能看出在 newg 的棧底塞了一個 goexit 函數(shù)的地址:
goexit 返回地址對應(yīng)的路徑是:
newporc?->?newporc1?->?gostartcallfn?->?gostartcall來看 newproc1 中的關(guān)鍵幾行代碼:
newg.sched.pc?=?funcPC(goexit)?+?sys.PCQuantum newg.sched.g?=?guintptr(unsafe.Pointer(newg)) gostartcallfn(&newg.sched,?fn)這里的 newg 就是創(chuàng)建的 goroutine,每個新建的 goroutine 都會執(zhí)行這些代碼。而 sched 結(jié)構(gòu)體其實保存的是 goroutine 的執(zhí)行現(xiàn)場,每當 goroutine 被調(diào)離 CPU,它的執(zhí)行進度就是保存到這里。進度主要就是 SP、BP、PC,分別表示棧頂?shù)刂贰5椎刂贰⒅噶钗恢?#xff0c;等 goroutine 再次得到 CPU 的執(zhí)行權(quán)時,會把 SP、BP、PC 加載到寄存器中,從而從斷點處恢復(fù)運行。
回到上面的幾行代碼,pc 被賦值成了 funcPC(goexit),最后在 gostartcall 里:
//?adjust?Gobuf?as?if?it?executed?a?call?to?fn?with?context?ctxt //?and?then?did?an?immediate?gosave. func?gostartcall(buf?*gobuf,?fn,?ctxt?unsafe.Pointer)?{sp?:=?buf.sp...sp?-=?sys.PtrSize*(*uintptr)(unsafe.Pointer(sp))?=?buf.pcbuf.sp?=?spbuf.pc?=?uintptr(fn)buf.ctxt?=?ctxt }sp 其實就是棧頂,第 7 行代碼把 buf.pc,也就是 goexit 的地址,放在了棧頂?shù)牡胤?#xff0c;熟悉 Go 函數(shù)調(diào)用規(guī)約的朋友知道,這個位置其實就是 return addr,將來等 func() 執(zhí)行完,就會回到父函數(shù)繼續(xù)執(zhí)行,這里的父函數(shù)其實就是 goexit。
一切早已注定。
不過注意一點,main goroutine 和普通的 goroutine 不同的是,前者執(zhí)行完用戶 main 函數(shù)后,會直接執(zhí)行 exit 調(diào)用,整個進程退出:
exit也就不會進入 goexit 函數(shù)。而普通 goroutine 執(zhí)行完畢后,則直接進入 goexit 函數(shù),做一些清理工作。
這也就是為什么只要 main goroutine 執(zhí)行完了,就不會等其他 goroutine,直接退出。一切都是因為 exit 這個調(diào)用。
今天我們主要講了 goexit 是怎么被安插到 goroutine 的棧上,從而實現(xiàn) goroutine 執(zhí)行完畢后再回到 goexit 函數(shù)。
原來看似很不理解的東西,是不是更清晰了?
源碼面前,了無秘密。
好了,這就是今天全部的內(nèi)容了~ 我是小X,我們下期再見~
歡迎關(guān)注曹大的 TechPaper 以及碼農(nóng)桃花源~
總結(jié)
以上是生活随笔為你收集整理的曹大带我学 Go(5)—— 哪里来的 goexit的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 假如 Go 能说话,听听 GMP 的心声
- 下一篇: 你真的了解 timeout 吗?