Go語言調度器之主動調度(20)

本文是《Go語言調度器源代碼情景分析》系列的第20篇,也是第五章《主動調度》的第1小節。linux


 

Goroutine的主動調度是指當前正在運行的goroutine經過直接調用runtime.Gosched()函數暫時放棄運行而發生的調度sass

主動調度徹底是用戶代碼本身控制的,咱們根據代碼就能夠預見什麼地方必定會發生調度。好比下面的程序,在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()開始分析。spa

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寄存器的值備用:rest

(gdb) i r rsprbprip
rsp   0xc000031fb0     0xc000031fb0
rbp   0xc000031fc8     0xc000031fc8
rip    0x44fcc3             0x44fcc3 <main.start+51>

繼續看0x000000000044fcc3位置的callq指令,它首先會把緊挨其後的下一條指令的地址0x000000000044fcc8放入g2的棧,而後跳轉到mcall函數的第一條指令開始執行。回憶一下第二章咱們詳細分析過的mcall函數的執行流程,結合如今這個場景,mcall將依次完成下面幾件事:code

  1. 把上面call指令壓棧的返回地址0x000000000044fcc8取出來保存在g2的sched.pc字段,把上面咱們查看到的rsp(0xc000031fb0)和rbp(0xc000031fc8)分別保存在g2的sched.sp和sched.bp字段,這幾個寄存器表明了g2的調度現場信息;

  2. 把保存在g0的sched.sp和sched.bp字段中的值分別恢復到CPU的rsp和rbp寄存器,這樣完成從g2的棧到g0的棧的切換;

  3. 在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()函數自願放棄了執行權,達到了調度的目的。

相關文章
相關標籤/搜索