千难万险 —— goroutine 从生到死(六)
上一讲说到调度器将 main goroutine 推上舞台,为它铺好了道路,开始执行 runtime.main
函数。这一讲,我们探索 main goroutine 以及普通 goroutine 从执行到退出的整个过程。
// The main goroutine.
func main() { // g = main goroutine,不再是 g0 了 g := getg() // …………………… if sys.PtrSize == 8 { maxstacksize = 1000000000 } else { maxstacksize = 250000000 } // Allow newproc to start new Ms. mainStarted = true systemstack(func() { // 创建监控线程,该线程独立于调度器,不需要跟 p 关联即可运行 newm(sysmon, nil) }) lockOSThread() if g.m != &m0 { throw("runtime.main not on m0") } // 调用 runtime 包的初始化函数,由编译器实现 runtime_init() // must be before defer if nanotime() == 0 { throw("nanotime returning zero") } // Defer unlock so that runtime.Goexit during init does the unlock too. needUnlock := true defer func() { if needUnlock { unlockOSThread() } }() // Record when the world started. Must be after runtime_init // because nanotime on some platforms depends on startNano. runtimeInitTime = nanotime() // 开启垃圾回收器 gcenable() main_init_done = make(chan bool) // …………………… // main 包的初始化,递归的调用我们 import 进来的包的初始化函数 fn := main_init fn() close(main_init_done) needUnlock = false unlockOSThread() // …………………… // 调用 main.main 函数 fn = main_main fn() if raceenabled { racefini() } // …………………… // 进入系统调用,退出进程,可以看出 main goroutine 并未返回,而是直接进入系统调用退出进程了 exit(0) // 保护性代码,如果 exit 意外返回,下面的代码会让该进程 crash 死掉 for { var x *int32 *x = 0 }
}
main
函数执行流程如下图:
从流程图可知,main goroutine 执行完之后就直接调用 exit(0)
退出了,这会导致整个进程退出,太粗暴了。
不过,main goroutine 实际上就是代表用户的 main 函数,它都执行完了,肯定是用户的任务都执行完了,直接退出就可以了,就算有其他的 goroutine 没执行完,同样会直接退出。
package main
import "fmt"
func main() { go func() {fmt.Println("hello qcrao.com")}()
}
在这个例子中,main gorutine 退出时,还来不及执行 go出去
的函数,整个进程就直接退出了,打印语句不会执行。因此,main goroutine 不会等待其他 goroutine 执行完再退出,知道这个有时能解释一些现象,比如上面那个例子。
这时,心中可能会跳出疑问,我们在新创建 goroutine 的时候,不是整出了个“偷天换日”,风风火火地设置了 goroutine 退出时应该跳到 runtime.goexit
函数吗,怎么这会不用了,闲得慌?
回顾一下上一讲的内容,跳转到 main 函数的两行代码:
// 把 sched.pc 值放入 BX 寄存器
MOVQ gobuf_pc(BX), BX
// JMP 把 BX 寄存器的包含的地址值放入 CPU 的 IP 寄存器,于是,CPU 跳转到该地址继续执行指令
JMP BX
直接使用了一个跳转,并没有使用 CALL
指令,而 runtime.main 函数中确实也没有 RET
返回的指令。所以,main goroutine 执行完后,直接调用 exit(0) 退出整个进程。
那之前整地“偷天换日”还有用吗?有的!这是针对非 main goroutine 起作用。
参考资料【阿波张 非 goroutine 的退出】中用调试工具验证了非 main goroutine 的退出,感兴趣的可以去跟着实践一遍。
我们继续探索非 main goroutine (后文我们就称 gp 好了)的退出流程。
gp
执行完后,RET 指令弹出 goexit
函数地址(实际上是 funcPC(goexit)+1),CPU 跳转到 goexit
的第二条指令继续执行:
// src/runtime/asm_amd64.s
// 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
直接调用 runtime·goexit1
:
// src/runtime/proc.go
// Finishes execution of the current goroutine.
func goexit1() { // …………………… mcall(goexit0)
}
调用 mcall
函数:
// 切换到 g0 栈,执行 fn(g)
// Fn 不能返回
TEXT runtime·mcall(SB), NOSPLIT, $0-8 // 取出参数的值放入 DI 寄存器,它是 funcval 对象的指针,此场景中 fn.fn 是 goexit0 的地址 MOVQ fn+0(FP), DI get_tls(CX) // AX = g MOVQ g(CX), AX // save state in g->sched // mcall 返回地址放入 BX MOVQ 0(SP), BX // caller's PC // g.sched.pc = BX,保存 g 的 PC MOVQ BX, (g_sched+gobuf_pc)(AX) LEAQ fn+0(FP), BX // caller's SP // 保存 g 的 SP MOVQ BX, (g_sched+gobuf_sp)(AX) MOVQ AX, (g_sched+gobuf_g)(AX) MOVQ BP, (g_sched+gobuf_bp)(AX) // switch to m->g0 & its stack, call fn MOVQ g(CX), BX MOVQ g_m(BX), BX // SI = g0 MOVQ m_g0(BX), SI CMPQ SI, AX // if g == m->g0 call badmcall JNE 3(PC) MOVQ $runtime·badmcall(SB), AX JMP AX // 把 g0 的地址设置到线程本地存储中 MOVQ SI, g(CX) // g = m->g0 // 从 g 的栈切换到了 g0 的栈D MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp // AX = g,参数入栈 PUSHQ AX MOVQ DI, DX // DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址 // 读取第一个成员到 DI 寄存器 MOVQ 0(DI), DI // 调用 goexit0(g) CALL DI POPQ AX MOVQ $runtime·badmcall2(SB), AX JMP AX RET
函数参数是:
type funcval struct { fn uintptr // variable-size, fn-specific data here
}
字段 fn 就表示 goexit0 函数的地址。
L5 将函数参数保存到 DI 寄存器,这里 fn.fn 就是 goexit0 的地址。
L7 将 tls 保存到 CX 寄存器,L9 将 当前线程指向的 goroutine (非 main goroutine,称为 gp)保存到 AX 寄存器,L11 将调用者(调用 mcall 函数)的栈顶,这里就是 mcall 完成后的返回地址,存入 BX 寄存器。
L13 将 mcall 的返回地址保存到 gp 的 g.sched.pc 字段,L14 将 gp 的栈顶,也就是 SP 保存到 BX 寄存器,L16 将 SP 保存到 gp 的 g.sched.sp 字段,L17 将 g 保存到 gp 的 g.sched.g 字段,L18 将 BP 保存 到 gp 的 g.sched.bp 字段。这一段主要是保存 gp 的调度信息。
L21 将当前指向的 g 保存到 BX 寄存器,L22 将 g.m 字段保存到 BX 寄存器,L23 将 g.m.g0 字段保存到 SI,g.m.g0 就是当前工作线程的 g0。
现在,SI = g0, AX = gp,L25 判断 gp 是否是 g0,如果 gp == g0 说明有问题,执行 runtime·badmcall。正常情况下,PC 值加 3,跳过下面的两条指令,直接到达 L30。
L30 将 g0 的地址设置到线程本地存储中,L32 将 g0.SP 设置到 CPU 的 SP 寄存器,这也就意味着我们从 gp 栈切换到了 g0 的栈,要变天了!
L34 将参数 gp 入栈,为调用 goexit0 构造参数。L35 将 DI 寄存器的内容设置到 DX 寄存器,DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址。L36 读取 DI 第一成员,也就是 goexit0 函数的地址。
L40 调用 goexit0 函数,这已经是在 g0 栈上执行了,函数参数就是 gp。
到这里,就会去执行 goexit0 函数,注意,这里永远都不会返回。所以,在 CALL 指令后面,如果返回了,又会去调用 runtime.badmcall2
函数去处理意外情况。
来继续看 goexit0:
// goexit continuation on g0.
// 在 g0 上执行
func goexit0(gp *g) { // g0 _g_ := getg() casgstatus(gp, _Grunning, _Gdead) if isSystemGoroutine(gp) { atomic.Xadd(&sched.ngsys, -1) } // 清空 gp 的一些字段 gp.m = nil gp.lockedm = nil _g_.m.lockedg = nil gp.paniconfault = false gp._defer = nil // should be true already but just in case. gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data. gp.writebuf = nil gp.waitreason = "" gp.param = nil gp.labels = nil gp.timer = nil // Note that gp's stack scan is now "valid" because it has no // stack. gp.gcscanvalid = true // 解除 g 与 m 的关系 dropg() if _g_.m.locked&^_LockExternal != 0 { print("invalid m->locked = ", _g_.m.locked, "\n") throw("internal lockOSThread error") } _g_.m.locked = 0 // 将 g 放入 free 队列缓存起来 gfput(_g_.m.p.ptr(), gp) schedule()
}
它主要完成最后的清理工作:
把 g 的状态从
_Grunning
更新为_Gdead
;清空 g 的一些字段;
调用 dropg 函数解除 g 和 m 之间的关系,其实就是设置 g->m = nil, m->currg = nil;
把 g 放入 p 的 freeg 队列缓存起来供下次创建 g 时快速获取而不用从内存分配。freeg 就是 g 的一个对象池;
调用 schedule 函数再次进行调度。
到这里,gp 就完成了它的历史使命,功成身退,进入了 goroutine 缓存池,待下次有任务再重新启用。
而工作线程,又继续调用 schedule 函数进行新一轮的调度,整个过程形成了一个循环。
总结一下,main goroutine 和普通 goroutine 的退出过程:
对于 main goroutine,在执行完用户定义的 main 函数的所有代码后,直接调用 exit(0) 退出整个进程,非常霸道。
对于普通 goroutine 则没这么“舒服”,需要经历一系列的过程。先是跳转到提前设置好的 goexit 函数的第二条指令,然后调用 runtime.goexit1,接着调用 mcall(goexit0)
,而 mcall 函数会切换到 g0 栈,运行 goexit0 函数,清理 goroutine 的一些字段,并将其添加到 goroutine 缓存池里,然后进入 schedule 调度循环。到这里,普通 goroutine 才算完成使命。
参考资料
千难万险 —— goroutine 从生到死(六)相关推荐
- 湖南人文科技学院没有计算机一级能毕业吗,在湖南人文科技学院读书真的是生不如死...
在湖南人文科技学院读了将近4年书了,本来是想:反正都快要走了,还是算了吧,毕竟都快忍了4了,但是越是忍耐,学校就越过分,今天实在是忍无可忍,有种生不如死的感觉... 好吧,进入今天的主题吧.在湖南人文 ...
- 整顿满月,如今现金贷生不如死
去年现金贷平台野蛮生长,现金贷业务高歌猛进,各种现金贷平台如雨后春笋般出现,但是由于准入门槛低,相关监管政策缺位等原因,现金贷业务也带来了很大的社会影响,最典型的是高息放款,不当催收,侵犯个人隐私等行 ...
- 【福利】囚犯抓绿豆,谁生谁死?
囚犯抓绿豆,谁生谁死? 五个囚犯先后从 100 颗绿豆中抓绿豆.抓得最多和最少的人将被处死,不能交流,可以摸出剩下绿豆的数量,谁的存活几率最大? 提示: 1.他们都是很聪明的人: 2.他们的原则是先求 ...
- wince 内存释放_【转载】让我生不如死的WINCE内存泄漏
转自天极网,URL=http://dev.yesky.com/346/8269346.shtml 很多实时嵌入式设备是长时间不间断运行的,即使是少许的内存泄漏,也会积少成多,对嵌入式系统带来灾难性的影 ...
- 兔子繁殖问题:一对兔子从出生后第三个月开始,每月生一对小兔子。小兔子到第三个月又开始生下一代小兔子。假若兔子只生不死,一月份抱来一对刚出生的小兔子,问一年中每个月各有多少对兔子(C++)(迭代法)
算法经典题型13 兔子繁殖问题:一对兔子从出生后第三个月开始,每月生一对小兔子.小兔子到第三个月又开始生下一代小兔子.假若兔子只生不死,一月份抱来一对刚出生的小兔子,问一年中每个月各有多少对兔子.(三 ...
- 专访网秦创始人林宇:我曾每天戴20公斤手铐 生不如死
雷帝网 雷建平 9月11日报道 网秦创始人林宇与凌动智行CEO史文勇这对昔日的高中同学.创业搭档的矛盾开始公开化. 林宇日前对雷帝网透露,其曾经有13个月度日如年,生不如死,其中,有9个多月是每天戴着 ...
- FaceBook中国程序员之死,38岁跳楼轻生,压力让他生不如死!
事件经过 本月19日,Facebook硅谷总部的园区,一名中国员工从4楼跳下轻生,年仅38岁! 随后警方认定这位中国员工当场死亡,没有涉嫌谋杀嫌疑,属于自杀!这名死者的英文名字叫Qin Chen,中文 ...
- iOS App由生到死的过程
万物都会经历由生到死的过程,人不例外,当然App也是如此,此谓App的声明周期.具体来说App的声明周期就是App从启动到关闭这一过程中发生的一系列事件.由于iOS4中引入了多任务功能,App能够在后 ...
- 生、死、腾讯、360
360和网易有道宣布,360搜索将为有道搜索提供技术支持服务,用户在使用有道搜索时,搜索结果将由360搜索提供,并且在搜索框旁会标注360搜索提供技术支持. 官方称之为战略合作升级,说得也没错. 但我 ...
最新文章
- AlphaGo Zero的伟大与局限——ResNet作者、旷视研究院院长孙剑深度评述
- 罗辑思维:情怀还是生意?
- 现在电脑的主流配置_玩手游是因为电脑配置差?现在来告诉你这些网游需要啥配置...
- Spark 1.4新特性概述
- 最短路径--Floyd算法
- c语言2维动态数组,如何创建一个动态2维数组?
- php取表中最大的id,php中的增删查改
- Java中的引用数据类型-BigDecimal
- Android 读写SDcard (转)
- 过去几年的互联网经济,首先是“免费经济”
- 模型与高性能服务器结合,Epoll模型的高性能服务器丢失数据问题解决
- 极速办公(word)如何绘制流程图
- 版权声明--关于本人BLOG发表的带有原创标识的文章相关
- 计算机一点桌面显示删除,电脑开机后任意点击桌面图标后不停出现删除文件窗口?...
- STN(Spatial Transformer Networks)
- 三星6818基于uboot的流水灯程序
- Matlab读取股票数据
- Java消息队列三道面试题详解
- Differentially Private Learning with Adaptive Clipping
- 高考导数大题中的双变量不等式问题的求解思路
热门文章
- excel自动生成舒尔特表_EXCEL自动生成的PPT
- mybatis-plus根据多个字段排序_Mybatis Plus学习笔记(逻辑删除/动态填充/常用插件)...
- EOS 帐户权限操作--你找不到的干货 (原创) 续集-EOS 3.0
- 调用远程service aidl接口定义
- [CF475E]Strongly Connected City 2
- 超级寒潮考验智慧城市万亿投资
- zabbix配置发送报警邮件
- 当网页太多时,用锚点 以及超链接的使用
- 去掉登陆下的“扫一扫,访问微社区
- java 正确使用 Volatile 变量