<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          曹大帶我學 Go(5)—— 哪里來的 goexit

          共 4310字,需瀏覽 9分鐘

           ·

          2021-06-11 18:28

          你好,我是小X。

          曹大最近開 Go 課程了,小X 正在和曹大學 Go。

          這個系列會講一些從課程中學到的讓人醍醐灌頂?shù)臇|西,撥云見日,帶你重新認識 Go。

          在學員群里,有同學在用 dlv 調試時看到了令人不解的 goexit:goexit 函數(shù)是啥,為啥 go fun(){}() 的上層是它?看著像是一個“退出”函數(shù),為什么會出現(xiàn)在最上層?

          其實如果看過 pprof 的火焰圖,也會經??吹?goexit 這個函數(shù)。

          我們來個例子重現(xiàn)一下:

          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

          執(zhí)行命令 c 運行到斷點處,再執(zhí)行 bt 命令得到 main 函數(shù)的調用棧:

          (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 函數(shù),它是 Go 進程的 main goroutine,這里會執(zhí)行一些 init 操作、開啟 GC、執(zhí)行用戶 main 函數(shù)……

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

          其中 fnmain_main 函數(shù),表示用戶的 main 函數(shù),執(zhí)行到了這里,才真正將權力交給用戶。

          繼續(xù)執(zhí)行 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,我們跟著注明了的代碼行數(shù),順藤摸瓜,找到 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

          這還是個匯編函數(shù),它接著調用 goexit1 函數(shù)、goexit0 函數(shù),主要的功能就是將 goroutine 的各個字段清零,放入 gFree 隊列里,等待將來進行復用。

          另一方面,goexit 函數(shù)的地址是在創(chuàng)建 goroutine 的過程中,塞到棧上的。讓 CPU “誤以為”:func() 是由 goexit 函數(shù)調用的。這樣一來,當 func() 執(zhí)行完畢時,會返回到 goexit 函數(shù)做一些清理工作。

          下面這張圖能看出在 newg 的棧底塞了一個 goexit 函數(shù)的地址:

          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 就是創(chuàng)建的 goroutine,每個新建的 goroutine 都會執(zhí)行這些代碼。而 sched 結構體其實保存的是 goroutine 的執(zhí)行現(xiàn)場,每當 goroutine 被調離 CPU,它的執(zhí)行進度就是保存到這里。進度主要就是 SP、BP、PC,分別表示棧頂?shù)刂贰5椎刂?、指令位置,?goroutine 再次得到 CPU 的執(zhí)行權時,會把 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 的地址,放在了棧頂?shù)牡胤剑煜?Go 函數(shù)調用規(guī)約的朋友知道,這個位置其實就是 return addr,將來等 func() 執(zhí)行完,就會回到父函數(shù)繼續(xù)執(zhí)行,這里的父函數(shù)其實就是 goexit。

          一切早已注定。

          不過注意一點,main goroutine 和普通的 goroutine 不同的是,前者執(zhí)行完用戶 main 函數(shù)后,會直接執(zhí)行 exit 調用,整個進程退出:

          exit

          也就不會進入 goexit 函數(shù)。而普通 goroutine 執(zhí)行完畢后,則直接進入 goexit 函數(shù),做一些清理工作。

          這也就是為什么只要 main goroutine 執(zhí)行完了,就不會等其他 goroutine,直接退出。一切都是因為 exit 這個調用。

          今天我們主要講了 goexit 是怎么被安插到 goroutine 的棧上,從而實現(xiàn) goroutine 執(zhí)行完畢后再回到 goexit 函數(shù)。

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

          源碼面前,了無秘密。

          好了,這就是今天全部的內容了~ 我是小X,我們下期再見~


          歡迎關注曹大的 TechPaper 以及碼農桃花源~

          瀏覽 125
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青青草黄色视屏 | 在线aⅴ亚洲中文字幕 | 草草网| 中文字幕一区二区三区四区五区人 | 日韩欧美中文字幕免费看 |