精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

聊一聊Go 協(xié)作與搶占

開發(fā) 前端
Go 的運行時并不具備操作系統(tǒng)內(nèi)核級的硬件中斷能力,基于工作竊取的調(diào)度器實現(xiàn),本質(zhì)上屬于先來先服務(wù)的協(xié)作式調(diào)度,為了解決響應(yīng)時間可能較高的問題,目前運行時實現(xiàn)了兩種不同的調(diào)度策略、每種策略各兩個形式。

[[323539]]

本文轉(zhuǎn)載自微信公眾號「 碼農(nóng)桃花源」,轉(zhuǎn)載本文請聯(lián)系 碼農(nóng)桃花源公眾號。

  • 協(xié)作式調(diào)度
    • 主動用戶讓權(quán):Gosched
    • 主動調(diào)度棄權(quán):棧擴張與搶占標記
  • 搶占式調(diào)度
    • P 搶占
    • M 搶占
  • 小結(jié)
  • 進一步閱讀的參考文獻

我們在分析調(diào)度循環(huán)[1]的時候總結(jié)過一個問題:如果某個 G 執(zhí)行時間過長,其他的 G 如何才能被正常地調(diào)度?這便涉及到有關(guān)調(diào)度的兩個理念:協(xié)作式調(diào)度與搶占式調(diào)度。

協(xié)作式和搶占式這兩個理念解釋起來很簡單:協(xié)作式調(diào)度依靠被調(diào)度方主動棄權(quán);搶占式調(diào)度則依靠調(diào)度器強制將被調(diào)度方被動中斷。這兩個概念其實描述了調(diào)度的兩種截然不同的策略,這兩種決策模式,在調(diào)度理論中其實已經(jīng)研究得很透徹了。

