Go調度器系列(4)源碼閱讀與探索

各位朋友,這次想跟大家分享一下Go調度器源碼閱讀相關的知識和經驗,網絡上已經有很多剖析源碼的好文章,所以這篇文章不是又一篇源碼剖析文章,注重的不是源碼分析分享,而是帶給大家一些學習經驗,希望大家能更好的閱讀和掌握Go調度器的實現

本文主要分2個部分:

  1. 解決如何閱讀源碼的問題。閱讀源碼本質是把腦海裏已經有的調度設計,看看到底是不是這麼實現的,是怎麼實現的。
  2. 帶給你一個探索Go調度器實現的辦法。源碼都到手了,你可以修改、窺探,通過這種方式解決閱讀源碼過程中的疑問,驗證一些想法。比如:負責調度的是g0,怎麼才能schedule()在執行時,當前是g0呢?

如何閱讀源碼

閱讀前提

閱讀Go源碼前,最好已經掌握Go調度器的設計和原理,如果你還無法回答以下問題:

  1. 爲什麼需要Go調度器?
  2. Go調度器與系統調度器有什麼區別和關係/聯繫?
  3. G、P、M是什麼,三者的關係是什麼?
  4. P有默認幾個?
  5. M同時能綁定幾個P?
  6. M怎麼獲得G?
  7. M沒有G怎麼辦?
  8. 爲什麼需要全局G隊列?
  9. Go調度器中的負載均衡的2種方式是什麼?
  10. work stealing是什麼?什麼原理?
  11. 系統調用對G、P、M有什麼影響?
  12. Go調度器搶佔是什麼樣的?一定能搶佔成功嗎?

建議閱讀Go調度器系列文章,以及文章中的參考資料:

  1. Go調度器系列(1)起源
  2. Go調度器系列(2)宏觀看調度器
  3. Go調度器系列(3)圖解調度原理

優秀源碼資料推薦

既然你已經能回答以上問題,說明你對Go調度器的設計已經有了一定的掌握,關於Go調度器源碼的優秀資料已經有很多,我這裏推薦2個:

  1. 雨痕的Go源碼剖析六章併發調度,不止是源碼,是以源碼爲基礎進行了詳細的Go調度器介紹:ttps://github.com/qyuhen/book
  2. Go夜讀第12期,golang中goroutine的調度,M、P、G各自的一生狀態,以及轉換關係:https://reading.developerlear...

Go調度器的源碼還涉及GC等,閱讀源碼時,可以暫時先跳過,主抓調度的邏輯。

另外,Go調度器涉及彙編,也許你不懂彙編,不用擔心,雨痕的文章對彙編部分有進行解釋。

最後,送大家一幅流程圖,畫出了主要的調度流程,大家也可邊閱讀邊畫,增加理解,高清版可到博客下載(原圖原文跳轉)

如何探索調度器

這部分教你探索Go調度器的源碼,驗證想法,主要思想就是,下載Go的源碼,添加調試打印,編譯修改的源文件,生成修改的go,然後使用修改go運行測試代碼,觀察結果。

下載和編譯Go

  1. Github下載,並且換到go1.11.2分支,本文所有代碼修改都基於go1.11.2版本。
$ GODIR=$GOPATH/src/github.com/golang/go
$ mkdir -p $GODIR
$ cd $GODIR/..
$ git clone https://github.com/golang/go.git
$ cd go
$ git fetch origin go1.11.2
$ git checkout origin/go1.11.2
$ git checkout -b go1.11.2
$ git checkout go1.11.2
  1. 初次編譯,會跑測試,耗時長一點
$ cd $GODIR/src
$ ./all.bash
  1. 以後每次修改go源碼後可以這樣,4分鐘左右可以編譯完成
$ cd  $GODIR/src
$ time ./make.bash
Building Go cmd/dist using /usr/local/go.
Building Go toolchain1 using /usr/local/go.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for linux/amd64.
---
Installed Go for linux/amd64 in /home/xxx/go/src/github.com/golang/go
Installed commands in /home/xxx/go/src/github.com/golang/go/bin

