当前位置: 首页 > 科技观察

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

时间:2023-03-16 14:26:25 科技观察

曹大带我去哪里学围棋?转载本文请联系码农桃花源公众号。大家好,我是小X,曹达最近开了围棋课程,小X跟曹达一起围棋。本系列将讨论从课程中学到的一些有启发性的东西,拨开乌云,带你回到Go。学生群里,有同学在用dlv调试的时候看到了莫名其妙的goexit:goexit函数是什么,为什么在gofun(){}()的上层?看起来是一个“退出”的功能,为什么会出现在顶层呢?事实上,如果你看过pprof的火焰图,你会经常看到goexit函数。举个例子重现一下:packagemainimport"time"funcmain(){gofunc(){println("helloworld")}()time.Sleep(10*time.Minute)}开始dlv调试,并在不同的地方打断点:(dlv)ba.go:5Breakpoint1(enabled)setat0x106d12fformain.main()./a.go:5(dlv)ba.go:6Breakpoint2(enabled)setat0x106d13dformain.main()./a.go:6(dlv)ba.go:7Breakpoint3(enabled)setat0x106d1a0formain.main.func1()./a.go:7执行命令c运行到断点处,然后执行bt命令获取main函数的调用栈:(dlv)bt00x000000000106d12finmain.mainat./a.go:510x0000000001035c0finruntime.mainat/usr/local/go/src/runtime/proc.go:20420x0000000001064961inruntime.goexitat/usr/local/go/src/runtime/asm_amd64.s:1374它的上层是runtime。main,找到原代码位置,main函数位于src/runtime/proc.go,是Go进程的主goroutine,这里会进行一些init操作,启用GC,执行用户main函数...fn:=main_main//proc.go:203fn()//proc.go:204其中fn为main_main函数,表示用户的main函数,执行到这里后才真正把权力交给了用户。继续执行c命令和bt命令得到go行的调用栈:00x000000000106d13dinmain.mainat./a.go:610x0000000001035c0finruntime.mainat/usr/local/go/src/runtime/proc.go:20420x0000000001064961inruntime.goexitat/usr/go/src/runtime/asm_amd64.s:1374和println的调用栈:00x000000000106d1a0inmain.main.func1at./a.go:710x0000000001064961inruntime.goexitat/usr/local/go/src/runtime/asm_amd64.s:1374可以看出调用栈最顶层是runtime.goexit,我们按照指示的代码行数,找到goexit代码://Thetop-mostfunctionrunningonagoroutine//returnstogoexit+PCQuantum.TEXTruntime·goexit(SB),NOSPLIT,$0-0BYTE$0x90//NOPCALLruntime·goexit1(SB)//不返回//tracebackfromgoexit1musthitcoderangeofgoexitBYTE$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.PCQuantumnewg.sched.g=guintptr(unsafe.Pointer(newg))gostartcallfn(&newg.sched,fn)其中newg是创建的goroutine,每个新的goroutine都会执行这些代码。sched结构实际上保存了goroutine的执行地点。每当goroutine从CPU调出时,它的执行进度就保存在这里。progress主要是SP,BP,PC,分别代表栈顶地址,栈底地址,指令位置。当goroutine再次获得CPU的执行权时,会将SP、BP、PC加载到寄存器中,从断点处继续运行。.回到上面几行代码,pc被赋值给funcPC(goexit),最后在gostartcall://adjustGobufasifitexecutedacalltofnwithcontextctxt//andthendidanimmediategosave.funcgostartcall(buf*gobuf,fn,ctxtunsafe.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函数调用协议的朋友都知道,这个位置其实就是returnaddr。以后func()执行完后,会回到parent函数继续执行,这里的parent函数其实就是goexit。一切都已经注定了。但是要注意,maingoroutine和普通goroutine的区别在于,前者执行完用户main函数后,会直接执行exit调用,整个流程退出:exit不会进入goexit函数。普通goroutine执行完后,直接进入goexit函数做一些清理工作。这就是为什么只要主goroutine执行完毕,就不会等待其他goroutine,直接退出。都是因为退出电话。今天我们主要讲的是如何将goexit放到goroutine栈中,让goroutine执行完后返回goexit函数。看似不可理解的事情是不是更清楚了?在源代码面前,没有秘密。好了,今天就到这里吧~我是小X,下期见~