免费观看又色又爽又黄的小说免费_美女福利视频国产片_亚洲欧美精品_美国一级大黄大色毛片

分析Go協(xié)作與搶占

這篇文章主要介紹“分析Go協(xié)作與搶占”,在日常操作中,相信很多人在分析Go協(xié)作與搶占問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”分析Go協(xié)作與搶占”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

成都創(chuàng)新互聯(lián)-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比柳河網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式柳河網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋柳河地區(qū)。費(fèi)用合理售后完善,10余年實(shí)體公司更值得信賴。

協(xié)作式調(diào)度

主動(dòng)用戶讓權(quán):Gosched

Gosched 是一種主動(dòng)放棄執(zhí)行的手段,用戶態(tài)代碼通過調(diào)用此接口來出讓執(zhí)行機(jī)會(huì),使其他“人”也能在密集的執(zhí)行過程中獲得被調(diào)度的機(jī)會(huì)。

Gosched 的實(shí)現(xiàn)非常簡單:

// Gosched 會(huì)讓出當(dāng)前的 P,并允許其他 Goroutine 運(yùn)行。 // 它不會(huì)推遲當(dāng)前的 Goroutine,因此執(zhí)行會(huì)被自動(dòng)恢復(fù) func Gosched() {   checkTimeouts()   mcall(gosched_m) }  // Gosched 在 g0 上繼續(xù)執(zhí)行 func gosched_m(gp *g) {   ...   goschedImpl(gp) }

它首先會(huì)通過 note 機(jī)制通知那些等待被 ready 的 Goroutine:

// checkTimeouts 恢復(fù)那些在等待一個(gè) note 且已經(jīng)觸發(fā)其 deadline 時(shí)的 Goroutine。 func checkTimeouts() {   now := nanotime()   for n, nt := range notesWithTimeout {     if n.key == note_cleared && now > nt.deadline {       n.key = note_timeout       goready(nt.gp, 1)     }   } }  func goready(gp *g, traceskip int) {   systemstack(func() {     ready(gp, traceskip, true)   }) }  // 將 gp 標(biāo)記為 ready 來運(yùn)行 func ready(gp *g, traceskip int, next bool) {   if trace.enabled {     traceGoUnpark(gp, traceskip)   }    status := readgstatus(gp)    // 標(biāo)記為 runnable.   _g_ := getg()   _g_.m.locks++ // 禁止搶占,因?yàn)樗梢栽诰植孔兞恐斜4?nbsp;p   if status&^_Gscan != _Gwaiting {     dumpgstatus(gp)     throw("bad g->status in ready")   }    // 狀態(tài)為 Gwaiting 或 Gscanwaiting, 標(biāo)記 Grunnable 并將其放入運(yùn)行隊(duì)列 runq   casgstatus(gp, _Gwaiting, _Grunnable)   runqput(_g_.m.p.ptr(), gp, next)   if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 {     wakep()   }   _g_.m.locks--   if _g_.m.locks == 0 && _g_.preempt { // 在 newstack 中已經(jīng)清除它的情況下恢復(fù)搶占請(qǐng)求     _g_.stackguard0 = stackPreempt   } }  func notetsleepg(n *note, ns int64) bool {   gp := getg()   ...    if ns >= 0 {     deadline := nanotime() + ns     ...     notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: deadline}     ...     gopark(nil, nil, waitReasonSleep, traceEvNone, 1)     ...     delete(notesWithTimeout, n)     ...   }    ... }

而后通過 mcall 調(diào)用 gosched_m 在 g0 上繼續(xù)執(zhí)行并讓出 P,實(shí)質(zhì)上是讓 G 放棄當(dāng)前在 M 上的執(zhí)行權(quán)利,M 轉(zhuǎn)去執(zhí)行其他的  G,并在上下文切換時(shí)候,將自身放入全局隊(duì)列等待后續(xù)調(diào)度:

func goschedImpl(gp *g) {   // 放棄當(dāng)前 g 的運(yùn)行狀態(tài)   status := readgstatus(gp)   ...   casgstatus(gp, _Grunning, _Grunnable)   // 使當(dāng)前 m 放棄 g   dropg()   // 并將 g 放回全局隊(duì)列中   lock(&sched.lock)   globrunqput(gp)   unlock(&sched.lock)    // 重新進(jìn)入調(diào)度循環(huán)   schedule() }

當(dāng)然,盡管具有主動(dòng)棄權(quán)的能力,但它對(duì) Go 語言的用戶要求比較高,因?yàn)橛脩粼诰帉懖l(fā)邏輯的時(shí)候需要自行甄別是否需要讓出時(shí)間片,這并非用戶友好的,而且很多  Go 的新用戶并不會(huì)了解到這個(gè)問題的存在,我們?cè)陔S后的搶占式調(diào)度中再進(jìn)一步展開討論。

主動(dòng)調(diào)度棄權(quán):棧擴(kuò)張與搶占標(biāo)記

另一種主動(dòng)放棄的方式是通過搶占標(biāo)記的方式實(shí)現(xiàn)的。基本想法是在每個(gè)函數(shù)調(diào)用的序言(函數(shù)調(diào)用的最前方)插入搶占檢測指令,當(dāng)檢測到當(dāng)前 Goroutine  被標(biāo)記為應(yīng)該被搶占時(shí),則主動(dòng)中斷執(zhí)行,讓出執(zhí)行權(quán)利。表面上看起來想法很簡單,但實(shí)施起來就比較復(fù)雜了。

在 6.6 執(zhí)行棧管理[2] 一節(jié)中我們已經(jīng)了解到,函數(shù)調(diào)用的序言部分會(huì)檢查 SP 寄存器與 stackguard0 之間的大小,如果 SP 小于  stackguard0 則會(huì)觸發(fā) morestack_noctxt,觸發(fā)棧分段操作。換言之,如果搶占標(biāo)記將 stackgard0 設(shè)為比所有可能的 SP  都要大(即 stackPreempt),則會(huì)觸發(fā) morestack,進(jìn)而調(diào)用 newstack:

// Goroutine 搶占請(qǐng)求 // 存儲(chǔ)到 g.stackguard0 來導(dǎo)致棧分段檢查失敗 // 必須比任何實(shí)際的 SP 都要大 // 十六進(jìn)制為:0xfffffade const stackPreempt = (1<<(8*sys.PtrSize) - 1) & -1314

從搶占調(diào)度的角度來看,這種發(fā)生在函數(shù)序言部分的搶占的一個(gè)重要目的就是能夠簡單且安全的記錄執(zhí)行現(xiàn)場(隨后的搶占式調(diào)度我們會(huì)看到記錄執(zhí)行現(xiàn)場給采用信號(hào)方式中斷線程執(zhí)行的調(diào)度帶來多大的困難)。事實(shí)也是如此,在  morestack 調(diào)用中:

TEXT runtime&middot;morestack(SB),NOSPLIT,$0-0     ...     MOVQ    0(SP), AX // f's PC     MOVQ    AX, (g_sched+gobuf_pc)(SI)     MOVQ    SI, (g_sched+gobuf_g)(SI)     LEAQ    8(SP), AX // f's SP     MOVQ    AX, (g_sched+gobuf_sp)(SI)     MOVQ    BP, (g_sched+gobuf_bp)(SI)     MOVQ    DX, (g_sched+gobuf_ctxt)(SI)     ...     CALL    runtime&middot;newstack(SB)

是有記錄 Goroutine 的 PC 和 SP 寄存器,而后才開始調(diào)用 newstack 的:

//go:nowritebarrierrec func newstack() {   thisg := getg()   ...    gp := thisg.m.curg   ...    morebuf := thisg.m.morebuf   thisg.m.morebuf.pc = 0   thisg.m.morebuf.lr = 0   thisg.m.morebuf.sp = 0   thisg.m.morebuf.g = 0    // 如果是發(fā)起的搶占請(qǐng)求而非真正的棧分段   preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt    // 保守的對(duì)用戶態(tài)代碼進(jìn)行搶占,而非搶占運(yùn)行時(shí)代碼   // 如果正持有鎖、分配內(nèi)存或搶占被禁用,則不發(fā)生搶占   if preempt {     if !canPreemptM(thisg.m) {       // 不發(fā)生搶占,繼續(xù)調(diào)度       gp.stackguard0 = gp.stack.lo + _StackGuard       gogo(&gp.sched) // 重新進(jìn)入調(diào)度循環(huán)     }   }   ...   // 如果需要對(duì)棧進(jìn)行調(diào)整   if preempt {     ...     if gp.preemptShrink {       // 我們正在一個(gè)同步安全點(diǎn),因此等待棧收縮       gp.preemptShrink = false       shrinkstack(gp)     }     if gp.preemptStop {       preemptPark(gp) // 永不返回     }     ...     // 表現(xiàn)得像是調(diào)用了 runtime.Gosched,主動(dòng)讓權(quán)     gopreempt_m(gp) // 重新進(jìn)入調(diào)度循環(huán)   }   ... } // 與 gosched_m 一致 func gopreempt_m(gp *g) {   ...   goschedImpl(gp) }

其中的 canPreemptM 驗(yàn)證了可以被搶占的條件:

  1. 運(yùn)行時(shí)沒有禁止搶占(m.locks == 0)

  2. 運(yùn)行時(shí)沒有在執(zhí)行內(nèi)存分配(m.mallocing == 0)

  3. 運(yùn)行時(shí)沒有關(guān)閉搶占機(jī)制(m.preemptoff == "")

  4. M 與 P 綁定且沒有進(jìn)入系統(tǒng)調(diào)用(p.status == _Prunning)

// canPreemptM 報(bào)告 mp 是否處于可搶占的安全狀態(tài)。 //go:nosplit func canPreemptM(mp *m) bool {   return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning }

從可被搶占的條件來看,能夠?qū)σ粋€(gè) G  進(jìn)行搶占其實(shí)是呈保守狀態(tài)的。這一保守體現(xiàn)在搶占對(duì)很多運(yùn)行時(shí)所需的條件進(jìn)行了判斷,這也理所當(dāng)然是因?yàn)檫\(yùn)行時(shí)優(yōu)先級(jí)更高,不應(yīng)該輕易發(fā)生搶占,但與此同時(shí)由于又需要對(duì)用戶態(tài)代碼進(jìn)行搶占,于是先作出一次不需要搶占的判斷(快速路徑),確定不能搶占時(shí)返回并繼續(xù)調(diào)度,如果真的需要進(jìn)行搶占,則轉(zhuǎn)入調(diào)用  gopreempt_m,放棄當(dāng)前 G 的執(zhí)行權(quán),將其加入全局隊(duì)列,重新進(jìn)入調(diào)度循環(huán)。

什么時(shí)候會(huì)給 stackguard0 設(shè)置搶占標(biāo)記 stackPreempt 呢?一共有以下幾種情況:

  1. 進(jìn)入系統(tǒng)調(diào)用時(shí)(runtime.reentersyscall,注意這種情況是為了保證不會(huì)發(fā)生棧分裂,真正的搶占是異步地通過系統(tǒng)監(jiān)控進(jìn)行的)

  2. 任何運(yùn)行時(shí)不再持有鎖的時(shí)候(m.locks == 0)

  3. 當(dāng)垃圾回收器需要停止所有用戶 Goroutine 時(shí)

搶占式調(diào)度

從上面提到的兩種協(xié)作式調(diào)度邏輯我們可以看出,這種需要用戶代碼來主動(dòng)配合的調(diào)度方式存在一些致命的缺陷:一個(gè)沒有主動(dòng)放棄執(zhí)行權(quán)、且不參與任何函數(shù)調(diào)用的函數(shù),直到執(zhí)行完畢之前,是不會(huì)被搶占的。