real    1m11.675s
user    4m4.464s
sys    0m18.312s

編譯好的go和gofmt在$GODIR/bin目錄。

$ ll $GODIR/bin
total 16044
-rwxrwxr-x 1 vnt vnt 13049123 Apr 14 10:53 go
-rwxrwxr-x 1 vnt vnt  3377614 Apr 14 10:53 gofmt
  1. 爲了防止我們修改的go和過去安裝的go衝突,創建igo軟連接,指向修改的go。
$ mkdir -p ~/testgo/bin
$ cd ~/testgo/bin
$ ln -sf $GODIR/bin/go igo
  1. 最後,把~/testgo/bin加入到PATH,就能使用igo來編譯代碼了,運行下igo,應當獲得go1.11.2的版本:
$ igo version
go version go1.11.2 linux/amd64

當前,已經掌握編譯和使用修改的go的辦法,接下來就以1個簡單的例子,教大家如何驗證想法。

驗證schedule()由g0執行

閱讀源碼的文章,你已經知道了g0是負責調度的,並且g0是全局變量,可在runtime包的任何地方直接使用,看到schedule()代碼如下(所在文件:$GODIR/src/runtime/proc.go):

// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
    // 獲取當前g,調度時這個g應當是g0
    _g_ := getg()

    if _g_.m.locks != 0 {
        throw("schedule: holding locks")
    }

    // m已經被某個g鎖定,先停止當前m,等待g可運行時,再執行g,並且還得到了g所在的p
    if _g_.m.lockedg != 0 {
        stoplockedm()
        execute(_g_.m.lockedg.ptr(), false) // Never returns.
    }

    // 省略...
}

問題:既然g0是負責調度的,爲何schedule()每次還都執行_g_ := getg(),直接使用g0不行嗎?schedule()真的是g0執行的嗎?

《Go調度器系列(2)宏觀看調度器》這篇文章中我曾介紹了trace的用法,閱讀代碼時發現使用debug.schedtraceprint()函數可以用作打印調試信息,那我們是不是可以使用這種方法打印我們想獲取的信息呢?當然可以。

另外,注意print()並不是fmt.Print(),也不是C語言的printf,所以不是格式化輸出,它是彙編實現的,我們不深入去了解它的實現了,現在要掌握它的用法:

// The print built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Print is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func print(args ...Type)

從上面可以看到,它接受可變長參數,我們使用的時候只需要傳進去即可,但要手動控制格式。

我們修改schedule()函數,使用debug.schedtrace > 0控制打印,加入3行代碼,把goid給打印出來,如果始終打印goid爲0,則代表調度確實是由g0執行的:

if debug.schedtrace > 0 {
    print("schedule(): goid = ", _g_.goid, "\n") // 會是0嗎?是的
}

schedule()如下:

// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
    // 獲取當前g,調度時這個g應當是g0
    _g_ := getg()

    if debug.schedtrace > 0 {
        print("schedule(): goid = ", _g_.goid, "\n") // 會是0嗎?是的
    }

    if _g_.m.locks != 0 {
        throw("schedule: holding locks")
    }
    // ...
}

編譯igo:

$ cd  $GODIR/src
$ ./make.bash

編寫一個簡單的demo(不能更簡單):

package main

func main() {
}

結果如下,你會發現所有的schedule()函數調用都打印goid = 0,足以證明Go調度器的調度由g0完成(如果你認爲還是缺乏說服力,可以寫複雜一些的demo):

$ GODEBUG=schedtrace=1000 igo run demo1.go
schedule(): goid = 0
schedule(): goid = 0
SCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
schedule(): goid = 0
// 省略幾百行

啓發比結論更重要,希望各位朋友在學習Go調度器的時候,能多一些自己的探索和研究,而不僅僅停留在看看別人文章之上

參考資料

  1. Installing Go from source
  1. 如果這篇文章對你有幫助,請點個贊/喜歡,感謝
  2. 本文作者:大彬
  3. 如果喜歡本文,隨意轉載,但請保留此原文鏈接:http://lessisbetter.site/2019...

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章