Go調(diào)度源碼淺析

前一篇文章大致介紹了Go語言調(diào)度的各個方面至耻,這篇文章通過介紹源碼來進一步了解調(diào)度的一些過程悉稠。源碼是基于最新的Go 1.12疮装。

Go的編譯方式是靜態(tài)編譯托酸,把runtime本身直接編譯到了最終的可執(zhí)行文件里。

入口是系統(tǒng)和平臺架構(gòu)對應(yīng)的rt0_[OS]_[arch].s(runtime文件夾下)不跟,這是一段匯編代碼颓帝,做一些初始化工作,例如初始化g,新建一個線程等购城,然后會調(diào)用runtime.rt0_go(runtime/asm_[arch].s中)吕座。

runtime.rt0_go會繼續(xù)檢查cpu信息,設(shè)置好程序運行標(biāo)志瘪板,tls(thread local storage)初始化等吴趴,設(shè)置g0與m0的相互引用,然后調(diào)用runtime.args侮攀、runtime.osinit(os_[arch].go)锣枝、runtime.schedinit(proc.go),在runtime.schedinit會調(diào)用stackinit()兰英, mallocinit()等初始化棧撇叁,內(nèi)存分配器等等。接下來調(diào)用runtime.newproc(proc.go)創(chuàng)建新的goroutine用于執(zhí)行runtime.main進而綁定用戶寫的main方法畦贸。runtime.mstart(proc.go)啟動m0開始goroutine的調(diào)度(也就是執(zhí)行main函數(shù)的線程就是m0陨闹?)。

// The bootstrap sequence is:
//
//  call osinit
//  call schedinit
//  make & queue new G
//  call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
    // raceinit must be the first call to race detector.
    // In particular, it must be done before mallocinit below calls racemapshadow.
    _g_ := getg()
    if raceenabled {
        _g_.racectx, raceprocctx0 = raceinit()
    }

    sched.maxmcount = 10000

    tracebackinit()
    moduledataverify()
    stackinit()
    mallocinit()
    mcommoninit(_g_.m)
    cpuinit()       // must run before alginit
    alginit()       // maps must not be used before this call
    modulesinit()   // provides activeModules
    typelinksinit() // uses maps, activeModules
    itabsinit()     // uses activeModules

有些文章會提到m0g0家制。上文提到的匯編中新建的第一個線程就是m0正林,它在全局變量中, 無需再heap上分配颤殴,是一個脫離go本身內(nèi)存分配機制的存在觅廓。而m0中的g0也是全局變量,上面提到的runtime.rt0_go中設(shè)置了很多g0的各個成員變量涵但。但同時每個之后創(chuàng)建的m也都有自己的g0杈绸,負(fù)責(zé)調(diào)度而不是執(zhí)行用戶程序里面的函數(shù)。

runtime.main

上文講到創(chuàng)建的goroutine會執(zhí)行runtime.main進而執(zhí)行main.main從而開啟用戶寫的程序部分的運行矮瘟。

這個函數(shù)在proc.go中:

// The main goroutine.
func main() {
    g := getg()

    // Racectx of m0->g0 is used only as the parent of the main goroutine.
    // It must not be used for anything else.
    g.m.g0.racectx = 0

    // Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
    // Using decimal instead of binary GB and MB because
    // they look nicer in the stack overflow failure message.
    if sys.PtrSize == 8 {
        maxstacksize = 1000000000
    } else {
        maxstacksize = 250000000
    }

    // Allow newproc to start new Ms.
    mainStarted = true

這個函數(shù)會標(biāo)記mainStarted從而表示newproc能創(chuàng)建新的M了瞳脓,創(chuàng)建新的M來啟動sysmon函數(shù)(gc相關(guān),g搶占調(diào)度相關(guān))澈侠,調(diào)用runtime_init劫侧,gcenable等,如果是作為c的類庫編譯哨啃,這時就退出了烧栋。作為go程序,就繼續(xù)執(zhí)行main.main函數(shù)拳球,這就是用戶自己定義的程序了审姓。等用戶寫的程序執(zhí)行完,如果發(fā)生了panic則等待panic處理祝峻,最后exit(0)退出魔吐。

runtime.newproc (G的創(chuàng)建)

runtime.newproc函數(shù)本身比較簡單扎筒,傳入兩個參數(shù),其中siz是funcval+額外參數(shù)的長度酬姆,fn是指向函數(shù)機器代碼的指針嗜桌。過程只是獲取參數(shù)的起始地址和調(diào)用段返回地址的pc寄存器。然后通過systemstack調(diào)用newproc1來實現(xiàn)G的創(chuàng)建和入隊轴踱。

func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    gp := getg()
    pc := getcallerpc()
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, gp, pc)
    })
}

systemstack會切換當(dāng)前的g到g0(每個m里專門用于調(diào)度的g)症脂,然后調(diào)用newproc1谚赎。

func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
    _g_ := getg()

    if fn == nil {
        _g_.m.throwing = -1 // do not dump full stacks
        throw("go of nil func value")
    }
    _g_.m.locks++ // disable preemption because it can be holding p in a local var
    siz := narg
    siz = (siz + 7) &^ 7
        ...

runtime.newproc1做的事情大概包括:

  • 獲取當(dāng)前的G(也就是G0)淫僻,并使綁定的M不可搶占,獲取M對應(yīng)的P
  • 獲群健(或新建)一個G:
    • 通過gfget從P的gfree鏈表里獲取G
    • 獲取不到則調(diào)用malg分配一個G雳灵,初始棧2K,設(shè)置G的狀態(tài)為_Gdead闸盔,這樣gc不會掃描這個G悯辙。然后把G放入全局的G隊列里
  • 參數(shù)和返回地址復(fù)制到G的棧上
  • 設(shè)置G的調(diào)度信息(sched)
  • 設(shè)置G的狀態(tài)為_Grunnable
  • 調(diào)用runqput把G放入隊列等待運行:
    • 嘗試把G放到P的runnext
    • 嘗試把G放到P的runq(本地運行隊列)
    • 如果P的runq滿了則調(diào)用runqputslow把G放入全局隊列sched中(本地隊列的一半G放入,而不是一次放一個)
  • 檢查:如果無自旋的M但是有空閑的P迎吵,則喚醒或新建一個M躲撰。這本身跟創(chuàng)建G已經(jīng)無關(guān)了,主要是保證有足夠的M來運行G击费。
    • 喚醒或新建M通過wakeup函數(shù)
  • 釋放不可搶占狀態(tài)

runtime.mstart (M對G的執(zhí)行)

