曹大带我学 Go之哪里来的 Goexit

开发 后端
在学员群里,有同学在用 dlv 调试时看到了令人不解的 goexit:goexit 函数是啥,为啥 go fun(){}() 的上层是它?看着像是一个“退出”函数,为什么会出现在最上层?

[[404148]]

本文转载自微信公众号「码农桃花源」,作者小X。转载本文请联系码农桃花源公众号。

你好,我是小X。

曹大最近开 Go 课程了,小X 正在和曹大学 Go。

这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go。

在学员群里,有同学在用 dlv 调试时看到了令人不解的 goexit:goexit 函数是啥,为啥 go fun(){}() 的上层是它?看着像是一个“退出”函数,为什么会出现在最上层?

其实如果看过 pprof 的火焰图,也会经常看到 goexit 这个函数。

我们来个例子重现一下:

  1. package main 
  2.  
  3. import "time" 
  4.  
  5. func main() { 
  6.  go func ()  { 
  7.   println("hello world"
  8.  }() 
  9.   
  10.  time.Sleep(10*time.Minute

启动 dlv 调试,并分别在不同的地方打上断点:

  1. (dlv) b a.go:5  
  2. Breakpoint 1 (enabled) set at 0x106d12f for main.main() ./a.go:5 
  3. (dlv) b a.go:6 
  4. Breakpoint 2 (enabled) set at 0x106d13d for main.main() ./a.go:6 
  5. (dlv) b a.go:7 
  6. Breakpoint 3 (enabled) set at 0x106d1a0 for main.main.func1() ./a.go:7 

执行命令 c 运行到断点处,再执行 bt 命令得到 main 函数的调用栈:

  1. (dlv) bt 
  2. 0  0x000000000106d12f in main.main 
  3.    at ./a.go:5 
  4. 1  0x0000000001035c0f in runtime.main 
  5.    at /usr/local/go/src/runtime/proc.go:204 
  6. 2  0x0000000001064961 in runtime.goexit 
  7.    at /usr/local/go/src/runtime/asm_amd64.s:1374 

它的上一层是 runtime.main,找到原代码位置,位于 src/runtime/proc.go 里的 main 函数,它是 Go 进程的 main goroutine,这里会执行一些 init 操作、开启 GC、执行用户 main 函数……

  1. fn := main_main // proc.go:203 
  2. fn() // proc.go:204 

其中 fn 是 main_main 函数,表示用户的 main 函数,执行到了这里,才真正将权力交给用户。

继续执行 c 命令和 bt 命令,得到 go 这一行的调用栈:

  1. 0  0x000000000106d13d in main.main 
  2.    at ./a.go:6 
  3. 1  0x0000000001035c0f in runtime.main 
  4.    at /usr/local/go/src/runtime/proc.go:204 
  5. 2  0x0000000001064961 in runtime.goexit 
  6.    at /usr/local/go/src/runtime/asm_amd64.s:1374 

以及 println 这一句的调用栈:

  1. 0  0x000000000106d1a0 in main.main.func1 
  2.    at ./a.go:7 
  3. 1  0x0000000001064961 in runtime.goexit 
  4.    at /usr/local/go/src/runtime/asm_amd64.s:1374 

可以看到,调用栈的最上层都是 runtime.goexit,我们跟着注明了的代码行数,顺藤摸瓜,找到 goexit 代码:

  1. // The top-most function running on a goroutine 
  2. // returns to goexit+PCQuantum. 
  3. TEXT runtime·goexit(SB),NOSPLIT,$0-0 
  4.     BYTE    $0x90   // NOP 
  5.     CALL    runtime·goexit1(SB) // does not return 
  6.     // traceback from goexit1 must hit code range of goexit 
  7.     BYTE    $0x90   // NOP 

这还是个汇编函数,它接着调用 goexit1 函数、goexit0 函数,主要的功能就是将 goroutine 的各个字段清零,放入 gFree 队列里,等待将来进行复用。

另一方面,goexit 函数的地址是在创建 goroutine 的过程中,塞到栈上的。让 CPU “误以为”:func() 是由 goexit 函数调用的。这样一来,当 func() 执行完毕时,会返回到 goexit 函数做一些清理工作。

下面这张图能看出在 newg 的栈底塞了一个 goexit 函数的地址:

goexit 返回地址

对应的路径是:

  1. newporc -> newporc1 -> gostartcallfn -> gostartcall 

来看 newproc1 中的关键几行代码:

  1. newg.sched.pc = funcPC(goexit) + sys.PCQuantum 
  2. newg.sched.g = guintptr(unsafe.Pointer(newg)) 
  3. gostartcallfn(&newg.sched, fn) 

这里的 newg 就是创建的 goroutine,每个新建的 goroutine 都会执行这些代码。而 sched 结构体其实保存的是 goroutine 的执行现场,每当 goroutine 被调离 CPU,它的执行进度就是保存到这里。进度主要就是 SP、BP、PC,分别表示栈顶地址、栈底地址、指令位置,等 goroutine 再次得到 CPU 的执行权时,会把 SP、BP、PC 加载到寄存器中,从而从断点处恢复运行。

回到上面的几行代码,pc 被赋值成了 funcPC(goexit),最后在 gostartcall 里:

  1. // adjust Gobuf as if it executed a call to fn with context ctxt 
  2. // and then did an immediate gosave. 
  3. func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) { 
  4.  sp := buf.sp 
  5.  ... 
  6.  sp -= sys.PtrSize 
  7.  *(*uintptr)(unsafe.Pointer(sp)) = buf.pc 
  8.  buf.sp = sp 
  9.  buf.pc = uintptr(fn) 
  10.  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,我们下期再见~

 

责任编辑:武晓燕 来源: 码农桃花源
相关推荐

2021-06-10 09:00:32

Go底层代码

2021-08-09 07:47:39

ExtraGoMap

2021-06-01 09:27:53

Ast Go语言

2021-07-15 08:58:15

指定配置项Go

2021-05-20 08:59:47

Go调度本质

2021-05-27 08:59:09

Go汇编命令

2022-01-05 08:56:20

Go火焰图编程

2011-05-03 09:34:14

项目经理

2020-04-17 10:50:19

5G运营商网络

2023-05-26 08:21:59

Lock_TimeMySQL

2021-12-31 14:39:29

AI 数据人工智能

2018-05-23 10:23:18

数据系统机器学习

2015-05-04 17:36:49

2017-06-19 07:58:40

2022-04-06 08:58:39

归并排序Go算法

2022-02-09 07:52:36

GolangGo语言

2021-10-10 15:01:09

Go 源码Github

2020-07-31 07:55:21

JavaFuture接口

2021-12-07 11:30:32

Go煮蛋计时器

2021-02-22 09:30:09

go开发环境桌面系统
点赞
收藏

51CTO技术栈公众号