哪来里的 goexit?
source link: https://qcrao.com/2021/06/07/where-is-goexit-from/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
哪来里的 goexit?
有同学在用 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 调用,整个进程退出:
也就不会进入 goexit 函数。而普通 goroutine 执行完毕后,则直接进入 goexit 函数,做一些清理工作。
这也就是为什么只要 main goroutine 执行完了,就不会等其他 goroutine,直接退出。一切都是因为 exit
这个调用。
今天我们主要讲了 goexit 是怎么被安插到 goroutine 的栈上,从而实现 goroutine 执行完毕后再回到 goexit 函数。
原来看似很不理解的东西,是不是更清晰了?
源码面前,了无秘密。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK