你好,我是小X。

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

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

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

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

我们来个例子重现一下:

package mainimport "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.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 函数,它是 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.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 这一句的调用栈:

0  0x000000000106d1a0 in main.main.func1at ./a.go:7
1  0x0000000001064961 in runtime.goexitat /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-0BYTE    $0x90   // NOPCALL  runtime·goexit1(SB) // does not return// traceback from goexit1 must hit code range of goexitBYTE   $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.pcbuf.sp = spbuf.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 以及码农桃花源~

曹大带我学 Go(5)—— 哪里来的 goexit相关推荐

  1. 曹大带我学 Go(8)—— 一个打点引发的事故

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 最近线上事故频发,搞得焦头烂额,但是能用上跟曹 ...

  2. 曹大带我学 Go(6)—— 技术之外

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 有学员私下和我说,这个课程挺打击他的自信心.我 ...

  3. 曹大带我学 Go(2)—— 迷惑的 goroutine 执行顺序

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 上一篇文章我们讲了 Go 调度的本质是一个生产 ...

  4. 『曹大带我学 Go 』系列文章汇总

    你好,我是小 X. 之前写了 11 篇跟着曹大学 Go 的文章,今天来汇总一下. 曹大的功力深厚,但能学到多少全看自己.第一期 Go 训练营也早就结束了,但学习还得继续.后面我也会继续发布这个系列,希 ...

  5. 曹大带我学 Go(12)—— 面向火焰图编程

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 现实中听过各种面向 XX 编程,什么面向过程编 ...

  6. 曹大带我学 Go(11)—— 从 map 的 extra 字段谈起

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 熟悉 map 结构体的读者应该知道,hmap ...

  7. 曹大带我学 Go(10)—— 如何给 Go 提性能优化的 pr

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 之前 qcrao 写了一篇<成为 Go ...

  8. 曹大带我学 Go(9)—— 开始积累自己的工具库

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 不知道你有没有这样的经验:看了很多计算机相关的 ...

  9. 曹大带我学 Go(7)—— 如何优雅地指定配置项

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 最近一个年久失修的库导致了线上事故,不得不去做 ...

最新文章

  1. 利用swapoff和swapon刷新swap缓存
  2. windos10下编译opencv_4.0.1+opencv-contrib_4.0.1
  3. 入坑 Electron 开发跨平台桌面应用
  4. 如何将h5网页改成微信网页
  5. linux认证哪家好,linux认证 考哪种好?
  6. 云服务器 ECS 搭建WordPress网站:备案
  7. 天文坐标系的转换 时角坐标和赤道坐标系的转化
  8. CMMI认证要求有哪些
  9. 【R语言】如何直接调取Wind、iFinD数据接口教程
  10. 2.1微信小程序简介
  11. 微信小程序中wx.canIUse的坑
  12. 2021年美妆护肤行业电商营销报告
  13. QT .pro文件详解
  14. ffmpeg截取一段视频
  15. win10浏览器不能联网,电脑上其他软件可以联网
  16. 高性能永磁交流伺服电机系统控制策略
  17. 云豹php短视频源码实现身份证验证的方法
  18. MyBatis的基础查询
  19. Windows下如何查看十几G的日志文件
  20. 小小滑块可笑可笑-安卓滑块验证码通杀方案研究(5)

热门文章

  1. rlm sql mysql.so_UBUUTU7.10上安装配置freeradius+mysql+rp-pppoe手记
  2. 瑞士桁架机器人_机器人库晚报:人工智能可以在实验室中预测人的血糖水平
  3. 关于自己写博客的重要性
  4. Ubuntu 16.04 安装wine
  5. 10-python-字典
  6. 《配置管理最佳实践》——1.2 从哪里开始
  7. XMNetworking 网络库的设计与使用
  8. EL函数以及自定义标签的应用
  9. 韩拓-七牛产品演进之路
  10. 剑指-从尾到头打印链表