M調(diào)用的的函數(shù)拢蛋。m0在初始化后調(diào)用,其他m在線程啟動時調(diào)用蔫巩。
函數(shù)在proc.go中谆棱,處理大致如下:

  • 調(diào)用getg獲取當(dāng)前的G,會得到g0
  • 如果g未分配椩沧校空間垃瞧,從系統(tǒng)棧空間分配
  • 調(diào)用mstart1
    • 檢查坪郭,g不是g0就報錯
    • 調(diào)用save保存當(dāng)前狀態(tài)个从,以后每次調(diào)度從這個棧地址開始
    • 執(zhí)行asminit,minit歪沃,設(shè)置當(dāng)前線程可以接收的信號
    • 調(diào)用schedule函數(shù)嗦锐,開始調(diào)度。
  • schedule是調(diào)度的核心
    • 獲取g绸罗,檢查是否lock意推,是則報錯
    • 如果m被某個g鎖住(locked to a g)珊蟀,則等待那個g能執(zhí)行
    • 如果是cgo菊值,也報錯
    • 然后才進入主要的循環(huán):
      • 如果gc需要stw(stop the world)外驱,那么用stopm休眠當(dāng)前的m
      • m的p指定了需要在安全點運行的函數(shù),就運行它
      • 獲取特定的幾種g腻窒,一旦獲取到昵宇,就跳過獲取階段了:
        • 有trace(參考go tool trace)相關(guān)的g,執(zhí)行
        • gc標(biāo)記階段儿子,有待運行的gc worker(也是一個g)瓦哎,執(zhí)行
        • 每61次調(diào)度,從全局g隊列中獲取g柔逼。主要是為公平起見蒋譬,防止全局g一直不執(zhí)行
        • 從p本地獲取,調(diào)用runqget
      • 沒有獲取到愉适,則調(diào)用findrunnable獲取
        • 檢查gc的stw犯助,安全點運行函數(shù)
        • 有finalizer相關(guān)的g,運行
        • 從q的本地隊列中取维咸, runqget
        • 從全局隊列中取剂买,globrunqget,需要鎖
        • 用netpoll獲取可運行的g(見下面netpoll相關(guān)說明)癌蓖,這一步非必須瞬哼,可以跳過
        • 還是沒獲取到的話, 檢查有沒有其他p有g(shù)(查看npidle)租副;檢查自旋的M和忙碌的P的數(shù)量(為啥代碼里乘以2坐慰?),如果M多則當(dāng)前M可以停了附井;設(shè)置當(dāng)前M為自旋狀態(tài)讨越,然后隨機從其他p偷一半g過來(work steal算法)
        • 上面的異常分支或者最終沒有偷到g,都會導(dǎo)致m進入休眠(findrunnable的stop部分)永毅,休眠步驟是:
          • 如果在gc標(biāo)記把跨,看有沒有g(shù)c worker,運行沼死。有trace相關(guān)着逐,也要處理
          • gc需要stw,或者p有安全運行點函數(shù)意蛀,重新跳到findrunnable的開始執(zhí)行
          • 再次檢查全局隊列是否有G耸别,有則獲取并返回
          • 釋放P,P的狀態(tài)變?yōu)開Pidle县钥。P被添加到空閑列表
          • 讓M離開自旋狀態(tài)秀姐,然后再次找所有P的本地隊列,GC worker等若贮,找到就跳到findrunnable頂部重新執(zhí)行
          • 最終獲取不到G省有,則休眠當(dāng)前的M痒留,調(diào)用的是stopm
          • 如果之后被喚醒,跳到findrunnable頂部重新執(zhí)行
      • 繼續(xù)執(zhí)行則表示找到了帶運行的G
      • 如果M在自旋蠢沿,讓M離開自旋狀態(tài)伸头,resetspinning
      • 如果找到的G要求回到指定的M運行(lockedm != 0,例如runtime.main)
        • 調(diào)用startlockedm把G和P交給那個M舷蟀,自己進入休眠
        • 自己從休眠中醒過來的時候恤磷,跳到schedule的主循環(huán)頭部,執(zhí)行
      • 調(diào)用execute函數(shù)執(zhí)行G(這塊我寫簡單點野宜,因為主要是G本身的設(shè)置)
        • 獲取當(dāng)前G扫步,設(shè)置狀態(tài)從Grunnable到Grunning
        • 增加對應(yīng)的P中記錄的調(diào)用次數(shù)(為了61倍數(shù)次的時候從全局隊列取)
        • 對應(yīng)g和m
        • 調(diào)用gogo(匯編)函數(shù),這個函數(shù)根據(jù)g.sched中保存的狀態(tài)恢復(fù)各個寄存器中的值并開始(對應(yīng)g剛創(chuàng)建)或繼續(xù)(對應(yīng)g中斷之后又執(zhí)行)運行g(shù)速缨。設(shè)置寄存器的狀態(tài)锌妻,然后函數(shù)執(zhí)行完返回的時候調(diào)用goexit(因為newproc1中設(shè)置了返回為goexit)代乃。
        • goexit本身的調(diào)用鏈?zhǔn)牵篻oexit(匯編)-> goexit1(proc.go)-> mcall(匯編)-> goexit0(proc.go)旬牲。而mcall會保存運行狀態(tài)到g.sched,然后切換到g0搁吓,再調(diào)用goexit0原茅。
        • goexit0會把G的狀態(tài)從Grunning設(shè)置為Gdead,清理G的各個成員堕仔,解除M和G的關(guān)系并把G放到P的自由列表(GFree)中方便下次復(fù)用擂橘,最后調(diào)用schedule函數(shù),讓M繼續(xù)運行其他待運行的G

