0

哪来里的 goexit?

 2 years ago
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.
neoserver,ios ssh client

哪来里的 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

其中 fnmain_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 函数。

原来看似很不理解的东西,是不是更清晰了?

源码面前,了无秘密。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK