真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

本篇內(nèi)容主要講解“如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度”吧!

扎賚諾爾網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)公司!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、自適應(yīng)網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)公司2013年至今到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)公司。

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

識(shí)別事故的本質(zhì),并且用一個(gè)非常簡(jiǎn)單的示例展示出來(lái),是功力的一種體現(xiàn)。那次事故的原因可以簡(jiǎn)化成如下的 demo:

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

demo-1

我來(lái)簡(jiǎn)單解釋一下上面這個(gè)程序。在主 goroutine 里,先用 GoMAXPROCS 函數(shù)拿到 CPU 的邏輯核心數(shù) threads。這意味著 Go  進(jìn)程會(huì)創(chuàng)建 threads 個(gè)數(shù)的 P。接著,啟動(dòng)了 threads 個(gè)數(shù)的 goroutine,每個(gè) goroutine  都在執(zhí)行一個(gè)無(wú)限循環(huán),并且這個(gè)無(wú)限循環(huán)只是簡(jiǎn)單地執(zhí)行 x++。

接著,主 goroutine sleep 了 1 秒鐘;最后,打印 x 的值。

你可以自己思考一下,輸出會(huì)是什么?

如果你想出了答案,接著再看下面這個(gè) demo:

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

demo-2

我也來(lái)解釋一下,在主 goroutine 里,只啟動(dòng)了一個(gè) goroutine(雖然程序里用了一個(gè) for 循環(huán),但其實(shí)只循環(huán)了一次,完全是為了和前面的  demo 看起來(lái)更協(xié)調(diào)一些),同樣執(zhí)行了一個(gè) x++ 的無(wú)限 for 循環(huán)。

和前一個(gè) demo 的不同點(diǎn)在于,在主 goroutine 里,我們手動(dòng)執(zhí)行了一次 GC;最后,打印 x 的值。

如果你能答對(duì)第一題,大概率也能答對(duì)第二題。

下面我就來(lái)揭曉答案。

其實(shí)我留了一個(gè)坑,我沒說用哪個(gè)版本的 Go 來(lái)運(yùn)行代碼。所以,正確的答案是:

Go 版本demo-1demo-2
1.13卡死卡死
1.1400

這個(gè)其實(shí)就是 Go 調(diào)度器的坑了。

假設(shè)在 demo-1 中,共有 4 個(gè) P,于是創(chuàng)建了 4 個(gè) goroutine。當(dāng)主 goroutine 執(zhí)行 sleep 的時(shí)候,剛剛創(chuàng)建的 4 個(gè)  goroutine 馬上就把 4 個(gè) P 霸占了,執(zhí)行死循環(huán),而且竟然沒有進(jìn)行函數(shù)調(diào)用,就只有一個(gè)簡(jiǎn)單的賦值語(yǔ)句。Go 1.13  對(duì)這種情況是無(wú)能為力的,沒有任何辦法讓這些 goroutine 停下來(lái),進(jìn)程對(duì)外表現(xiàn)出“死機(jī)”。

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

demo-1 示意圖

由于 Go 1.14 實(shí)現(xiàn)了基于信號(hào)的搶占式調(diào)度,這些執(zhí)行無(wú)限循環(huán)的 goroutine 會(huì)被調(diào)度器“拿下”,P 就會(huì)空出來(lái)。所以當(dāng)主 goroutine  sleep 時(shí)間到了之后,馬上就能獲得 P,并得以打印出 x 的值。至于 x 為什么輸出的是  0,不太好解釋,因?yàn)檫@是一種未定義(有數(shù)據(jù)競(jìng)爭(zhēng),正常情況下要加鎖)的行為,可能的一個(gè)原因是 CPU 的 cache 沒有來(lái)得及更新,不過不太好驗(yàn)證。

理解了這個(gè) demo,第二個(gè) demo 其實(shí)是類似的道理:

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

demo-2 示意圖

當(dāng)主 goroutine 主動(dòng)觸發(fā) GC 時(shí),需要把所有當(dāng)前正在運(yùn)行的 goroutine 停止下來(lái),即 stw(stop the world),但是  goroutine 正在執(zhí)行無(wú)限循環(huán),沒法讓它停下來(lái)。當(dāng)然,Go 1.14 還是可以搶占掉這個(gè) goroutine,從而打印出 x 的值,也是 0。

Go 1.14 之前的版本,能否搶占一個(gè)正在執(zhí)行死循環(huán)的 goroutine 其實(shí)是有講究的:

能否被搶占,不是看有沒有調(diào)用函數(shù),而是看函數(shù)的序言部分有沒有插入擴(kuò)棧檢測(cè)指令。

如果沒有調(diào)用函數(shù),肯定不會(huì)被搶占。

有些雖然也調(diào)用了函數(shù),但其實(shí)不會(huì)插入檢測(cè)指令,這個(gè)時(shí)候也不會(huì)被搶占。

像前面的兩個(gè) demo,不可能有機(jī)會(huì)在函數(shù)擴(kuò)棧檢測(cè)期間主動(dòng)放棄 CPU  使用權(quán),從而完成搶占,因?yàn)闆]有函數(shù)調(diào)用。具體的過程后面有機(jī)會(huì)再寫一篇文章詳細(xì)講,本文主要看基于信號(hào)的搶占式調(diào)度如何實(shí)現(xiàn)。

preemptone

一方面,Go 進(jìn)程在啟動(dòng)的時(shí)候,會(huì)開啟一個(gè)后臺(tái)線程 sysmon,監(jiān)控執(zhí)行時(shí)間過長(zhǎng)的 goroutine,進(jìn)而發(fā)出搶占。另一方面,GC 執(zhí)行 stw  時(shí),會(huì)讓所有的 goroutine 都停止,其實(shí)就是搶占。這兩者都會(huì)調(diào)用 preemptone() 函數(shù)。

preemptone() 函數(shù)會(huì)沿著下面這條路徑:

preemptone->preemptM->signalM->tgkill

向正在運(yùn)行的 goroutine 所綁定的的那個(gè) M(也可以說是線程)發(fā)出 SIGURG 信號(hào)。

注冊(cè) sighandler

每個(gè) M 在初始化的時(shí)候都會(huì)設(shè)置信號(hào)處理函數(shù):

initsig->setsig->sighandler

信號(hào)執(zhí)行過程

我們從“宏觀”層面看一下信號(hào)的執(zhí)行過程:

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

信號(hào)執(zhí)行過程

主程序(線程)正在“勤勤懇懇”地執(zhí)行指令:它已經(jīng)執(zhí)行完了指令 m,接著就要執(zhí)行指令 m+1 了……不幸在這個(gè)時(shí)候發(fā)生了,線程收到了一個(gè)信號(hào),對(duì)應(yīng)圖中的  ①。

接著,內(nèi)核會(huì)接管執(zhí)行流,轉(zhuǎn)而去執(zhí)行預(yù)先設(shè)置好的信號(hào)處理器程序,對(duì)應(yīng)到 Go 里,就是執(zhí)行 sighandler,對(duì)應(yīng)圖中的 ② 和 ③。

最后,執(zhí)行流又交到線程手上,繼續(xù)執(zhí)行指令 m+1,對(duì)應(yīng)圖中的 ④。

這里其實(shí)涉及到了一些現(xiàn)場(chǎng)的保護(hù)和恢復(fù),內(nèi)核都幫我們搞定了,我們不用操心。

dosigPreempt

當(dāng)線程收到 SIGURG 信號(hào)的時(shí)候,就會(huì)去執(zhí)行 sighandler 函數(shù),核心是 doSigPreempt 函數(shù)。

func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {     ...          if sig == sigPreempt && debug.asyncpreemptoff == 0 {   doSigPreempt(gp, c)  }    ... }

doSigPreempt 這個(gè)函數(shù)其實(shí)很短,一會(huì)兒就執(zhí)行完了。

func doSigPreempt(gp *g, ctxt *sigctxt) {  ...  if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {   // Adjust the PC and inject a call to asyncPreempt.   ctxt.pushCall(funcPC(asyncPreempt), newpc)  }  ... }

isAsyncSafePoint 函數(shù)會(huì)返回當(dāng)前 goroutine 能否被搶占,以及從哪條指令開始搶占,返回的 newpc 表示安全的搶占地址。

接著,pushCall 調(diào)整了一下 SP,設(shè)置了幾個(gè)寄存器的值就返回了。按理說,返回之后,就會(huì)接著執(zhí)行指令 m+1 了,但那還怎么實(shí)現(xiàn)搶占呢?其實(shí)魔法都在  pushCall 這個(gè)函數(shù)里。

pushCall

在分析這個(gè)函數(shù)之前,我們需要先復(fù)習(xí)一下 Go 函數(shù)的調(diào)用規(guī)約,重點(diǎn)回顧一下 CALL 和 RET 指令就行了。

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

call 和 ret 指令

call 指令可以簡(jiǎn)單地理解為 push ip + JMP。這個(gè) ip 其實(shí)就是返回地址,也就是調(diào)用完子函數(shù)接下來(lái)該執(zhí)行啥指令的地址。所以 push ip  就是在 call 一個(gè)子函數(shù)之前,將返回地址壓入棧中,然后 JMP 到子函數(shù)的地址執(zhí)行。

ret 指令和 call 指令剛好相反,它將返回地址從棧上 pop 到 IP 寄存器,使得 CPU 從這個(gè)地址繼續(xù)執(zhí)行。

理解了 call 和 ret,我們?cè)賮?lái)分析 pushCall 函數(shù):

func (c *sigctxt) pushCall(targetPC, resumePC uintptr) {  // Make it look like we called target at resumePC.  sp := uintptr(c.rsp())  sp -= sys.PtrSize  *(*uintptr)(unsafe.Pointer(sp)) = resumePC  c.set_rsp(uint64(sp))  c.set_rip(uint64(targetPC)) }

注意看這行注釋:

// Make it look like we called target at resumePC.

它清晰地說明了這個(gè)函數(shù)的作用:讓 CPU 誤以為是 resumePC 調(diào)用了 targetPC。而這個(gè) resumePC 就是上一步調(diào)用  isAsyncSafePoint 函數(shù)返回的 newpc,它代表我們搶占 goroutine 的指令地址。

前兩行代碼將 SP 下移了 8 個(gè)字節(jié),并且把 resumePC 入棧(注意,它其實(shí)是一個(gè)返回地址),接著把 targetPC 設(shè)置到 ip 寄存器,sp  設(shè)置到 SP 寄存器。這使得從內(nèi)核返回到用戶態(tài)執(zhí)行時(shí),不是從指令 m+1,而是直接從 targetPC 開始執(zhí)行,等到 targetPC 執(zhí)行完,才會(huì)返回到  resumePC 繼續(xù)執(zhí)行。整個(gè)過程就像是 resumePC 調(diào)用了 targetPC 一樣。而 targetPC 其實(shí)就是  funcPC(asyncPreempt),也就是搶占函數(shù)。

于是我們可以看到,信號(hào)處理器程序 sighandler 只是將一個(gè)異步搶占函數(shù)給“安插”進(jìn)來(lái)了,而真正的搶占過程則是在 asyncPreempt  函數(shù)中完成。

異步搶占

當(dāng)執(zhí)行完 sighandler,執(zhí)行流再次回到線程。由于 sighandler 插入了一個(gè) asyncPreempt 的函數(shù)調(diào)用,所以 goroutine  原本的任務(wù)就得不到推進(jìn),轉(zhuǎn)而執(zhí)行 asyncPreempt 去了:

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

asyncPreempt 調(diào)用鏈路

mcall(fn) 的作用是切到 g0 棧去執(zhí)行函數(shù) fn, fn 永不返回。在 mcall(gopreempt_m) 這里,fn 就是  gopreempt_m。

gopreempt_m 直接調(diào)用 goschedImpl:

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

goschedImpl

如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度

dropg

最精彩的部分就在 goschedImpl 函數(shù)。它首先將 goroutine 的狀態(tài)從 running 改成 runnable;接著調(diào) dropg 將 g  和 m 解綁;然后調(diào)用 globrunqput 將 goroutine 丟到全局可運(yùn)行隊(duì)列,由于是全局可運(yùn)行隊(duì)列,所以需要加鎖。最后,調(diào)用 schedule()  函數(shù)進(jìn)入調(diào)度循環(huán)。關(guān)于調(diào)度循環(huán),可以看這篇文章。

運(yùn)行 schedule 函數(shù)用的是 g0 棧,它會(huì)去尋找其他可運(yùn)行的 goroutine,包括從當(dāng)前 P 本地可運(yùn)行隊(duì)列獲取、從全局可運(yùn)行隊(duì)列獲取、從其他  P 偷等方式找到下一個(gè)可運(yùn)行的 goroutine 并執(zhí)行。

至此,這個(gè)線程就轉(zhuǎn)而去執(zhí)行其他的 goroutine,當(dāng)前的 goroutine 也就被搶占了。

那被搶占的這個(gè) goroutine 什么時(shí)候會(huì)再次得到執(zhí)行呢?

因?yàn)樗呀?jīng)被丟到全局可運(yùn)行隊(duì)列了,所以它的優(yōu)先級(jí)就會(huì)降低,得到調(diào)度的機(jī)會(huì)也就降低,但總還是有機(jī)會(huì)再次執(zhí)行的,并且它會(huì)從調(diào)用 mcall  的下一條指令接著執(zhí)行。

還記得 mcall 函數(shù)的作用嗎?它會(huì)切到 g0 棧執(zhí)行 gopreempt_m,自然它也會(huì)保存 goroutine 的執(zhí)行進(jìn)度,其實(shí)就是  SP、BP、PC 寄存器的值,當(dāng) goroutine 再次被調(diào)度執(zhí)行時(shí),就會(huì)從原來(lái)的執(zhí)行流斷點(diǎn)處繼續(xù)執(zhí)行下去。

到此,相信大家對(duì)“如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!


網(wǎng)站欄目:如何理解Go語(yǔ)言基于信號(hào)的搶占式調(diào)度
文章鏈接:http://weahome.cn/article/geppig.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部