Go排程器系列(4)原始碼閱讀與探索
各位朋友,這次想跟大家分享一下Go排程器原始碼閱讀相關的知識和經驗,網路上已經有很多剖析原始碼的好文章,所以這篇文章不是又一篇原始碼剖析文章,注重的不是原始碼分析分享,而是帶給大家一些學習經驗,希望大家能更好的閱讀和掌握Go排程器的實現。
本文主要分2個部分:
- 解決如何閱讀原始碼的問題。閱讀原始碼本質是把腦海裡已經有的排程設計,看看到底是不是這麼實現的,是怎麼實現的。
- 帶給你一個探索Go排程器實現的辦法。原始碼都到手了,你可以修改、窺探,通過這種方式解決閱讀原始碼過程中的疑問,驗證一些想法。比如:負責排程的是g0,怎麼才能
schedule()
在執行時,當前是g0呢?
如何閱讀原始碼
閱讀前提
閱讀Go原始碼前,最好已經掌握Go排程器的設計和原理,如果你還無法回答以下問題:
- 為什麼需要Go排程器?
- Go排程器與系統排程器有什麼區別和關係/聯絡?
- G、P、M是什麼,三者的關係是什麼?
- P有預設幾個?
- M同時能繫結幾個P?
- M怎麼獲得G?
- M沒有G怎麼辦?
- 為什麼需要全域性G佇列?
- Go排程器中的負載均衡的2種方式是什麼?
- work stealing是什麼?什麼原理?
- 系統呼叫對G、P、M有什麼影響?
- Go排程器搶佔是什麼樣的?一定能搶佔成功嗎?
建議閱讀Go排程器系列文章,以及文章中的參考資料:
優秀原始碼資料推薦
既然你已經能回答以上問題,說明你對Go排程器的設計已經有了一定的掌握,關於Go排程器原始碼的優秀資料已經有很多,我這裡推薦2個:
- 雨痕的Go原始碼剖析六章併發排程,不止是原始碼,是以原始碼為基礎進行了詳細的Go排程器介紹:ttps://github.com/qyuhen/book
- Go夜讀第12期,golang中goroutine的排程,M、P、G各自的一生狀態,以及轉換關係:https://reading.developerlearning.cn/reading/12-2018-08-02-goroutine-gpm/
Go排程器的原始碼還涉及GC等,閱讀原始碼時,可以暫時先跳過,主抓排程的邏輯。
另外,Go排程器涉及彙編,也許你不懂彙編,不用擔心,雨痕的文章對彙編部分有進行解釋。
最後,送大家一幅流程圖,畫出了主要的排程流程,大家也可邊閱讀邊畫,增加理解,高清版可到部落格下載(原圖原文跳轉)。
如何探索排程器
這部分教你探索Go排程器的原始碼,驗證想法,主要思想就是,下載Go的原始碼,新增除錯列印,編譯修改的原始檔,生成修改的go,然後使用修改go執行測試程式碼,觀察結果。
下載和編譯Go
-
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
-
初次編譯,會跑測試,耗時長一點
$ cd $GODIR/src $ ./all.bash
- 以後每次修改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`目錄。
```bash
$ 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
-
為了防止我們修改的go和過去安裝的go衝突,建立igo軟連線,指向修改的go。
$ mkdir -p ~/testgo/bin $ cd ~/testgo/bin $ ln -sf $GODIR/bin/go igo
- 最後,把
~/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.schedtrace
和print()
函式可以用作列印除錯資訊,那我們是不是可以使用這種方法列印我們想獲取的資訊呢?當然可以。
另外,注意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排程器的時候,能多一些自己的探索和研究,而不僅僅停留在看看別人文章之上。
參考資料
- 如果這篇文章對你有幫助,請點個贊/喜歡,感謝。
- 本文作者:大彬
- 如果喜歡本文,隨意轉載,但請保留此原文連結:http://lessisbetter.site/2019/04/14/golang-scheduler-4-explore-source-code/
相關文章
- goroutine排程原始碼閱讀筆記Go原始碼筆記
- Go排程器系列(2)巨集觀看排程器Go
- Go排程器系列(3)圖解排程原理Go圖解
- go 原始碼分析 goroutine 概覽與排程Go原始碼
- basictracer-go原始碼閱讀——SpanRecorder與wireGo原始碼
- go 中 select 原始碼閱讀Go原始碼
- opentracing-go原始碼閱讀一Go原始碼
- JDK原始碼閱讀(4):HashMap類閱讀筆記JDK原始碼HashMap筆記
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- swoole 協程原始碼解讀 (協程的排程)原始碼
- Jaeger tchannel-go —— readme原始碼閱讀Go原始碼
- Jaeger tchannel-go原始碼閱讀——PeerGo原始碼
- basictracer-go原始碼閱讀二——SpanGo原始碼
- go micro 原始碼閱讀-Options [Functional OGo原始碼Function
- DM 原始碼閱讀系列文章(一)序原始碼
- basictracer-go原始碼閱讀——event&propagationGo原始碼
- basictracer-go原始碼閱讀——examples(完結)Go原始碼
- spring原始碼閱讀--容器啟動過程Spring原始碼
- Vue原始碼閱讀--過濾器Vue原始碼過濾器
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- Appdash原始碼閱讀——Annotations與EventAPP原始碼
- Appdash原始碼閱讀——Recorder與CollectorAPP原始碼
- golang 原始碼分析之scheduler排程器Golang原始碼
- Go Runtime 的排程器Go
- Go語言排程器之主動排程(20)Go
- Go runtime 排程器精講(五):排程策略Go
- TiDB 原始碼閱讀系列文章(二十)Table PartitionTiDB原始碼
- opentracing-go原始碼閱讀——資訊攜帶Go原始碼
- 第 27 期 go mod 原始碼閱讀 part 2Go原始碼
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- Go runtime 排程器精講(二):排程器初始化Go
- SOFAJRaft原始碼閱讀-模組啟動過程Raft原始碼
- Go語言排程器之排程main goroutine(14)GoAI
- TiDB Operator 原始碼閱讀 (三) 編排元件控制迴圈TiDB原始碼元件
- Linux程式排程邏輯與原始碼分析Linux原始碼
- Golang的GMP排程模型與原始碼解析Golang模型原始碼
- ReactorKit原始碼閱讀React原始碼