那么這種不會(huì)被搶占的函數(shù)會(huì)導(dǎo)致什么嚴(yán)重的問題呢?回答是,由于運(yùn)行時(shí)無法停止該用戶代碼,則當(dāng)需要進(jìn)行垃圾回收時(shí),無法及時(shí)進(jìn)行;對(duì)于一些實(shí)時(shí)性要求較高的用戶態(tài)  Goroutine  而言,也久久得不到調(diào)度。我們這里不去深入討論垃圾回收的具體細(xì)節(jié),讀者將在垃圾回收器[3]一章中詳細(xì)看到這類問題導(dǎo)致的后果。單從調(diào)度的角度而言,我們直接來看一個(gè)非常簡單的例子:

// 此程序在 Go 1.14 之前的版本不會(huì)輸出 OK package main import (   "runtime"   "time" ) func main() {   runtime.GOMAXPROCS(1)   go func() {     for {     }   }()   time.Sleep(time.Millisecond)   println("OK") }

這段代碼中處于死循環(huán)的 Goroutine 永遠(yuǎn)無法被搶占,其中創(chuàng)建的 Goroutine 會(huì)執(zhí)行一個(gè)不產(chǎn)生任何調(diào)用、不主動(dòng)放棄執(zhí)行權(quán)的死循環(huán)。由于主  Goroutine 優(yōu)先調(diào)用了休眠,此時(shí)唯一的 P 會(huì)轉(zhuǎn)去執(zhí)行 for 循環(huán)所創(chuàng)建的 Goroutine。進(jìn)而主 Goroutine  永遠(yuǎn)不會(huì)再被調(diào)度,進(jìn)而程序徹底阻塞在了這個(gè) Goroutine 上,永遠(yuǎn)無法退出。這樣的例子非常多,但追根溯源,均為此問題導(dǎo)致。

Go 團(tuán)隊(duì)其實(shí)很早(1.0 以前)就已經(jīng)意識(shí)到了這個(gè)問題,但在 Go 1.2  時(shí)增加了上文提到的在函數(shù)序言部分增加搶占標(biāo)記后,此問題便被擱置,直到越來越多的用戶提交并報(bào)告此問題。在 Go 1.5 前后,Austin Clements  希望僅解決這種由密集循環(huán)導(dǎo)致的無法搶占的問題 [Clements, 2015],于是嘗試通過協(xié)作式 loop  循環(huán)搶占,通過編譯器輔助的方式,插入搶占檢查指令,與流程圖回邊(指節(jié)點(diǎn)被訪問過但其子節(jié)點(diǎn)尚未訪問完畢)安全點(diǎn)(在一個(gè)線程執(zhí)行中,垃圾回收器能夠識(shí)別所有對(duì)象引用狀態(tài)的一個(gè)狀態(tài))的方式進(jìn)行解決。

盡管此舉能為搶占帶來顯著的提升,但是在一個(gè)循環(huán)中引入分支顯然會(huì)降低性能。盡管隨后 David Chase 對(duì)這個(gè)方法進(jìn)行了改進(jìn),僅在插入了一條 TESTB  指令 [Chase, 2017],在完全沒有分支以及寄存器壓力的情況下,仍然造成了幾何平均 7.8%  的性能損失。這種結(jié)果其實(shí)是情理之中的,很多需要進(jìn)行密集循環(huán)的計(jì)算時(shí)間都是在運(yùn)行時(shí)才能確定的,直接由編譯器檢測這類密集循環(huán)而插入額外的指令可想而知是欠妥的做法。

終于在 Go 1.10 后 [Clements, 2019],Austin  進(jìn)一步提出的解決方案,希望使用每個(gè)指令與執(zhí)行棧和寄存器的映射關(guān)系,通過記錄足夠多的信息,并通過異步線程來發(fā)送搶占信號(hào)的方式來支持異步搶占式調(diào)度。

我們知道現(xiàn)代操作系統(tǒng)的調(diào)度器多為搶占式調(diào)度,其實(shí)現(xiàn)方式通過硬件中斷來支持線程的切換,進(jìn)而能安全的保存運(yùn)行上下文。在 Go  運(yùn)行時(shí)實(shí)現(xiàn)搶占式調(diào)度同樣也可以使用類似的方式,通過向線程發(fā)送系統(tǒng)信號(hào)的方式來中斷 M  的執(zhí)行,進(jìn)而達(dá)到搶占的目的。但與操作系統(tǒng)的不同之處在于,由于運(yùn)行時(shí)諸多機(jī)制的存在(例如垃圾回收器),還必須能夠在 Goroutine  被停止時(shí),保存充足的上下文信息(見 8.9  安全點(diǎn)分析[4])。這就給中斷信號(hào)帶來了麻煩,如果中斷信號(hào)恰好發(fā)生在一些關(guān)鍵階段(例如寫屏障期間),則無法保證程序的正確性。這也就要求我們需要嚴(yán)格考慮觸發(fā)異步搶占的時(shí)機(jī)。

異步搶占式調(diào)度的一種方式就與運(yùn)行時(shí)系統(tǒng)監(jiān)控有關(guān),監(jiān)控循環(huán)會(huì)將發(fā)生阻塞的 Goroutine 搶占,解綁 P 與 M,從而讓其他的線程能夠獲得 P  繼續(xù)執(zhí)行其他的 Goroutine。這得益于 sysmon中調(diào)用的 retake 方法。這個(gè)方法處理了兩種搶占情況,一是搶占阻塞在系統(tǒng)調(diào)用上的  P,二是搶占運(yùn)行時(shí)間過長的 G。其中搶占運(yùn)行時(shí)間過長的 G 這一方式還會(huì)出現(xiàn)在垃圾回收需要進(jìn)入 STW 時(shí)。

P 搶占

我們先來看搶占阻塞在系統(tǒng)調(diào)用上的 G 這種情況。這種搶占的實(shí)現(xiàn)方法非常的自然,因?yàn)?Goroutine 已經(jīng)阻塞在了系統(tǒng)調(diào)用上,我們可以非常安全的將 M  與 P 進(jìn)行解綁,即便是 Goroutine 從阻塞中恢復(fù),也會(huì)檢查自身所在的 M 是否仍然持有 P,如果沒有 P 則重新考慮與可用的 P  進(jìn)行綁定。這種異步搶占的本質(zhì)是:搶占 P。

