詳解 Go 程序的啓動流程,你知道 g0,m0 是什麼嗎?

大家好,我是煎魚。

自古應用程序均從 Hello World 開始,你我所寫的 Go 語言亦然:

import "fmt"

func main() {
 fmt.Println("hello world.")
}

這段程序的輸出結果爲 hello world.,就是這麼的簡單又直接。但這時候又不禁思考了起來,這個 hello world. 是怎麼輸出來,經歷了什麼過程。

真是非常的好奇,今天我們就一起來探一探 Go 程序的啓動流程。其中涉及到 Go Runtime 的調度器啓動,g0,m0 又是什麼?

車門焊死,正式開始吸魚之路。

Go 引導階段

查找入口

首先編譯上文提到的示例程序:

GOFLAGS="-ldflags=-compressdwarf=false" go build

在命令中指定了 GOFLAGS 參數,這是因爲在 Go1.11 起,爲了減少二進制文件大小,調試信息會被壓縮。導致在 MacOS 上使用 gdb 時無法理解壓縮的 DWARF 的含義是什麼(而我恰恰就是用的 MacOS)。

因此需要在本次調試中將其關閉,再使用 gdb 進行調試,以此達到觀察的目的:

$ gdb awesomeProject 
(gdb) info files
Symbols from "/Users/eddycjy/go-application/awesomeProject/awesomeProject".
Local exec file:
 `/Users/eddycjy/go-application/awesomeProject/awesomeProject', file type mach-o-x86-64.
 Entry point: 0x1063c80
 0x0000000001001000 - 0x00000000010a6aca is .text
 ...
(gdb) b *0x1063c80
Breakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.

通過 Entry point 的調試,可看到真正的程序入口在 runtime 包中,不同的計算機架構指向不同。例如:

其最終指向了 rt0_darwin_amd64.s 文件,這個文件名稱非常的直觀:

Breakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.

rt0 代表 runtime0 的縮寫,指代運行時的創世,超級奶爸:

同時 Go 語言還支持更多的目標系統架構,例如:AMD64、AMR、MIPS、WASM 等:

源碼目錄

若有興趣可到 src/runtime 目錄下進一步查看,這裏就不一一介紹了。

入口方法

在 rt0_linux_amd64.s 文件中,可發現 _rt0_amd64_darwin JMP 跳轉到了 _rt0_amd64 方法:

TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
 JMP _rt0_amd64(SB)
...

緊接着又跳轉到 runtime·rt0_go 方法:

TEXT _rt0_amd64(SB),NOSPLIT,$-8
 MOVQ 0(SP), DI // argc
 LEAQ 8(SP), SI // argv
 JMP runtime·rt0_go(SB)

該方法將程序輸入的 argc 和 argv 從內存移動到寄存器中。

棧指針(SP)的前兩個值分別是 argc 和 argv,其對應參數的數量和具體各參數的值。

開啓主線

程序參數準備就緒後,正式初始化的方法落在 runtime·rt0_go 方法中:

TEXT runtime·rt0_go(SB),NOSPLIT,$0
 ...
 CALL runtime·check(SB)
 MOVL 16(SP), AX  // copy argc
 MOVL AX, 0(SP)
 MOVQ 24(SP), AX  // copy argv
 MOVQ AX, 8(SP)
 CALL runtime·args(SB)
 CALL runtime·osinit(SB)
 CALL runtime·schedinit(SB)

 // create a new goroutine to start program
 MOVQ $runtime·mainPC(SB), AX  // entry
 PUSHQ AX
 PUSHQ $0   // arg size
 CALL runtime·newproc(SB)
 POPQ AX
 POPQ AX

 // start this M
 CALL runtime·mstart(SB)
 ...

runtime·rt0_go 方法中,其主要是完成各類運行時的檢查,系統參數設置和獲取,並進行大量的 Go 基礎組件初始化。

初始化完畢後進行主協程(main goroutine)的運行,並放入等待隊列(GMP 模型),最後調度器開始進行循環調度。

小結

根據上述源碼剖析,可以得出如下 Go 應用程序引導的流程圖:

Go 程序引導過程

在 Go 語言中,實際的運行入口並不是用戶日常所寫的 main func,更不是 runtime.main 方法,而是從 rt0_*_amd64.s 開始,最終再一路 JMP 到 runtime·rt0_go 裏去,再在該方法裏完成一系列 Go 自身所需要完成的絕大部分初始化動作。

其中整體包括:

後續將會繼續剖析將進一步剖析 runtime·rt0_go 裏的愛與恨,尤其像是 runtime.mainruntime.schedinit 等調度方法,都有非常大的學習價值,有興趣的小夥伴可以持續關注。

Go 調度器初始化

知道了 Go 程序是怎麼引導起來的之後,我們需要了解 Go Runtime 中調度器是怎麼流轉的。

runtime.mstart

這裏主要關注 runtime.mstart 方法:

func mstart() {
 // 獲取 g0
 _g_ := getg()

 // 確定棧邊界
 osStack := _g_.stack.lo == 0
 if osStack {
  size := _g_.stack.hi
  if size == 0 {
   size = 8192 * sys.StackGuardMultiplier
  }
  _g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
  _g_.stack.lo = _g_.stack.hi - size + 1024
 }
 _g_.stackguard0 = _g_.stack.lo + _StackGuard
 _g_.stackguard1 = _g_.stackguard0
  
  // 啓動 m,進行調度器循環調度
 mstart1()

 // 退出線程
 if mStackIsSystemAllocated() {
  osStack = true
 }
 mexit(osStack)
}

runtime.mstart1

這麼看來其實質邏輯在 mstart1 方法,我們繼續往下剖析:

func mstart1() {
 // 獲取 g,並判斷是否爲 g0
 _g_ := getg()
 if _g_ != _g_.m.g0 {
  throw("bad runtime·mstart")
 }

 // 初始化 m 並記錄調用方 pc、sp
 save(getcallerpc(), getcallersp())
 asminit()
 minit()

 // 設置信號 handler
 if _g_.m == &m0 {
  mstartm0()
 }
 // 運行啓動函數
 if fn := _g_.m.mstartfn; fn != nil {
  fn()
 }

 if _g_.m != &m0 {
  acquirep(_g_.m.nextp.ptr())
  _g_.m.nextp = 0
 }
 schedule()
}

忙活了一大圈,終於進入到開題的主菜了,原來潛伏的很深的 schedule 方法纔是真正做調度的方法,其他都是前置處理和準備數據。

由於篇幅問題,schedule 方法會放到下篇再繼續剖析,我們先聚焦本篇的一些細節點。

問題深剖

不過到這裏篇幅也已經比較長了,積累了不少問題。我們針對在 Runtime 中出鏡率最高的兩個元素進行剖析:

  1. m0 是什麼,作用是?

  2. g0 是什麼,作用是?

m0

m0 是 Go Runtime 所創建的第一個系統線程,一個 Go 進程只有一個 m0,也叫主線程。

從多個方面來看:

g0

g 一般分爲三種,分別是:

g0 比較特殊,每一個 m 都只有一個 g0(僅此只有一個 g0),且每個 m 都只會綁定一個 g0。在 g0 的賦值上也是通過彙編賦值的,其餘後續所創建的都是常規的 g。

從多個方面來看:

小結

在本章節中我們講解了 Go 調度器初始化的一個過程,分別涉及:

基於此也瞭解到了在調度器初始化過程中,需要準備什麼,初始化什麼。另外針對調度過程中最常提到的 m0、g0 的概念我們進行了梳理和說明。

總結

在今天這篇文章中,我們詳細的介紹了 Go 語言的引導啓動過程中的所有流程和初始化動作。

同時針對調度器的初始化進行了初步分析,詳細介紹了 m0、g0 的用途和區別。在下一篇文章中我們將進一步對真正調度的 schedule 方法進行詳解,這塊也是個硬骨頭了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YK-TD3bZGEgqC0j-8U6VkQ