M的小結(jié)

上面的過程摩骨,是最基本的創(chuàng)建G和創(chuàng)建M的過程通贞。其中可以看到M的創(chuàng)建或喚醒主要包含在3個地方:

  • runtime.newproc1的最后,入隊G之后恼五,如果無自旋轉(zhuǎn)的M但有空閑的P昌罩,則喚醒或創(chuàng)建一個M(wakep)
  • M獲取到G,離開自旋狀態(tài)的時候(在schedule中)灾馒,如果當(dāng)前無自旋的M但有空閑的P茎用,就喚醒或創(chuàng)建一個M(wakep)
  • M取不到待執(zhí)行的G的時候,離開自旋狀態(tài)準(zhǔn)備休眠時(在findrunnable的stop部分)睬罗,再次檢查有沒有可運行的G轨功,有則重新進入findrunnable(從而再次進入自旋狀態(tài))
  • channel喚醒G的時候,無自旋M有空閑P容达,則喚醒或創(chuàng)建M

wakep函數(shù)也位于proc.go中:

func wakep() {
    // be conservative about spinning threads
    if !atomic.Cas(&sched.nmspinning, 0, 1) {
        return
    }
    startm(nil, true)
}
  • 原子交換nmspinning為1古涧,保證多個線程執(zhí)行wakep只有一個成功
  • 調(diào)用startm:
    • 從空閑列表獲取P,沒有則結(jié)束
    • 從空閑列表獲取M(mget)花盐,沒有則調(diào)用newm創(chuàng)建羡滑。newm調(diào)用allocm創(chuàng)建M圆米,會包含g0,然后調(diào)用newm1進而調(diào)用newosproc創(chuàng)建線程(天書般的代碼)
    • 調(diào)用notewakeup喚醒線程

G的小結(jié)

上面說了G從創(chuàng)建啄栓,到退出的過程娄帖。然而實際執(zhí)行的時候, 并不是這樣“一帆風(fēng)順”的昙楚。有很多情況會導(dǎo)致G在執(zhí)行過程中“中斷”近速。下面會大致介紹這些情況,但并不具體展開(因為代碼實在太多堪旧,每個都可以單獨形成一篇文章了)削葱。

搶占

每個M并不是執(zhí)行一個G到完成再執(zhí)行下一個,而是可能發(fā)生搶占淳梦。但是又不像操作系統(tǒng)的線程有時間片的概念析砸。搶占由sysmon(runtime.main里面創(chuàng)建的)觸發(fā),調(diào)用的是retake函數(shù)爆袍,這里不再詳細(xì)按代碼說明首繁,只說個大概:

  • 對于每個P,如果P在系統(tǒng)調(diào)用Psyscall且超過一次sysmon循環(huán)陨囊,搶占這個P弦疮,解除M和P的關(guān)系(handoffp)
  • 對于每個P,如果P在運行Prunning蜘醋,且超過一次sysmon循環(huán)且G的運行時間超過了一定值胁塞,搶占這個P,設(shè)置g.stackguard0為stackPreempt压语。這個值會在G調(diào)用函數(shù)的時候觸發(fā)morestack啸罢,然后經(jīng)過一系列復(fù)雜的檢查,再調(diào)用gopreempt_m完成搶占胎食。

gopreempt_m調(diào)用goschedImpl:

  • 設(shè)置G從Grunning到Grunnable
  • 解綁G和M
  • 把G放到全局隊列
  • 調(diào)用schedule函數(shù)扰才,讓M繼續(xù)執(zhí)行

搶占可以保證一個G不會長時間運行導(dǎo)致其他G餓死。前提是這個G要調(diào)用函數(shù)斥季,因為搶占在調(diào)用函數(shù)的時候才能檢測出來训桶。

channel

channel收發(fā)時可能會“阻塞”,導(dǎo)致G從Grunning變成Gwaiting酣倾,并與M解綁舵揭,M繼續(xù)調(diào)用schedule函數(shù)。

網(wǎng)絡(luò)調(diào)用