Go 的運行時并不具備操作系統(tǒng)內(nèi)核級的硬件中斷能力,基于工作竊取的調(diào)度器實現(xiàn),本質(zhì)上屬于先來先服務(wù)的協(xié)作式調(diào)度,為了解決響應(yīng)時間可能較高的問題,目前運行時實現(xiàn)了兩種不同的調(diào)度策略、每種策略各兩個形式。保證在大部分情況下,不同的 G 能夠獲得均勻的時間片:

  • 同步協(xié)作式調(diào)度
  1. 主動用戶讓權(quán):通過 runtime.Gosched 調(diào)用主動讓出執(zhí)行機會;
  2. 主動調(diào)度棄權(quán):當(dāng)發(fā)生執(zhí)行棧分段時,檢查自身的搶占標記,決定是否繼續(xù)執(zhí)行;
  • 異步搶占式調(diào)度
  1. 被動監(jiān)控搶占:當(dāng) G 阻塞在 M 上時(系統(tǒng)調(diào)用、channel 等),系統(tǒng)監(jiān)控會將 P 從 M 上搶奪并分配給其他的 M 來執(zhí)行其他的 G,而位于被搶奪 P 的本地調(diào)度隊列中的 G 則可能會被偷取到其他 M 執(zhí)行。
  2. 被動 GC 搶占:當(dāng)需要進行垃圾回收時,為了保證不具備主動搶占處理的函數(shù)執(zhí)行時間過長,導(dǎo)致垃圾回收遲遲不能執(zhí)行而導(dǎo)致的高延遲,而強制停止 G 并轉(zhuǎn)為執(zhí)行垃圾回收。

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

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

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

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

  1. // Gosched 會讓出當(dāng)前的 P,并允許其他 Goroutine 運行。 
  2. // 它不會推遲當(dāng)前的 Goroutine,因此執(zhí)行會被自動恢復(fù) 
  3. func Gosched() { 
  4.   checkTimeouts() 
  5.   mcall(gosched_m) 
  6.  
  7. // Gosched 在 g0 上繼續(xù)執(zhí)行 
  8. func gosched_m(gp *g) { 
  9.   ... 
  10.   goschedImpl(gp) 

它首先會通過 note 機制通知那些等待被 ready 的 Goroutine:

  1. // checkTimeouts 恢復(fù)那些在等待一個 note 且已經(jīng)觸發(fā)其 deadline 時的 Goroutine。 
  2. func checkTimeouts() { 
  3.   now := nanotime() 
  4.   for n, nt := range notesWithTimeout { 
  5.     if n.key == note_cleared && now > nt.deadline { 
  6.       n.key = note_timeout 
  7.       goready(nt.gp, 1) 
  8.     } 
  9.   } 
  10.  
  11. func goready(gp *g, traceskip int) { 
  12.   systemstack(func() { 
  13.     ready(gp, traceskip, true
  14.   }) 
  15.  
  16. // 將 gp 標記為 ready 來運行 
  17. func ready(gp *g, traceskip intnext bool) { 
  18.   if trace.enabled { 
  19.     traceGoUnpark(gp, traceskip) 
  20.   } 
  21.  
  22.   status := readgstatus(gp) 
  23.  
  24.   // 標記為 runnable. 
  25.   _g_ := getg() 
  26.   _g_.m.locks++ // 禁止搶占,因為它可以在局部變量中保存 p 
  27.   if status&^_Gscan != _Gwaiting { 
  28.     dumpgstatus(gp) 
  29.     throw("bad g->status in ready"
  30.   } 
  31.  
  32.   // 狀態(tài)為 Gwaiting 或 Gscanwaiting, 標記 Grunnable 并將其放入運行隊列 runq 
  33.   casgstatus(gp, _Gwaiting, _Grunnable) 
  34.   runqput(_g_.m.p.ptr(), gp, next
  35.   if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 { 
  36.     wakep() 
  37.   } 
  38.   _g_.m.locks-- 
  39.   if _g_.m.locks == 0 && _g_.preempt { // 在 newstack 中已經(jīng)清除它的情況下恢復(fù)搶占請求 
  40.     _g_.stackguard0 = stackPreempt 
  41.   } 
  42.  
  43. func notetsleepg(n *note, ns int64) bool { 
  44.   gp := getg() 
  45.   ... 
  46.  
  47.   if ns >= 0 { 
  48.     deadline := nanotime() + ns 
  49.     ... 
  50.     notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: deadline} 
  51.     ... 
  52.     gopark(nil, nil, waitReasonSleep, traceEvNone, 1) 
  53.     ... 
  54.     delete(notesWithTimeout, n) 
  55.     ... 
  56.   } 
  57.  
  58.   ... 

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

  1. func goschedImpl(gp *g) { 
  2.   // 放棄當(dāng)前 g 的運行狀態(tài) 
  3.   status := readgstatus(gp) 
  4.   ... 
  5.   casgstatus(gp, _Grunning, _Grunnable) 
  6.   // 使當(dāng)前 m 放棄 g 
  7.   dropg() 
  8.   // 并將 g 放回全局隊列中 
  9.   lock(&sched.lock) 
  10.   globrunqput(gp) 
  11.   unlock(&sched.lock) 
  12.  
  13.   // 重新進入調(diào)度循環(huán) 
  14.   schedule() 

當(dāng)然,盡管具有主動棄權(quán)的能力,但它對 Go 語言的用戶要求比較高,因為用戶在編寫并發(fā)邏輯的時候需要自行甄別是否需要讓出時間片,這并非用戶友好的,而且很多 Go 的新用戶并不會了解到這個問題的存在,我們在隨后的搶占式調(diào)度中再進一步展開討論。

主動調(diào)度棄權(quán):棧擴張與搶占標記

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

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

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

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

  1. TEXT runtime·morestack(SB),NOSPLIT,$0-0 
  2.     ... 
  3.     MOVQ    0(SP), AX // f's PC 
  4.     MOVQ    AX, (g_sched+gobuf_pc)(SI) 
  5.     MOVQ    SI, (g_sched+gobuf_g)(SI) 
  6.     LEAQ    8(SP), AX // f's SP 
  7.     MOVQ    AX, (g_sched+gobuf_sp)(SI) 
  8.     MOVQ    BP, (g_sched+gobuf_bp)(SI) 
  9.     MOVQ    DX, (g_sched+gobuf_ctxt)(SI) 
  10.     ... 
  11.     CALL    runtime·newstack(SB) 

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

  1. //go:nowritebarrierrec 
  2. func newstack() { 
  3.   thisg := getg() 
  4.   ... 
  5.  
  6.   gp := thisg.m.curg 
  7.   ... 
  8.  
  9.   morebuf := thisg.m.morebuf 
  10.   thisg.m.morebuf.pc = 0 
  11.   thisg.m.morebuf.lr = 0 
  12.   thisg.m.morebuf.sp = 0 
  13.   thisg.m.morebuf.g = 0 
  14.  
  15.   // 如果是發(fā)起的搶占請求而非真正的棧分段 
  16.   preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt 
  17.  
  18.   // 保守的對用戶態(tài)代碼進行搶占,而非搶占運行時代碼 
  19.   // 如果正持有鎖、分配內(nèi)存或搶占被禁用,則不發(fā)生搶占 
  20.   if preempt { 
  21.     if !canPreemptM(thisg.m) { 
  22.       // 不發(fā)生搶占,繼續(xù)調(diào)度 
  23.       gp.stackguard0 = gp.stack.lo + _StackGuard 
  24.       gogo(&gp.sched) // 重新進入調(diào)度循環(huán) 
  25.     } 
  26.   } 
  27.   ... 
  28.   // 如果需要對棧進行調(diào)整 
  29.   if preempt { 
  30.     ... 
  31.     if gp.preemptShrink { 
  32.       // 我們正在一個同步安全點,因此等待棧收縮 
  33.       gp.preemptShrink = false 
  34.       shrinkstack(gp) 
  35.     } 
  36.     if gp.preemptStop { 
  37.       preemptPark(gp) // 永不返回 
  38.     } 
  39.     ... 
  40.     // 表現(xiàn)得像是調(diào)用了 runtime.Gosched,主動讓權(quán) 
  41.     gopreempt_m(gp) // 重新進入調(diào)度循環(huán) 
  42.   } 
  43.   ... 
  44. // 與 gosched_m 一致 
  45. func gopreempt_m(gp *g) { 
  46.   ... 
  47.   goschedImpl(gp) 

其中的 canPreemptM 驗證了可以被搶占的條件:

  1. 運行時沒有禁止搶占(m.locks == 0)
  2. 運行時沒有在執(zhí)行內(nèi)存分配(m.mallocing == 0)
  3. 運行時沒有關(guān)閉搶占機制(m.preemptoff == "")
  4. M 與 P 綁定且沒有進入系統(tǒng)調(diào)用(p.status == _Prunning)
  1. // canPreemptM 報告 mp 是否處于可搶占的安全狀態(tài)。 
  2. //go:nosplit 
  3. func canPreemptM(mp *m) bool { 
  4.   return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning 

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

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

  1. 進入系統(tǒng)調(diào)用時(runtime.reentersyscall,注意這種情況是為了保證不會發(fā)生棧分裂,真正的搶占是異步地通過系統(tǒng)監(jiān)控進行的)
  2. 任何運行時不再持有鎖的時候(m.locks == 0)
  3. 當(dāng)垃圾回收器需要停止所有用戶 Goroutine 時

搶占式調(diào)度

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

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

  1. // 此程序在 Go 1.14 之前的版本不會輸出 OK 
  2. package main 
  3. import ( 
  4.   "runtime" 
  5.   "time" 
  6. func main() { 
  7.   runtime.GOMAXPROCS(1) 
  8.   go func() { 
  9.     for { 
  10.     } 
  11.   }() 
  12.   time.Sleep(time.Millisecond) 
  13.   println("OK"

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

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

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

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

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

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

P 搶占

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

  1. unc retake(now int64) uint32 { 
  2.   n := 0 
  3.   // 防止 allp 數(shù)組發(fā)生變化,除非我們已經(jīng) STW,此鎖將完全沒有人競爭 
  4.   lock(&allpLock) 
  5.   for i := 0; i < len(allp); i++ { 
  6.     _p_ := allp[i] 
  7.     ... 
  8.     pd := &_p_.sysmontick 
  9.     s := _p_.status 
  10.     sysretake := false 
  11.     if s == _Prunning || s == _Psyscall { 
  12.       // 如果 G 運行時時間太長則進行搶占 
  13.       t := int64(_p_.schedtick) 
  14.       if int64(pd.schedtick) != t { 
  15.         pd.schedtick = uint32(t) 
  16.         pd.schedwhen = now 
  17.       } else if pd.schedwhen+forcePreemptNS <= now { 
  18.         ... 
  19.         sysretake = true 
  20.       } 
  21.     } 
  22.     // 對阻塞在系統(tǒng)調(diào)用上的 P 進行搶占 
  23.     if s == _Psyscall { 
  24.       // 如果已經(jīng)超過了一個系統(tǒng)監(jiān)控的 tick(20us),則從系統(tǒng)調(diào)用中搶占 P 
  25.       t := int64(_p_.syscalltick) 
  26.       if !sysretake && int64(pd.syscalltick) != t { 
  27.         pd.syscalltick = uint32(t) 
  28.         pd.syscallwhen = now 
  29.         continue 
  30.       } 
  31.       // 一方面,在沒有其他 work 的情況下,我們不希望搶奪 P 
  32.       // 另一方面,因為它可能阻止 sysmon 線程從深度睡眠中喚醒,所以最終我們?nèi)韵M麚寠Z P 
  33.       if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now { 
  34.         continue 
  35.       } 
  36.       // 解除 allpLock,從而可以獲取 sched.lock 
  37.       unlock(&allpLock) 
  38.       // 在 CAS 之前需要減少空閑 M 的數(shù)量(假裝某個還在運行) 
  39.       // 否則發(fā)生搶奪的 M 可能退出 syscall 然后再增加 nmidle ,進而發(fā)生死鎖 
  40.       // 這個過程發(fā)生在 stoplockedm 中 
  41.       incidlelocked(-1) 
  42.       if atomic.Cas(&_p_.status, s, _Pidle) { // 將 P 設(shè)為 idle,從而交與其他 M 使用 
  43.         ... 
  44.         n++ 
  45.         _p_.syscalltick++ 
  46.         handoffp(_p_) 
  47.       } 
  48.       incidlelocked(1) 
  49.       lock(&allpLock) 
  50.     } 
  51.   } 
  52.   unlock(&allpLock) 
  53.   return uint32(n) 

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

  1. 如果此時隊列為空,那么完全沒有必要進行搶占,這時候似乎可以繼續(xù)遍歷其他的 P,但必須在調(diào)度器中自旋的 M 和 空閑的 P 同時存在時、且系統(tǒng)調(diào)用阻塞時間非常長的情況下才能這么做。否則,這個 retake 過程可能返回 0,進而系統(tǒng)監(jiān)控可能看起來像是什么事情也沒做的情況下調(diào)整自己的步調(diào)進入深度睡眠。
  2. 在將 P 設(shè)置為空閑狀態(tài)前,必須先將 M 的數(shù)量減少,否則當(dāng) M 退出系統(tǒng)調(diào)用時,會在 exitsyscall0 中調(diào)用 stoplockedm 從而增加空閑 M 的數(shù)量,進而發(fā)生死鎖。

M 搶占

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

  1. func retake(now int64) uint32 { 
  2.   ... 
  3.   for i := 0; i < len(allp); i++ { 
  4.     _p_ := allp[i] 
  5.     ... 
  6.     if s == _Prunning || s == _Psyscall { 
  7.       ... 
  8.       } else if pd.schedwhen+forcePreemptNS <= now { 
  9.         // 對于 syscall 的情況,因為 M 沒有與 P 綁定, 
  10.         // preemptone() 不工作 
  11.         preemptone(_p_) 
  12.         sysretake = true 
  13.       } 
  14.     } 
  15.     ... 
  16.   } 
  17.   ... 
  18. func preemptone(_p_ *p) bool { 
  19.   // 檢查 M 與 P 是否綁定 
  20.   mp := _p_.m.ptr() 
  21.   if mp == nil || mp == getg().m { 
  22.     return false 
  23.   } 
  24.   gp := mp.curg 
  25.   if gp == nil || gp == mp.g0 { 
  26.     return false 
  27.   } 
  28.  
  29.   // 將 G 標記為搶占 
  30.   gp.preempt = true 
  31.  
  32.   // 一個 Goroutine 中的每個調(diào)用都會通過比較當(dāng)前棧指針和 gp.stackgard0 
  33.   // 來檢查棧是否溢出。 
  34.   // 設(shè)置 gp.stackgard0 為 StackPreempt 來將搶占轉(zhuǎn)換為正常的棧溢出檢查。 
  35.   gp.stackguard0 = stackPreempt 
  36.  
  37.   // 請求該 P 的異步搶占 
  38.   if preemptMSupported && debug.asyncpreemptoff == 0 { 
  39.     _p_.preempt = true 
  40.     preemptM(mp) 
  41.   } 
  42.  
  43.   return true 

搶占信號的選取

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

  1. 默認情況下,SIGURG 已經(jīng)用于調(diào)試器傳遞信號。
  2. SIGURG 可以不加選擇地虛假發(fā)生的信號。例如,我們不能選擇 SIGALRM,因為信號處理程序無法分辨它是否是由實際過程引起的(可以說這意味著信號已損壞)。而常見的用戶自定義信號 SIGUSR1 和 SIGUSR2 也不夠好,因為用戶態(tài)代碼可能會將其進行使用。
  3. 需要處理沒有實時信號的平臺(例如 macOS)。

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

  1. const sigPreempt = _SIGURG 
  2.  
  3. // preemptM 向 mp 發(fā)送搶占請求。該請求可以異步處理,也可以與對 M 的其他請求合并。 
  4. // 接收到該請求后,如果正在運行的 G 或 P 被標記為搶占,并且 Goroutine 處于異步安全點, 
  5. // 它將搶占 Goroutine。在處理搶占請求后,它始終以原子方式遞增 mp.preemptGen。 
  6. func preemptM(mp *m) { 
  7.   ... 
  8.   signalM(mp, sigPreempt) 
  9. func signalM(mp *m, sig int) { 
  10.   tgkill(getpid(), int(mp.procid), sig) 

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

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

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

  1. //go:nowritebarrierrec 
  2. func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) { 
  3.   ... 
  4.   c := &sigctxt{info, ctxt} 
  5.   ... 
  6.   if sig == sigPreempt { 
  7.     // 可能是一個搶占信號 
  8.     doSigPreempt(gp, c) 
  9.     // 即便這是一個搶占信號,它也可能與其他信號進行混合,因此我們 
  10.     // 繼續(xù)進行處理。 
  11.   } 
  12.   ... 
  13. // doSigPreempt 處理了 gp 上的搶占信號 
  14. func doSigPreempt(gp *g, ctxt *sigctxt) { 
  15.   // 檢查 G 是否需要被搶占、搶占是否安全 
  16.   if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) { 
  17.     // 插入搶占調(diào)用 
  18.     ctxt.pushCall(funcPC(asyncPreempt)) 
  19.   } 
  20.  
  21.   // 記錄搶占 
  22.   atomic.Xadd(&gp.m.preemptGen, 1) 

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

  1. func (c *sigctxt) pushCall(targetPC uintptr) { 
  2.   pc := uintptr(c.rip()) 
  3.   sp := uintptr(c.rsp()) 
  4.   sp -= sys.PtrSize 
  5.   *(*uintptr)(unsafe.Pointer(sp)) = pc 
  6.   c.set_rsp(uint64(sp)) 
  7.   c.set_rip(uint64(targetPC)) 

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

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

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

  1. TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 
  2.     ... 
  3.     MOVQ AX, 0(SP) 
  4.     ... 
  5.     MOVUPS X15, 352(SP) 
  6.     CALL ·asyncPreempt2(SB) 
  7.     MOVUPS 352(SP), X15 
  8.     ... 
  9.     MOVQ 0(SP), AX 
  10.     ... 
  11.     RET 

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

  1. //go:nosplit 
  2. func asyncPreempt2() { 
  3.   gp := getg() 
  4.   gp.asyncSafePoint = true 
  5.   if gp.preemptStop { 
  6.     mcall(preemptPark) 
  7.   } else { 
  8.     mcall(gopreempt_m) 
  9.   } 
  10.   // 異步搶占過程結(jié)束 
  11.   gp.asyncSafePoint = false 

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

  1. M1 發(fā)送中斷信號(signalM(mp, sigPreempt))
  2. M2 收到信號,操作系統(tǒng)中斷其執(zhí)行代碼,并切換到信號處理函數(shù)(sighandler(signum, info, ctxt, gp))
  3. M2 修改執(zhí)行的上下文,并恢復(fù)到修改后的位置(asyncPreempt)
  4. 重新進入調(diào)度循環(huán)進而調(diào)度其他 Goroutine(preemptPark 和 gopreempt_m)

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

小結(jié)

總的來說,應(yīng)用層的調(diào)度策略不易實現(xiàn),因此實現(xiàn)上也并不是特別緊急。我們回顧 Go 語言調(diào)度策略的調(diào)整過程不難發(fā)現(xiàn),實現(xiàn)它們的動力是從實際需求出發(fā)的。Go 語言從設(shè)計之初并沒有刻意地去考慮對 Goroutine 的搶占機制。從早期無法對 Goroutine 進行搶占的原始時代,到現(xiàn)在的協(xié)作與搶占同時配合的調(diào)度策略,其問題的核心是垃圾回收的需要。

運行時需要執(zhí)行垃圾回收時,協(xié)作式調(diào)度能夠保證具備函數(shù)調(diào)用的用戶 Goroutine 正常停止;搶占式調(diào)度則能避免由于死循環(huán)導(dǎo)致的任意時間的垃圾回收延遲。至此,Go 語言的用戶可以放心地寫出各種形式的代碼邏輯,運行時垃圾回收也能夠在適當(dāng)?shù)臅r候及時中斷用戶代碼,不至于導(dǎo)致整個系統(tǒng)進入不可預(yù)測的停頓。

進一步閱讀的參考文獻[Clements, 2019] Austin Clements. Proposal: Non-cooperative goroutine preemption. January 18, 2019. https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md

[Clements, 2015] Austin Clements. runtime: tight loops should be preemptible](https://golang.org/issue/10958

[Chase, 2017] David Chase. cmd/compile: loop preemption with "fault branch" on amd64. May 09, 2019. https://golang.org/cl/43050

參考資料

[1]分析調(diào)度循環(huán): https://changkun.de/golang/zh-cn/part2runtime/ch06sched/exec[2]6.6 執(zhí)行棧管理: https://changkun.de/golang/zh-cn/part2runtime/ch06sched/stack/[3]垃圾回收器: https://github.com/qcrao/Go-Questions/blob/master/GC/GC.md[4]8.9 安全點分析: https://changkun.de/golang/zh-cn/part2runtime/ch08gc/safe[5]信號處理一節(jié): https://changkun.de/golang/zh-cn/part2runtime/ch06sched/signal/

本文作者歐長坤,德國慕尼黑大學(xué)在讀博士,Go/etcd/Tensorflow contributor,開源書籍《Go 語言原本》作者,《Go 夜讀》SIG 成員/講師,對 Go 有很深的研究。Github:@changkun,https://changkun.de。

責(zé)任編輯:武曉燕 來源: 碼農(nóng)桃花源
相關(guān)推薦

2021-04-15 12:10:42

Go語言Go開發(fā)者

2018-03-23 10:30:56

微網(wǎng)關(guān)服務(wù)嚙合微服務(wù)

2021-08-11 09:37:11

Redis持久化磁盤

2023-09-27 09:04:50

2021-09-15 14:52:43

數(shù)字貨幣傳銷虛擬貨幣

2022-03-31 10:41:35

iOS應(yīng)用提審發(fā)布

2018-06-07 13:17:12

契約測試單元測試API測試

2021-01-28 22:31:33

分組密碼算法

2020-05-22 08:16:07

PONGPONXG-PON

2023-09-22 17:36:37

2021-08-04 10:15:14

Go路徑語言

2021-01-01 09:01:05

前端組件化設(shè)計

2020-08-12 08:34:16

開發(fā)安全We

2022-10-08 11:33:56

邊緣計算云計算

2022-11-26 00:00:06

裝飾者模式Component

2020-06-28 09:30:37

Linux內(nèi)存操作系統(tǒng)

2022-03-08 16:10:38

Redis事務(wù)機制

2022-03-29 09:56:21

游戲版本運營

2018-01-10 14:13:04

測試矩陣API測試

2020-09-08 06:54:29

Java Gradle語言
點贊
收藏

51CTO技術(shù)棧公眾號

九九热精品在线观看| 亚洲不卡视频在线| 香蕉av在线播放| 久久久国产精品无码| 爽爽视频在线观看| 蜜臀av性久久久久av蜜臀妖精| 日韩亚洲精品电影| 四虎永久免费观看| 亚洲www啪成人一区二区| 亚洲品质自拍视频网站| 精品一区二区三区视频日产| 中文字幕精品在线观看| 狠狠久久婷婷| 中文字幕日韩综合av| 黄色av电影网站| av一区在线播放| 亚洲国产精品精华液网站| 国产高清精品一区二区| 亚洲中文一区二区三区| 亚洲一区亚洲| 美女撒尿一区二区三区| 国产一二三四五区| 凹凸av导航大全精品| 欧洲国产伦久久久久久久| 无码人妻少妇伦在线电影| 77777影视视频在线观看| 9色porny自拍视频一区二区| 成人免费视频在线观看超级碰| 国产香蕉视频在线| 欧美.www| 色av中文字幕一区| 色婷婷在线影院| av日韩精品| 51精品国自产在线| 亚洲精品中文字幕无码蜜桃| 国产在线美女| 亚洲一区二区三区精品在线| 亚洲区一区二区三区| 你懂的视频在线| www.在线成人| 国产精品一区而去| www.97超碰| 国内精品视频666| 国产美女扒开尿口久久久| 国产精品suv一区| 亚洲视频播放| 韩国视频一区二区| 精品亚洲永久免费精品 | 久久人人超碰| 69**夜色精品国产69乱| 日本在线视频免费| 一区在线免费| 久久久久亚洲精品成人网小说| 美国黄色小视频| 91精品国产乱码久久久久久久| 在线日韩av观看| 91精品久久久久久久久久久久| 九一精品国产| 国产亚洲欧美一区| 亚洲一区 欧美| 欧美高清视频手机在在线| 最好看的2019年中文视频| 国产白丝一区二区三区| 欧美成人直播| 久久偷看各类女兵18女厕嘘嘘| 国产免费久久久久| 欧美日韩第一区| 午夜精品久久久久久久男人的天堂 | 亚洲一区视频在线观看视频| 日本黄色片一级片| av中文在线资源库| 欧美日韩亚洲高清| 给我免费观看片在线电影的| 青青视频在线免费观看| a91a精品视频在线观看| 91sa在线看| 日韩黄色一级视频| 久久99国产精品尤物| 91亚洲国产成人精品性色| www.99视频| 91香蕉视频污| 亚洲图片欧洲图片日韩av| 免费看美女视频在线网站| 亚洲人成7777| 日韩av综合网站| 国产精品国产精品国产专区不卡| 刘亦菲毛片一区二区三区| 91首页免费视频| 亚洲成人一区二区三区| 26uuu亚洲电影在线观看| 午夜精品一区二区三区电影天堂 | 久久精品免费观看| 91精品国自产在线观看| 天堂网av2014| 中文字幕av资源一区| 最新av网址在线观看| 超碰91在线观看| 欧美视频在线观看一区| 人妻体体内射精一区二区| 日韩欧美中文字幕电影| 精品国产欧美一区二区三区成人| 日本三级片在线观看| 免费一级片91| 精品乱色一区二区中文字幕| 色欧美激情视频在线| 午夜精品123| 男女污污视频网站| 亚洲盗摄视频| 久久久久久这里只有精品| 久久久久久亚洲av无码专区| 国产精品 日产精品 欧美精品| 欧美日本韩国在线| 9999热视频在线观看| 欧美男男青年gay1069videost | 欧美日韩一区二区三区视频播放| 九九精品视频在线| 姑娘第5集在线观看免费好剧| 成人少妇影院yyyy| 中文字幕欧美人与畜| 亚洲最大网站| 亚洲第一天堂av| 国产av 一区二区三区| 日本女优在线视频一区二区| 精品国产综合久久| 肉肉视频在线观看| 欧美巨大另类极品videosbest | 成人h猎奇视频网站| 蜜桃av鲁一鲁一鲁一鲁俄罗斯的 | 亚洲熟女综合色一区二区三区| 国产一区激情在线| 亚洲日本精品一区| 欧美性片在线观看| 精品丝袜一区二区三区| 国产亚洲精品av| 国产精品99精品久久免费| 一区二区欧美日韩| 99亚洲伊人久久精品影院| 婷婷丁香久久五月婷婷| 深夜视频在线观看| 欧美午夜在线视频| 97神马电影| 在线中文字幕视频观看| 欧美一区二区人人喊爽| 欧美特黄一级片| 毛片基地黄久久久久久天堂| 婷婷精品国产一区二区三区日韩 | 亚洲激情自拍| 国产精品av一区| 牛牛在线精品视频| 欧美不卡一二三| 欧美成人精品激情在线视频| 国产在线精品一区二区三区不卡| 亚洲在线不卡| 国产精品日本一区二区不卡视频| 日韩在线免费视频观看| 国产精品美女一区| 亚洲欧美日韩精品久久久久| 在线a免费观看| 欧美 日韩 国产一区二区在线视频 | 国产欧美一区二区三区在线看蜜臂| av高清久久久| 国产性xxxx18免费观看视频| 综合综合综合综合综合网| 国产91九色视频| 尤物网在线观看| 69精品人人人人| 国产亚洲精品久久久久久豆腐| 精品一区二区在线看| 丰满女人性猛交| 欧美日韩亚洲自拍| 国产欧美一区二区三区精品观看| 国产精品热视频| 黄色免费在线看| 日韩精品在线一区| 日韩精品手机在线| 国产欧美中文在线| 99精品视频国产| 在线日韩视频| 日韩精品一区二区三区外面| 四虎地址8848精品| 午夜精品99久久免费| 成人在线观看网站| 欧美一区二区三区播放老司机| 亚洲国产精一区二区三区性色| 久久综合色婷婷| gai在线观看免费高清| 亚洲午夜黄色| 日本一区美女| 日韩区一区二| 日本欧美一二三区| 成人在线app| 日韩精品福利在线| 国产精品毛片一区视频播| 午夜精品免费在线观看| 香蕉成人在线视频| a级高清视频欧美日韩| 日日噜噜夜夜狠狠| 91久久午夜| 在线视频一区观看| 亚洲精品播放| 99久久精品久久久久久ai换脸| 成人性生交大片免费网站| 大胆人体色综合| 狠狠狠综合7777久夜色撩人| 日韩网站在线看片你懂的| www.国产毛片| 亚洲成人av中文| 国产精品久久久免费看| 26uuu国产在线精品一区二区| 国产一级片自拍| 久久一区欧美| av免费观看国产| 综合激情在线| 五月天久久狠狠| 清纯唯美亚洲经典中文字幕| 91亚洲永久免费精品| 日韩和的一区二在线| 欧美日本亚洲视频| 黄色网址免费在线观看| 亚洲欧美日韩一区二区在线 | 亚洲国产精品成人av| 一级黄色片在线| 色婷婷激情综合| 久久久午夜影院| www视频在线看| 在线电影院国产精品| 欧产日产国产69| 亚洲mv在线观看| 免费在线观看黄色av| 中文字幕一区三区| 人与嘼交av免费| 久久精品夜色噜噜亚洲aⅴ| www.88av| 99re热视频这里只精品 | 91香蕉视频mp4| 日本69式三人交| 丁香六月综合激情| 亚洲少妇中文字幕| 国产成人av电影| 国产xxx在线观看| 国产成人综合在线观看| 污污视频在线免费| 国产主播一区二区三区| 日韩va在线观看| 国内精品在线播放| 三日本三级少妇三级99| 国产精品中文字幕日韩精品| 欧美又黄又嫩大片a级| 国内精品伊人久久久久av影院| 在线播放免费视频| 丁香另类激情小说| 国产一卡二卡三卡四卡| av激情亚洲男人天堂| 国产男男chinese网站| 久久精品人人做人人爽97| 美女爆乳18禁www久久久久久| 国产亚洲成年网址在线观看| 日本精品在线观看视频| 欧美国产精品专区| 国产黄色录像片| 亚洲精品国产成人久久av盗摄 | 色悠悠亚洲一区二区| 亚洲天堂视频网站| 欧美在线免费视屏| 国产又粗又猛又爽又黄91| 日韩三级在线观看| 天天操天天操天天| 亚洲亚裔videos黑人hd| 麻豆网站在线| 久久久久久久亚洲精品| av高清不卡| 国产欧美中文字幕| 97久久超碰| 欧美日韩在线高清| 亚洲成人tv| 九色自拍视频在线观看| 狂野欧美性猛交xxxx巴西| 在线观看日本一区二区| 国产91精品精华液一区二区三区 | 欧美中文在线字幕| 日日夜夜一区| 亚洲精品久久久蜜桃| 看av免费毛片手机播放| 美女性感视频久久| 不许穿内裤随时挨c调教h苏绵 | 91精品国产综合久久精品| 亚洲av无码乱码国产精品| 亚洲精品视频久久| 麻豆av在线免费看| 欧美精品电影在线| av有声小说一区二区三区| 成人中心免费视频| 日韩高清一级| 中文字幕不卡每日更新1区2区| 国产综合网站| 久久99爱视频| 99精品久久久久久| 2025国产精品自拍| 91精品福利在线| 国产91绿帽单男绿奴| 国产一区二区日韩| 黄色美女视频在线观看| 国产精品美女无圣光视频| 欧美电影在线观看免费| 午夜精品短视频| 最新成人av网站| 天堂av手机在线| 国产日韩视频一区二区三区| 久操免费在线视频| 欧美日韩高清一区二区| 日韩a级作爱片一二三区免费观看| 久久影院免费观看| 成人网ww555视频免费看| 国产一区二区无遮挡| 91精品电影| 色播五月激情五月| 国产欧美一区二区精品性色 | 欧美日韩一区成人| 欧美亚洲日本| 97视频免费观看| 91麻豆精品| 亚洲综合视频一区| 丝袜美腿一区二区三区| 右手影院亚洲欧美| 偷拍与自拍一区| 亚洲乱码国产乱码精品精软件| www国产亚洲精品久久网站| 精品亚洲美女网站| 欧美黄色直播| 香蕉久久夜色精品| 玖草视频在线观看| 欧美日韩午夜剧场| 人妻91麻豆一区二区三区| 欧美激情18p| 一区二区视频| 日韩成人三级视频| 国产精品自拍av| 欧美精品成人久久| 欧美岛国在线观看| 成人免费高清| 亚洲综合中文字幕在线观看| 亚洲国产一区二区三区在线播放| 天堂视频免费看| 亚洲蜜桃精久久久久久久| 99国产精品一区二区三区| 久久视频免费在线播放| 国产一区二区| 日本黄色播放器| 国产一区二区不卡| 欧美黑人精品一区二区不卡| 精品欧美一区二区久久| 激情五月婷婷六月| 日韩专区在线视频| 天天摸日日摸狠狠添| 欧美欧美午夜aⅴ在线观看| 日本欧美在线视频免费观看| 国产日韩欧美视频| 亚洲视频电影在线| 国产婷婷在线观看| 懂色av影视一区二区三区| 九色视频在线观看免费播放| 国产精品福利在线观看| 日韩欧美精品一区| www激情五月| 亚洲第一搞黄网站| 免费在线超碰| 91精品美女在线| 国内精品美女在线观看 | 老汉色老汉首页av亚洲| 无罩大乳的熟妇正在播放| 久久久99精品久久| 国产精品久久久久久久成人午夜| 欧美国产精品va在线观看| 欧美18免费视频| 亚洲国产高清av| 一区二区三区日韩精品视频| 午夜成人免费影院| 国产精品久久久久一区二区 | 国产精品久久精品日日| 国产裸体无遮挡| 欧美野外猛男的大粗鳮| 久久激情电影| 在线中文字日产幕| 欧美图片一区二区三区| 在线观看男女av免费网址| 欧美xxxx黑人又粗又长密月| 久久99国产精品麻豆| 国产精品成人aaaa在线| 中文字幕无线精品亚洲乱码一区| 欧一区二区三区| 国产又粗又长又大的视频| 一片黄亚洲嫩模| 成人免费在线视频网| 成人自拍爱视频| 日本视频中文字幕一区二区三区| 久久久久久久福利| 中日韩午夜理伦电影免费| 国产精品流白浆在线观看| 在线免费视频一区| 无吗不卡中文字幕|