# 2.4 主 Goroutine 的生與死
上一節中我們已經知道`schedinit`完成初始化工作后并不會立即執行`runtime.main`(即主 Goroutine 運行的地方)。相反,會在后續的`mstart`調用中被調度器調度執行。 這個過程中,只會將`runtime.main`的入口地址壓棧,進而將其傳遞給`newproc`進行使用, 而后`newproc`完成 G 的創建保存到 G 的運行現場中,因此真正執行會等到`mstart`后才會被調度執行。 我們在調度器一章中詳細討論調度器的調度過程,現在我們先將目光聚焦在`runtime.main`已經開始執行時的情況。
## 2.4.1 主 Goroutine 的一生
運行時包的 main 函數`runtime.main`承載了用戶代碼的 main 函數`main.main`, 并在同一個 Goroutine 上執行:
```
// 主 Goroutine
func main() {
...
// 執行棧最大限制:1GB(64位系統)或者 250MB(32位系統)
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
...
// 啟動系統后臺監控(定期垃圾回收、搶占調度等等)
systemstack(func() {
newm(sysmon, nil)
})
...
// 執行 runtime.init。運行時包中有多個 init 函數,編譯器會將他們鏈接起來。
runtime_init()
...
// 啟動垃圾回收器后臺操作
gcenable()
...
// 執行用戶 main 包中的 init 函數,因為鏈接器設定運行時不知道 main 包的地址,處理為非間接調用
fn := main_init
fn()
...
// 執行用戶 main 包中的 main 函數,同理
fn = main_main
fn()
...
// 退出
exit(0)
}
```
整個執行過程有這樣幾個關鍵步驟:
1. `systemstack`會運行`newm(sysmon, nil)`啟動后臺監控
2. `runtime_init`負責執行運行時的多個初始化函數`runtime.init`
3. `gcenable`啟用垃圾回收器
4. `main_init`開始執行用戶態`main.init`函數,這意味著所有的`main.init`均在同一個主 Goroutine 中執行
5. `main_main`開始執行用戶態`main.main`函數,這意味著`main.main`和`main.init`均在同一個 Goroutine 中執行。
## 2.4.2`pkg.init`的執行順序
運行時的`runtime_init`則由編譯器將多個`runtime.init`進行鏈接,我們可以從 函數的聲明中看到:
```
//go:linkname runtime_init runtime.init
func runtime_init()
```
運行時存在多個 init 函數,其中較為重要的幾個函數包括:
1. 垃圾回收器所需的參數檢查并創建強制啟動 GC 的監控 Goroutine
```
const (
_WorkbufSize = 2048
workbufAlloc = 32 << 10
)
func init() {
if workbufAlloc%pageSize != 0 || workbufAlloc%_WorkbufSize != 0 {
throw("bad workbufAlloc")
}
}
func init() {
go forcegchelper()
}
```
2. 確定`defer`的運行時類型:
```
var deferType *_type // _defer 結構的類型
func init() {
var x interface{}
x = (*_defer)(nil)
deferType = (*(**ptrtype)(unsafe.Pointer(&x))).elem
}
```
從這兩個`init`函數可以看出,在用戶代碼正式啟動之前,運行時還額外準備了強制 GC 的 監控并確定了 defer 的類型。 本節中我們不對這些方法做詳細分析,等到他們各自的章節中再做詳談。那么我們仍然還會有這樣 的疑問:包含多個`init`的執行順序怎樣由編譯器控制的? 我們可以驗證下面這兩個不同的程序:
```
// main1.go
package main
import (
"fmt"
_ "net/http"
)
func main() {
fmt.Printf("hello, %s", "world!")
}
```
```
// main2.go
package main
import (
_ "net/http"
"fmt"
)
func main() {
fmt.Printf("hello, %s", "world!")
}
```
他們的唯一區別就是導入包的順序不同,通過`go tool objdump -s "main.init"`可以獲得`init`函數的實際匯編代碼:
~~~asm
TEXT main.init.0(SB)
main1.go:8 0x11f0f40 65488b0c2530000000 MOVQ GS:0x30, CX
...
main1.go:9 0x11f0f76 e8a5b8e3ff CALL runtime.printstring(SB)
...
TEXT main.init(SB) <autogenerated>
...
<autogenerated>:1 0x11f10a8 e8e3b0ebff CALL fmt.init(SB)
<autogenerated>:1 0x11f10ad e88e5affff CALL net/http.init(SB)
<autogenerated>:1 0x11f10b2 e889feffff CALL main.init.0(SB)
...
~~~
~~~asm
TEXT main.init.0(SB)
...
main2.go:10 0x11f0f76 e8a5b8e3ff CALL runtime.printstring(SB)
...
TEXT main.init(SB) <autogenerated>
<autogenerated>:1 0x11f1060 65488b0c2530000000 MOVQ GS:0x30, CX
...
<autogenerated>:1 0x11f10a8 e8935affff CALL net/http.init(SB)
<autogenerated>:1 0x11f10ad e81e40ecff CALL fmt.init(SB)
<autogenerated>:1 0x11f10b2 e889feffff CALL main.init.0(SB)
...
~~~
從實際的匯編代碼可以看到,init 的順序由實際包調用順序給出,所有引入的外部包的 init 均會被 編譯器安插在當前包的`main.init.0`之前執行,而外部包的順序與引入包的順序有關。
那么某個包內的多個 init 函數是否有順序可言?我們簡單看一看編譯器關于 init 函數的實現:
```
// cmd/compile/internal/gc/init.go
// 將 init 的名字 pkg.init 重命名為 pkg.init.0
var renameinitgen int
func renameinit() *types.Sym {
s := lookupN("init.", renameinitgen)
renameinitgen++
return s
}
```
`renameinit`這個函數中實現了對 init 函數的重命名,并通過`renameinitgen`在全局記錄了 init 的索引后綴。`renameinit`會在處理函數聲明時被調用:
```
// cmd/compile/internal/gc/noder.go
func (p *noder) funcDecl(fun *syntax.FuncDecl) *Node {
name := p.name(fun.Name)
t := p.signature(fun.Recv, fun.Type)
f := p.nod(fun, ODCLFUNC, nil, nil)
// 函數沒有 reciver
if fun.Recv == nil {
// 且名字叫做 init
if name.Name == "init" {
name = renameinit() // 對其進行重命名
...
}
...
}
...
}
```
而`funcDecl`則會在 AST 的`noder`結構的方法`decls`中被調用:
```
func (p *noder) decls(decls []syntax.Decl) (l []*Node) {
var cs constState
for _, decl := range decls {
p.lineno(decl)
switch decl := decl.(type) {
case *syntax.FuncDecl:
l = append(l, p.funcDecl(decl))
...
}
}
return
}
```
一個包內的 init 函數的調用順序取決于聲明的順序,即從上而下依次調用。
## 2.4.3 小結
看到這里我們已經結束了整個 Go 程序的執行,但仍有海量的細節還沒有被敲定,完全還沒有深入 運行時的三大核心組件,運行時各類機制也都還沒有接觸。總結一下這節討論中遺留下來的問題:
1. `mstart`會如何將主 Goroutine 調度執行?
2. `sysmon`系統監控做了什么事情,它的工作原理是什么?
3. `runtime.init`的`forcegchelper`是什么?`gcenable`又做了什么?
我們在隨后的章節中一一介紹。
## 進一步閱讀的參考文獻
1. [Command compile](https://golang.org/cmd/compile/)
2. [`main_init_done`can be implemented more efficiently](https://github.com/golang/go/issues/15943)
- 第一部分 :基礎篇
- 第1章 Go語言的前世今生
- 1.2 Go語言綜述
- 1.3 順序進程通訊
- 1.4 Plan9匯編語言
- 第2章 程序生命周期
- 2.1 從go命令談起
- 2.2 Go程序編譯流程
- 2.3 Go 程序啟動引導
- 2.4 主Goroutine的生與死
- 第3 章 語言核心
- 3.1 數組.切片與字符串
- 3.2 散列表
- 3.3 函數調用
- 3.4 延遲語句
- 3.5 恐慌與恢復內建函數
- 3.6 通信原語
- 3.7 接口
- 3.8 運行時類型系統
- 3.9 類型別名
- 3.10 進一步閱讀的參考文獻
- 第4章 錯誤
- 4.1 問題的演化
- 4.2 錯誤值檢查
- 4.3 錯誤格式與上下文
- 4.4 錯誤語義
- 4.5 錯誤處理的未來
- 4.6 進一步閱讀的參考文獻
- 第5章 同步模式
- 5.1 共享內存式同步模式
- 5.2 互斥鎖
- 5.3 原子操作
- 5.4 條件變量
- 5.5 同步組
- 5.6 緩存池
- 5.7 并發安全散列表
- 5.8 上下文
- 5.9 內存一致模型
- 5.10 進一步閱讀的文獻參考
- 第二部分 運行時篇
- 第6章 并發調度
- 6.1 隨機調度的基本概念
- 6.2 工作竊取式調度
- 6.3 MPG模型與并發調度單
- 6.4 調度循環
- 6.5 線程管理
- 6.6 信號處理機制
- 6.7 執行棧管理
- 6.8 協作與搶占
- 6.9 系統監控
- 6.10 網絡輪詢器
- 6.11 計時器
- 6.12 非均勻訪存下的調度模型
- 6.13 進一步閱讀的參考文獻
- 第7章 內存分配
- 7.1 設計原則
- 7.2 組件
- 7.3 初始化
- 7.4 大對象分配
- 7.5 小對象分配
- 7.6 微對象分配
- 7.7 頁分配器
- 7.8 內存統計
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 寫屏幕技術
- 8.3 調步模型與強弱觸發邊界
- 8.4 掃描標記與標記輔助
- 8.5 免清掃式位圖技術
- 8.6 前進保障與終止檢測
- 8.7 安全點分析
- 8.8 分代假設與代際回收
- 8.9 請求假設與實務制導回收
- 8.10 終結器
- 8.11 過去,現在與未來
- 8.12 垃圾回收統一理論
- 8.13 進一步閱讀的參考文獻
- 第三部分 工具鏈篇
- 第9章 代碼分析
- 9.1 死鎖檢測
- 9.2 競爭檢測
- 9.3 性能追蹤
- 9.4 代碼測試
- 9.5 基準測試
- 9.6 運行時統計量
- 9.7 語言服務協議
- 第10章 依賴管理
- 10.1 依賴管理的難點
- 10.2 語義化版本管理
- 10.3 最小版本選擇算法
- 10.4 Vgo 與dep之爭
- 第12章 泛型
- 12.1 泛型設計的演進
- 12.2 基于合約的泛型
- 12.3 類型檢查技術
- 12.4 泛型的未來
- 12.5 進一步閱讀的的參考文獻
- 第13章 編譯技術
- 13.1 詞法與文法
- 13.2 中間表示
- 13.3 優化器
- 13.4 指針檢查器
- 13.5 逃逸分析
- 13.6 自舉
- 13.7 鏈接器
- 13.8 匯編器
- 13.9 調用規約
- 13.10 cgo與系統調用
- 結束語: Go去向何方?