聊聊 g0

Stefno發表於2021-01-16

很多時候,當我們跟著原始碼去理解某種事物時,基本上可以認為是以時間順序展開,這是編年體的邏輯。還有另一種邏輯,紀傳體,它以人物為中心編排史事,使得讀者更聚焦於某個人物。以一種新的視角,把所有的事情串連起來,令人大呼過癮。今天我們試著以這樣一種邏輯再看 g0。

回顧一下 Go 夜讀第 78 期,關於排程器原始碼分析的內容。我們講過,與主執行緒繫結的 M 對應的 g0 的主要作用是提供一個比一般 goroutine 要大的多棧(64K)供 runtime 程式碼執行。

初始化的過程中,在函式 runtime·rt0_go 裡會給主執行緒的 g0 分配棧空間:

g0 棧空間

之後,主執行緒會與 m0 繫結,m0 又與 g0 繫結:

主執行緒繫結 m0,g0

之後,又與 p0 繫結:

g0-p0-m0

這樣,主執行緒的這一套 GPM 就可以轉起來了。接著,就建立了 main goroutine,放入 p0 的本地待執行佇列。最後,通過 schedule() 函式進入排程迴圈。

前面說的是程式初始化的過程中,g0 是如何誕生的。當執行到 main.main() 函式,也說是使用者在 main 包下寫的 main 函式裡,我們隨手一句:

go func() {
    // 要做的事
}()

就啟動了一個 goroutine 時,在 Go 編譯器的作用下,最終會轉化成 newproc 函式。在 newproc 函式的內部,會在 g0 棧上呼叫 newproc1 函式,完成後續的工作。建立完成後,會將新建立的 goroutine 放入 _p_ 的本地待執行佇列。

因為新增加了一個 g,這時會嘗試去喚醒一個 P 來一起執行任務。判斷條件是:

if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
	wakep()
}

即在有空閒 P 以及沒有正在“找工作的 M”的情況下,才會嘗試去喚醒一個 P。我們又知道,其實 P 的數量在程式執行過程中一般不會變化,所以這裡所謂的喚醒其實就是把空閒的 P 利用起來。

通過 wakep() -> startm() -> newm() -> allocm() -> malg() 這條鏈路建立 g0,這裡 g0 的棧大小實際上為 8KB

mp.g0 = malg(8192 * sys.StackGuardMultiplier) // sys.StackGuardMultiplier 在 linux 裡為 1

g0 作為一個特殊的 goroutine,為 scheduler 執行排程迴圈提供了場地(棧)。對於一個執行緒來說,g0 總是它第一個建立的 goroutine。之後,它會不斷地尋找其他普通的 goroutine 來執行,直到程式退出。

當需要執行一些任務,且不想擴棧時,就可以用到 g0 了,因為 g0 的棧比較大。g0 其他的一些“職責”有:建立 goroutine、deferproc 函式裡新建 _defer、垃圾回收相關的工作(例如 stw、掃描 goroutine 的執行棧、一些標識清掃的工作、棧增長)等等。

因為 g0 這樣一個特殊的 goroutine 所做的工作,使得 Go 程式執行地更快。

注:最近在 medium 上看到了一個非常讚的關於 Go 的部落格,題圖畫得很有閱讀的慾望。這篇文章也是參考於其中的一篇

相關文章