為了效率躁锡,go的網(wǎng)絡(luò)調(diào)用采用了異步方式epoll或kqueue等午绳,當(dāng)網(wǎng)絡(luò)調(diào)用讀寫數(shù)據(jù)的時候,G也可能被“阻塞”映之,從而被調(diào)度拦焚。

補充說明

上面介紹代碼的時候蜡坊,提到了G,M赎败,P使用中用到的很多屬性秕衙,這些定義在runtime2.go中。

type g struct {
    // Stack parameters.
    // stack describes the actual stack memory: [stack.lo, stack.hi).
    // stackguard0 is the stack pointer compared in the Go stack growth prologue.
    // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
    // stackguard1 is the stack pointer compared in the C stack growth prologue.
    // It is stack.lo+StackGuard on g0 and gsignal stacks.
    // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
    stack       stack   // offset known to runtime/cgo
    stackguard0 uintptr // offset known to liblink
    stackguard1 uintptr // offset known to liblin
...
}

type m struct {
    g0      *g     // goroutine with scheduling stack
    morebuf gobuf  // gobuf arg to morestack
    divmod  uint32 // div/mod denominator for arm - known to liblin
...
}

type p struct {
    lock mutex

    id          int32
    status      uint32 // one of pidle/prunning/...
    link        puintpt
...
}

參考:

  1. https://github.com/JerryZhou/golang-doc/blob/master/Golang-Internals/Part-6.Bootstrapping.and.Memory.Allocator.Initialization.md
  2. https://studygolang.com/articles/11627
  3. http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/
  4. https://making.pusher.com/go-tool-trace/
  5. https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末僵刮,一起剝皮案震驚了整個濱河市据忘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搞糕,老刑警劉巖勇吊,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異窍仰,居然都是意外死亡汉规,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門驹吮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來针史,“玉大人,你說我怎么就攤上這事钥屈∥蛎瘢” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵篷就,是天一觀的道長。 經(jīng)常有香客問我近忙,道長竭业,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任及舍,我火速辦了婚禮未辆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锯玛。我一直安慰自己咐柜,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布攘残。 她就那樣靜靜地躺著拙友,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歼郭。 梳的紋絲不亂的頭發(fā)上遗契,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音病曾,去河邊找鬼牍蜂。 笑死漾根,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鲫竞。 我是一名探鬼主播辐怕,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼从绘!你這毒婦竟也來了秘蛇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤顶考,失蹤者是張志新(化名)和其女友劉穎赁还,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體驹沿,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡艘策,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了渊季。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朋蔫。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖却汉,靈堂內(nèi)的尸體忽然破棺而出驯妄,到底是詐尸還是另有隱情,我是刑警寧澤合砂,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布青扔,位于F島的核電站,受9級特大地震影響翩伪,放射性物質(zhì)發(fā)生泄漏微猖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一缘屹、第九天 我趴在偏房一處隱蔽的房頂上張望凛剥。 院中可真熱鬧,春花似錦轻姿、人聲如沸犁珠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽犁享。三九已至,卻和暖如春胳挎,著一層夾襖步出監(jiān)牢的瞬間饼疙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留窑眯,地道東北人屏积。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像磅甩,于是被迫代替她去往敵國和親炊林。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 閱讀Go并發(fā)編程對go語言線程模型的筆記卷要,解釋的非常到渣聚,好記性不如爛筆頭,忘記的時候回來翻一番僧叉,在此做下筆記奕枝。 G...
    WithLin閱讀 1,147評論 0 4
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評論 0 9
  • Goroutine是Go里的一種輕量級線程——協(xié)程瓶堕。相對線程隘道,協(xié)程的優(yōu)勢就在于它非常輕量級,進行上下文切換的代價非...
    witchiman閱讀 4,832評論 0 9
  • http://skoo.me/go/2013/11/29/golang-schedule?hmsr=studygo...
    baboon閱讀 2,255評論 0 3
  • 本篇文章是我對 Go 語言并發(fā)性的理解總結(jié)郎笆,適合初步了解并發(fā)谭梗,對 Go 語言的并發(fā)編程與調(diào)度器原理有興趣的讀者。 ...
    baiyi閱讀 1,235評論 0 4