unc retake(now int64) uint32 {   n := 0   // 防止 allp 數(shù)組發(fā)生變化,除非我們已經(jīng) STW,此鎖將完全沒有人競爭   lock(&allpLock)   for i := 0; i < len(allp); i++ {     _p_ := allp[i]     ...     pd := &_p_.sysmontick     s := _p_.status     sysretake := false     if s == _Prunning || s == _Psyscall {       // 如果 G 運(yùn)行時(shí)時(shí)間太長則進(jìn)行搶占       t := int64(_p_.schedtick)       if int64(pd.schedtick) != t {         pd.schedtick = uint32(t)         pd.schedwhen = now       } else if pd.schedwhen+forcePreemptNS <= now {         ...         sysretake = true       }     }     // 對(duì)阻塞在系統(tǒng)調(diào)用上的 P 進(jìn)行搶占     if s == _Psyscall {       // 如果已經(jīng)超過了一個(gè)系統(tǒng)監(jiān)控的 tick(20us),則從系統(tǒng)調(diào)用中搶占 P       t := int64(_p_.syscalltick)       if !sysretake && int64(pd.syscalltick) != t {         pd.syscalltick = uint32(t)         pd.syscallwhen = now         continue       }       // 一方面,在沒有其他 work 的情況下,我們不希望搶奪 P       // 另一方面,因?yàn)樗赡茏柚?nbsp;sysmon 線程從深度睡眠中喚醒,所以最終我們?nèi)韵M麚寠Z P       if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {         continue       }       // 解除 allpLock,從而可以獲取 sched.lock       unlock(&allpLock)       // 在 CAS 之前需要減少空閑 M 的數(shù)量(假裝某個(gè)還在運(yùn)行)       // 否則發(fā)生搶奪的 M 可能退出 syscall 然后再增加 nmidle ,進(jìn)而發(fā)生死鎖       // 這個(gè)過程發(fā)生在 stoplockedm 中       incidlelocked(-1)       if atomic.Cas(&_p_.status, s, _Pidle) { // 將 P 設(shè)為 idle,從而交與其他 M 使用         ...         n++         _p_.syscalltick++         handoffp(_p_)       }       incidlelocked(1)       lock(&allpLock)     }   }   unlock(&allpLock)   return uint32(n) }

在搶占 P 的過程中,有兩個(gè)非常小心的處理方式:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 如果此時(shí)隊(duì)列為空,那么完全沒有必要進(jìn)行搶占,這時(shí)候似乎可以繼續(xù)遍歷其他的 P,但必須在調(diào)度器中自旋的 M 和 空閑的 P  同時(shí)存在時(shí)、且系統(tǒng)調(diào)用阻塞時(shí)間非常長的情況下才能這么做。否則,這個(gè) retake 過程可能返回  0,進(jìn)而系統(tǒng)監(jiān)控可能看起來像是什么事情也沒做的情況下調(diào)整自己的步調(diào)進(jìn)入深度睡眠。

  3. 在將 P 設(shè)置為空閑狀態(tài)前,必須先將 M 的數(shù)量減少,否則當(dāng) M 退出系統(tǒng)調(diào)用時(shí),會(huì)在 exitsyscall0 中調(diào)用 stoplockedm  從而增加空閑 M 的數(shù)量,進(jìn)而發(fā)生死鎖。

M 搶占

在上面我們沒有展現(xiàn)一個(gè)細(xì)節(jié),那就是在檢查 P 的狀態(tài)時(shí),P  如果是運(yùn)行狀態(tài)會(huì)調(diào)用preemptone,來通過系統(tǒng)信號(hào)來完成搶占,之所以沒有在之前提及的原因在于該調(diào)用在 M 不與 P  綁定的情況下是不起任何作用直接返回的。這種異步搶占的本質(zhì)是:搶占 M。我們不妨繼續(xù)從系統(tǒng)監(jiān)控產(chǎn)生的搶占談起:

func retake(now int64) uint32 {   ...   for i := 0; i < len(allp); i++ {     _p_ := allp[i]     ...     if s == _Prunning || s == _Psyscall {       ...       } else if pd.schedwhen+forcePreemptNS <= now {         // 對(duì)于 syscall 的情況,因?yàn)?nbsp;M 沒有與 P 綁定,         // preemptone() 不工作         preemptone(_p_)         sysretake = true       }     }     ...   }   ... } func preemptone(_p_ *p) bool {   // 檢查 M 與 P 是否綁定   mp := _p_.m.ptr()   if mp == nil || mp == getg().m {     return false   }   gp := mp.curg   if gp == nil || gp == mp.g0 {     return false   }    // 將 G 標(biāo)記為搶占   gp.preempt = true    // 一個(gè) Goroutine 中的每個(gè)調(diào)用都會(huì)通過比較當(dāng)前棧指針和 gp.stackgard0   // 來檢查棧是否溢出。   // 設(shè)置 gp.stackgard0 為 StackPreempt 來將搶占轉(zhuǎn)換為正常的棧溢出檢查。   gp.stackguard0 = stackPreempt    // 請(qǐng)求該 P 的異步搶占   if preemptMSupported && debug.asyncpreemptoff == 0 {     _p_.preempt = true     preemptM(mp)   }    return true }

搶占信號(hào)的選取

preemptM 完成了信號(hào)的發(fā)送,其實(shí)現(xiàn)也非常直接,直接向需要進(jìn)行搶占的 M 發(fā)送 SIGURG 信號(hào)即可。但是真正的重要的問題是,為什么是  SIGURG 信號(hào)而不是其他的信號(hào)?如何才能保證該信號(hào)不與用戶態(tài)產(chǎn)生的信號(hào)產(chǎn)生沖突?這里面有幾個(gè)原因:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 默認(rèn)情況下,SIGURG 已經(jīng)用于調(diào)試器傳遞信號(hào)。

  3. SIGURG 可以不加選擇地虛假發(fā)生的信號(hào)。例如,我們不能選擇  SIGALRM,因?yàn)樾盘?hào)處理程序無法分辨它是否是由實(shí)際過程引起的(可以說這意味著信號(hào)已損壞)。而常見的用戶自定義信號(hào) SIGUSR1 和 SIGUSR2  也不夠好,因?yàn)橛脩魬B(tài)代碼可能會(huì)將其進(jìn)行使用。

  4. 需要處理沒有實(shí)時(shí)信號(hào)的平臺(tái)(例如 macOS)。

考慮以上的觀點(diǎn),SIGURG 其實(shí)是一個(gè)很好的、滿足所有這些條件、且極不可能因被用戶態(tài)代碼進(jìn)行使用的一種信號(hào)。

const sigPreempt = _SIGURG  // preemptM 向 mp 發(fā)送搶占請(qǐng)求。該請(qǐng)求可以異步處理,也可以與對(duì) M 的其他請(qǐng)求合并。 // 接收到該請(qǐng)求后,如果正在運(yùn)行的 G 或 P 被標(biāo)記為搶占,并且 Goroutine 處于異步安全點(diǎn), // 它將搶占 Goroutine。在處理搶占請(qǐng)求后,它始終以原子方式遞增 mp.preemptGen。 func preemptM(mp *m) {   ...   signalM(mp, sigPreempt) } func signalM(mp *m, sig int) {   tgkill(getpid(), int(mp.procid), sig) }

搶占調(diào)用的注入

我們?cè)谛盘?hào)處理一節(jié)[5]中已經(jīng)知道,每個(gè)運(yùn)行的 M  都會(huì)設(shè)置一個(gè)系統(tǒng)信號(hào)的處理的回調(diào),當(dāng)出現(xiàn)系統(tǒng)信號(hào)時(shí),操作系統(tǒng)將負(fù)責(zé)將運(yùn)行代碼進(jìn)行中斷,并安全的保護(hù)其執(zhí)行現(xiàn)場,進(jìn)而 Go  運(yùn)行時(shí)能將針對(duì)信號(hào)的類型進(jìn)行處理,當(dāng)信號(hào)處理函數(shù)執(zhí)行結(jié)束后,程序會(huì)再次進(jìn)入內(nèi)核空間,進(jìn)而恢復(fù)到被中斷的位置。

但是這里面有一個(gè)很巧妙的用法,因?yàn)?sighandler 能夠獲得操作系統(tǒng)所提供的執(zhí)行上下文參數(shù)(例如寄存器 rip, rep 等),如果在  sighandler 中修改了這個(gè)上下文參數(shù),OS 會(huì)根據(jù)就該的寄存器進(jìn)行恢復(fù),這也就為搶占提供了機(jī)會(huì)。

//go:nowritebarrierrec func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {   ...   c := &sigctxt{info, ctxt}   ...   if sig == sigPreempt {     // 可能是一個(gè)搶占信號(hào)     doSigPreempt(gp, c)     // 即便這是一個(gè)搶占信號(hào),它也可能與其他信號(hào)進(jìn)行混合,因此我們     // 繼續(xù)進(jìn)行處理。   }   ... } // doSigPreempt 處理了 gp 上的搶占信號(hào) func doSigPreempt(gp *g, ctxt *sigctxt) {   // 檢查 G 是否需要被搶占、搶占是否安全   if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) {     // 插入搶占調(diào)用     ctxt.pushCall(funcPC(asyncPreempt))   }    // 記錄搶占   atomic.Xadd(&gp.m.preemptGen, 1)

在 ctxt.pushCall 之前,ctxt.rip() 和 ctxt.rep() 都保存了被中斷的 Goroutine 所在的位置,但是  pushCall 直接修改了這些寄存器,進(jìn)而當(dāng)從 sighandler 返回用戶態(tài) Goroutine 時(shí),能夠從注入的 asyncPreempt  開始執(zhí)行:

