本文是《Go語言排程器原始碼情景分析》系列的第20篇,也是第五章《主動排程》的第1小節。
Goroutine的主動排程是指當前正在執行的goroutine通過直接呼叫runtime.Gosched()函式暫時放棄執行而發生的排程。
主動排程完全是使用者程式碼自己控制的,我們根據程式碼就可以預見什麼地方一定會發生排程。比如下面的程式,在main goroutine中建立了一個新的我們稱之為g2的goroutine去執行start函式,g2在start函式的迴圈中反覆呼叫Gosched()函式放棄自己的執行權,主動把CPU讓給排程器去執行排程。
package main import ( "runtime" "sync" ) const N = 1 func main() { var wg sync.WaitGroup wg.Add(N) for i := 0; i < N; i++ { go start(&wg) } wg.Wait() } func start(wg *sync.WaitGroup) { for i := 0; i < 1000 * 1000 * 1000; i++ { runtime.Gosched() } wg.Done() }
下面我們就從這個程式開始分析主動排程是如何實現的。
首先從主動排程的入口函式Gosched()開始分析。
runtime/proc.go : 262
// Gosched yields the processor, allowing other goroutines to run. It does not // suspend the current goroutine, so execution resumes automatically. func Gosched() { checkTimeouts() //amd64 linux平臺空函式 //切換到當前m的g0棧執行gosched_m函式 mcall(gosched_m) //再次被排程起來則從這裡開始繼續執行 }
因為我們需要關注程式執行起來之後g2 goroutine的狀態,所以這裡用gdb配合原始碼一起來進行除錯和分析,首先使用b proc.go:266在Gosched函式的mcall(gosched_m)這一行設定一個斷點,然後執行程式,等程式被斷下來之後,反彙編一下程式當前正在執行的函式
(gdb) disass Dump of assembler code for function main.start: 0x000000000044fc90 <+0>:mov %fs:0xfffffffffffffff8,%rcx 0x000000000044fc99 <+9>:cmp 0x10(%rcx),%rsp 0x000000000044fc9d <+13>:jbe 0x44fcfa <main.start+106> 0x000000000044fc9f <+15>:sub $0x20,%rsp 0x000000000044fca3 <+19>:mov %rbp,0x18(%rsp) 0x000000000044fca8 <+24>:lea 0x18(%rsp),%rbp 0x000000000044fcad <+29>:xor %eax,%eax 0x000000000044fcaf <+31>:jmp 0x44fcd0 <main.start+64> 0x000000000044fcb1 <+33>:mov %rax,0x10(%rsp) 0x000000000044fcb6 <+38>:nop 0x000000000044fcb7 <+39>:nop => 0x000000000044fcb8 <+40>:lea 0x241e1(%rip),%rax # 0x473ea0 0x000000000044fcbf <+47>:mov %rax,(%rsp) 0x000000000044fcc3 <+51>:callq 0x447380 <runtime.mcall> 0x000000000044fcc8 <+56>:mov 0x10(%rsp),%rax 0x000000000044fccd <+61>:inc %rax 0x000000000044fcd0 <+64>:cmp $0x3b9aca00,%rax 0x000000000044fcd6 <+70>:jl 0x44fcb1 <main.start+33> 0x000000000044fcd8 <+72>:nop 0x000000000044fcd9 <+73>:mov 0x28(%rsp),%rax 0x000000000044fcde <+78>:mov %rax,(%rsp) 0x000000000044fce2 <+82>:movq $0xffffffffffffffff,0x8(%rsp) 0x000000000044fceb <+91>:callq 0x44f8f0 <sync.(*WaitGroup).Add> 0x000000000044fcf0 <+96>:mov 0x18(%rsp),%rbp 0x000000000044fcf5 <+101>:add $0x20,%rsp 0x000000000044fcf9 <+105>:retq 0x000000000044fcfa <+106>:callq 0x447550 <runtime.morestack_noctxt> 0x000000000044fcff <+111>:jmp 0x44fc90 <main.start>
可以看到當前正在執行的函式是main.start而不是runtime.Gosched,在整個start函式中都找不到Gosched函式的身影,原來它被編譯器優化了。程式現在停在了0x000000000044fcb8 <+40>: lea 0x241e1(%rip),%rax 這一指令處,該指令下面的第二條callq指令在呼叫runtime.mcall,我們首先使用si 2來執行2條彙編指令讓程式停在下面這條指令處:
=> 0x000000000044fcc3 <+51>: callq 0x447380 <runtime.mcall>
然後使用i r rsp rbp rip記錄一下CPU的rsp、rbp和rip暫存器的值備用:
(gdb) i r rsprbprip rsp 0xc000031fb0 0xc000031fb0 rbp 0xc000031fc8 0xc000031fc8 rip 0x44fcc3 0x44fcc3 <main.start+51>
繼續看0x000000000044fcc3位置的callq指令,它首先會把緊挨其後的下一條指令的地址0x000000000044fcc8放入g2的棧,然後跳轉到mcall函式的第一條指令開始執行。回憶一下第二章我們詳細分析過的mcall函式的執行流程,結合現在這個場景,mcall將依次完成下面幾件事:
-
把上面call指令壓棧的返回地址0x000000000044fcc8取出來儲存在g2的sched.pc欄位,把上面我們檢視到的rsp(0xc000031fb0)和rbp(0xc000031fc8)分別儲存在g2的sched.sp和sched.bp欄位,這幾個暫存器代表了g2的排程現場資訊;
-
把儲存在g0的sched.sp和sched.bp欄位中的值分別恢復到CPU的rsp和rbp暫存器,這樣完成從g2的棧到g0的棧的切換;
-
在g0棧執行gosched_m函式(gosched_m函式是runtime.Gosched函式呼叫mcall時傳遞給mcall的引數)。
繼續看gosched_m函式
runtime/proc.go : 2623
// Gosched continuation on g0. func gosched_m(gp *g) { if trace.enabled { //traceback 不關注 traceGoSched() } goschedImpl(gp) //我們這個場景:gp = g2 }
gosched_m函式只是簡單的在呼叫goschedImpl:
runtime/proc.go : 2608
func goschedImpl(gp *g) { ...... casgstatus(gp, _Grunning, _Grunnable) dropg() //設定當前m.curg = nil, gp.m = nil lock(&sched.lock) globrunqput(gp) //把gp放入sched的全域性執行佇列runq unlock(&sched.lock) schedule() //進入新一輪排程 }
goschedImpl函式有一個g指標型別的形參,我們這個場景傳遞給它的實參是g2,goschedImpl函式首先把g2的狀態從_Grunning設定為_Grunnable,並通過dropg函式解除當前工作執行緒m和g2之間的關係(把m.curg設定成nil,把g2.m設定成nil),然後通過呼叫我們已經分析過的globrunqput函式把g2放入全域性執行佇列之中。
g2被掛入全域性執行佇列之後,g2以及其它一些相關部分的狀態和關係如下圖所示:
從上圖我們可以清晰的看到,g2被掛在了sched的全域性執行佇列裡面,該佇列有一個head頭指標指向佇列中的第一個g物件,還有一個tail尾指標指向佇列中的最後一個g物件,佇列中各個g物件通過g的schedlink指標成員相互連結起在一起;g2的sched結構體成員中儲存了排程所需的所有現場資訊(比如棧暫存器sp和bp的值,pc指令暫存器的值等等),這樣當g2下次被schedule函式排程時,gogo函式會負責把這些資訊恢復到CPU的rsp, rbp和rip暫存器中,從而使g2又得以從0x44fcc8地址處開始在g2的棧中執行g2的程式碼。
把g2掛入全域性執行佇列之後,goschedImpl函式繼續呼叫schedule()進入下一輪排程迴圈,至此g2通過自己主動呼叫Gosched()函式自願放棄了執行權,達到了排程的目的。