曹大帶我學 Go(5)—— 哪裏來的 goexit
你好,我是小 X。
曹大最近開 Go 課程了,小 X 正在和曹大學 Go。
這個系列會講一些從課程中學到的讓人醍醐灌頂的東西,撥雲見日,帶你重新認識 Go。
在學員羣裏,有同學在用 dlv 調試時看到了令人不解的 goexit
:goexit 函數是啥,爲啥 go fun(){}()
的上層是它?看着像是一個 “退出” 函數,爲什麼會出現在最上層?
其實如果看過 pprof 的火焰圖,也會經常看到 goexit
這個函數。
我們來個例子重現一下:
package main
import "time"
func main() {
go func () {
println("hello world")
}()
time.Sleep(10*time.Minute)
}
啓動 dlv 調試,並分別在不同的地方打上斷點:
(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
執行命令 c
運行到斷點處,再執行 bt
命令得到 main 函數的調用棧:
(dlv) bt
0 0x000000000106d12f in main.main
at ./a.go:5
1 0x0000000001035c0f in runtime.main
at /usr/local/go/src/runtime/proc.go:204
2 0x0000000001064961 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1374
它的上一層是 runtime.main
,找到原代碼位置,位於 src/runtime/proc.go
裏的 main
函數,它是 Go 進程的 main goroutine,這裏會執行一些 init 操作、開啓 GC、執行用戶 main 函數……
fn := main_main // proc.go:203
fn() // proc.go:204
其中 fn
是 main_main
函數,表示用戶的 main 函數,執行到了這裏,才真正將權力交給用戶。
繼續執行 c
命令和 bt
命令,得到 go
這一行的調用棧:
0 0x000000000106d13d in main.main
at ./a.go:6
1 0x0000000001035c0f in runtime.main
at /usr/local/go/src/runtime/proc.go:204
2 0x0000000001064961 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1374
以及 println
這一句的調用棧:
0 0x000000000106d1a0 in main.main.func1
at ./a.go:7
1 0x0000000001064961 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1374
可以看到,調用棧的最上層都是 runtime.goexit
,我們跟着註明了的代碼行數,順藤摸瓜,找到 goexit 代碼:
// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT,$0-0
BYTE $0x90 // NOP
CALL runtime·goexit1(SB) // does not return
// traceback from goexit1 must hit code range of goexit
BYTE $0x90 // NOP
這還是個彙編函數,它接着調用 goexit1 函數、goexit0 函數,主要的功能就是將 goroutine 的各個字段清零,放入 gFree 隊列裏,等待將來進行復用。
另一方面,goexit 函數的地址是在創建 goroutine 的過程中,塞到棧上的。讓 CPU “誤以爲”:func()
是由 goexit 函數調用的。這樣一來,當 func()
執行完畢時,會返回到 goexit 函數做一些清理工作。
下面這張圖能看出在 newg 的棧底塞了一個 goexit 函數的地址:
goexit 返回地址
對應的路徑是:
newporc -> newporc1 -> gostartcallfn -> gostartcall
來看 newproc1
中的關鍵幾行代碼:
newg.sched.pc = funcPC(goexit) + sys.PCQuantum
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&newg.sched, fn)
這裏的 newg 就是創建的 goroutine,每個新建的 goroutine 都會執行這些代碼。而 sched
結構體其實保存的是 goroutine 的執行現場,每當 goroutine 被調離 CPU,它的執行進度就是保存到這裏。進度主要就是 SP、BP、PC,分別表示棧頂地址、棧底地址、指令位置,等 goroutine 再次得到 CPU 的執行權時,會把 SP、BP、PC 加載到寄存器中,從而從斷點處恢復運行。
回到上面的幾行代碼,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.pc
buf.sp = sp
buf.pc = uintptr(fn)
buf.ctxt = ctxt
}
sp
其實就是棧頂,第 7 行代碼把 buf.pc
,也就是 goexit
的地址,放在了棧頂的地方,熟悉 Go 函數調用規約的朋友知道,這個位置其實就是 return addr
,將來等 func()
執行完,就會回到父函數繼續執行,這裏的父函數其實就是 goexit
。
一切早已註定。
不過注意一點,main goroutine
和普通的 goroutine 不同的是,前者執行完用戶 main 函數後,會直接執行 exit 調用,整個進程退出:
exit
也就不會進入 goexit 函數。而普通 goroutine 執行完畢後,則直接進入 goexit 函數,做一些清理工作。
這也就是爲什麼只要 main goroutine 執行完了,就不會等其他 goroutine,直接退出。一切都是因爲 exit
這個調用。
今天我們主要講了 goexit 是怎麼被安插到 goroutine 的棧上,從而實現 goroutine 執行完畢後再回到 goexit 函數。
原來看似很不理解的東西,是不是更清晰了?
源碼面前,了無祕密。
好了,這就是今天全部的內容了~ 我是小 X,我們下期再見~
歡迎關注曹大的 TechPaper 以及碼農桃花源~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6qGV6QpBHyucpADLr4r3Xg