func (c *sigctxt) pushCall(targetPC uintptr) {   pc := uintptr(c.rip())   sp := uintptr(c.rsp())   sp -= sys.PtrSize   *(*uintptr)(unsafe.Pointer(sp)) = pc   c.set_rsp(uint64(sp))   c.set_rip(uint64(targetPC)) }

完成 sighandler 之,我們成功恢復(fù)到 asyncPreempt 調(diào)用:

// asyncPreempt 保存了所有用戶寄存器,并調(diào)用 asyncPreempt2 // // 當(dāng)棧掃描遭遇 asyncPreempt 棧幀時(shí),將會(huì)保守的掃描調(diào)用方棧幀 func asyncPreempt()

該函數(shù)的主要目的是保存用戶態(tài)寄存器,并且在調(diào)用完畢前恢復(fù)所有的寄存器上下文就好像什么事情都沒有發(fā)生過一樣:

TEXT &middot;asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0     ...     MOVQ AX, 0(SP)     ...     MOVUPS X15, 352(SP)     CALL &middot;asyncPreempt2(SB)     MOVUPS 352(SP), X15     ...     MOVQ 0(SP), AX     ...     RET

當(dāng)調(diào)用 asyncPreempt2 時(shí),會(huì)根據(jù) preemptPark 或者 gopreempt_m  重新切換回調(diào)度循環(huán),從而打斷密集循環(huán)的繼續(xù)執(zhí)行。

//go:nosplit func asyncPreempt2() {   gp := getg()   gp.asyncSafePoint = true   if gp.preemptStop {     mcall(preemptPark)   } else {     mcall(gopreempt_m)   }   // 異步搶占過程結(jié)束   gp.asyncSafePoint = false }

至此,異步搶占過程結(jié)束。我們總結(jié)一下?lián)屨颊{(diào)用的整體邏輯:

  1. M1 發(fā)送中斷信號(hào)(signalM(mp, sigPreempt))

  2. M2 收到信號(hào),操作系統(tǒng)中斷其執(zhí)行代碼,并切換到信號(hào)處理函數(shù)(sighandler(signum, info, ctxt, gp))

  3. M2 修改執(zhí)行的上下文,并恢復(fù)到修改后的位置(asyncPreempt)

  4. 重新進(jìn)入調(diào)度循環(huán)進(jìn)而調(diào)度其他 Goroutine(preemptPark 和 gopreempt_m)

上述的異步搶占流程我們是通過系統(tǒng)監(jiān)控來說明的,正如前面所提及的,異步搶占的本質(zhì)是在為垃圾回收器服務(wù),由于我們還沒有討論過 Go  語言垃圾回收的具體細(xì)節(jié),這里便不做過多展開,讀者只需理解,在垃圾回收周期開始時(shí),垃圾回收器將通過上述異步搶占的邏輯,停止所有用戶  Goroutine,進(jìn)而轉(zhuǎn)去執(zhí)行垃圾回收。

到此,關(guān)于“分析Go協(xié)作與搶占”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

網(wǎng)站題目:分析Go協(xié)作與搶占
URL分享:http://m.newbst.com/article20/pohgjo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站策劃ChatGPT網(wǎng)站導(dǎo)航虛擬主機(jī)移動(dòng)網(wǎng)站建設(shè)外貿(mào)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站優(yōu)化排名