協程調度時機一:系統調用
前言
在講述系統調用發生的協程調度之前,讓我們看看go是如何進入系統調用的,理解了這個讓我們不會對後面所說的一些東西感到很陌生。
golang對操作系統的系統調用作了封裝,提供了syscall這樣的庫讓我們執行系統調用。例如,Read系統調用實現如下:
func Read(fd int, p []byte) (n int, err error) { n, err = read(fd, p) if raceenabled { if n > 0 { ...... } ...... } return}// 最終封裝了Syscall func read(fd int, p []byte) (n int, err error) { var _p0 unsafe.Pointer if len(p) > 0 { _p0 = unsafe.Pointer(&p[0]) } else { _p0 = unsafe.Pointer(&_zero) } r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p))) n = int(r0) if e1 != 0 { err = e1 } return}// 我們只關心進入系統調用時調用的runtime·entersyscall// 和退出時調用的runtime·exitsyscallTEXT ·Syscall(SB),NOSPLIT,$0-56 CALL runtime·entersyscall(SB) MOVQ 16(SP), DI MOVQ 24(SP), SI MOVQ 32(SP), DX MOVQ $0, R10 MOVQ $0, R8 MOVQ $0, R9 MOVQ 8(SP), AX // syscall entry SYSCALL CMPQ AX, $0xfffffffffffff001 JLS ok MOVQ $-1, 40(SP) // r1 MOVQ $0, 48(SP) // r2 NEGQ AX MOVQ AX, 56(SP) // errno CALL runtime·exitsyscall(SB) RET
我們並不關心系統調用到底怎麼實現。我們只關心系統調用過程與調度器相關內容,因為Golang自己接管系統調用,調度器便可以在進出系統調用時做一些你所不明白的優化,這裡我要帶你弄清楚調度器怎麼做優化的。
進入系統調用前
我們前面說過,系統調用是一個相對耗時的過程。一旦P中的某個G進入系統調用狀態而阻塞了該P內的其他協程。此時調度器必須得做點什麼吧,這就是調度器在進入系統調用前call runtime·entersyscall目的所在。
void·entersyscall(int32 dummy){ runtime·reentersyscall((uintptr)runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));}voidruntime·reentersyscall(uintptr pc, uintptr sp){ void (*fn)(void); // 為什麼g->m->locks++? g->m->locks++; g->stackguard0 = StackPreempt; g->throwsplit = 1; // Leave SP around for GC and traceback. // save()到底在save什麼? save(pc, sp); g->syscallsp = sp; g->syscallpc = pc; runtime·casgstatus(g, Grunning, Gsyscall); // 這些堆棧之間到底是什麼關係? if(g->syscallsp < g->stack.lo || g->stack.hi < g->syscallsp) { fn = entersyscall_bad; runtime·onM(&fn); } // 這個還不知道是啥意思 if(runtime·atomicload(&runtime·sched.sysmonwait)) { fn = entersyscall_sysmon; runtime·onM(&fn); save(pc, sp); } // 這裡很關鍵:P的M已經陷入系統調用,於是P忍痛放棄該M // 但是請注意:此時M還指向P,在M從系統調用返回後還能找到P g->m->mcache = nil; g->m->p->m = nil; // P的狀態變為Psyscall runtime·atomicstore(&g->m->p->status, Psyscall); if(runtime·sched.gcwaiting) { fn = entersyscall_gcwait; runtime·onM(&fn); save(pc, sp); } g->stackguard0 = StackPreempt; g->m->locks--;}
上面與調度器相關的內容其實就是將M從P剝離出去,告訴調度器,我已經放棄M了,我不能餓著我的孩子們(G)。但是M內心還是記著P的,在系統調用返回後,M還盡量找回原來的P,至於P是不是另結新歡就得看情況了。
注意這時候P放棄了前妻M,但是還沒有給孩子們找後媽(M),只是將P的狀態標記為PSyscall,那麼什麼時候以及怎麼樣給孩子們找後媽呢?我們在後面詳細闡述。
從系統調用返回後
從系統調用返回後,也要告訴調度器,因為需要調度器做一些事情,根據前面系統調用的實現,具體實現是:
void·exitsyscall(int32 dummy){ void (*fn)(G*); // 這個g到底是什麼? g->m->locks++; // see comment in entersyscall if(runtime·getcallersp(&dummy) > g->syscallsp) runtime·throw("exitsyscall: syscall frame is no longer valid"); g->waitsince = 0; // 判斷能否快速找到歸屬 if(exitsyscallfast()) { g->m->p->syscalltick++; // g的狀態從syscall變成running,繼續歡快地跑著 runtime·casgstatus(g, Gsyscall, Grunning); g->syscallsp = (uintptr)nil; g->m->locks--; if(g->preempt) { g->stackguard0 = StackPreempt; } else { g->stackguard0 = g->stack.lo + StackGuard; } g->throwsplit = 0; return; } g->m->locks--; // Call the scheduler. // 如果M回來發現P已經有別人服務了,那隻能將自己掛起 // 等著服務別人。 fn = exitsyscall0; runtime·mcall(&fn); ......}static boolexitsyscallfast(void){ void (*fn)(void); if(runtime·sched.stopwait) { g->m->p = nil; return false; } // 如果之前附屬的P尚未被其他M,嘗試綁定該P if(g->m->p && g->m->p->status == Psyscall && runtime·cas(&g->m->p->status, Psyscall, Prunning)) { g->m->mcache = g->m->p->mcache; g->m->p->m = g->m; return true; } // Try to get any other idle P. // 否則從空閑P列表中隨便撈一個出來 g->m->p = nil; if(runtime·sched.pidle) { fn = exitsyscallfast_pidle; runtime·onM(&fn); if(g->m->scalararg[0]) { g->m->scalararg[0] = 0; return true; } } return false;}
G從系統調用返回的過程,其實就是失足婦女找男人的邏輯:
- 首先看看能否回到當初愛人(P)的懷抱:找到當初被我拋棄的男人,我這裡還存著它的名片(m->p),家庭住址什麼的我都還知道;
- 如果愛人受不了寂寞和撫養孩子的壓力已經變節(P的狀態不再是Psyscall),那我就隨便找個單身待解救男人從了也行;
- 如果上面的1、2都找不到,那也沒辦法,男人都死絕了,老娘只好另想他法。
以上過程1和2其實就是exitsyscallfast()的主要流程,用懷孕了的失足婦女找男人再合適不過。 一個女人由於年輕不懂事失足,拋家棄子(家是P,子是P的G)。當浪子回頭後,意欲尋回從前的夫君,只能有兩種可能:
- 等了很久已然心灰意冷的夫君在家人的安排下另娶他人;
- 痴情的夫君已然和嗷嗷待哺的孩子們依然在等待她的歸回。
當然第二種的結局比較圓滿,這個女人從此死心塌地守著這個家,於是p->m又回來了,孩子們(g)又可以繼續活下去了。 第一種就比較難辦了,女人(m)心灰意冷,將產下的兒子(陷入系統調用的g)交於他人(全局g的運行隊列)撫養,遠走他鄉,從此接收命運的安排(參與調度,以後可能服務於別的p)。 對於第二種可能性,只能說女人的命運比較悲慘了:
static voidexitsyscall0(G *gp){ P *p; runtime·casgstatus(gp, Gsyscall, Grunnable); dropg(); runtime·lock(&runtime·sched.lock); // 這裡M再次嘗試為自己找個歸宿P p = pidleget(); // 如果沒找到P,M講自己放入全局的運行隊列中 // 同時將它的g放置到全局的P queue中進去,自己不管了 if(p == nil) globrunqput(gp); else if(runtime·atomicload(&runtime·sched.sysmonwait)) { runtime·atomicstore(&runtime·sched.sysmonwait, 0); runtime·notewakeup(&runtime·sched.sysmonnote); } runtime·unlock(&runtime·sched.lock); // 如果找到了P,佔有P並且開始執行P內的g,永不回頭 if(p) { acquirep(p); execute(gp); // Never returns. } if(g->m->lockedg) { // Wait until another thread schedules gp and so m again. stoplockedm(); execute(gp); // Never returns. } // 找了一圈還是沒找到,釋放掉M當前執行環境,M不再做事 // stopm會暫停當前M直到其找到了可運行的P為止 // 找到以後進入schedule,執行P內的g stopm(); // m從stopm()中返回以後,說明該m被綁定至某個P,可以開始 // 繼續歡快地跑了,此時就需要調度找到一個g去執行 // 這就是調用schedule的目的所在 schedule(); // Never returns. }
話說到這裡,其實這個M當前沒有運行的價值了(無法找到p運行它),那麼我們就將她掛起,直到被其他人喚醒。 m被掛起調用的函數是stopm()
// Stops execution of the current m until new work is available. // Returns with acquired P. static void stopm(void){ if(g->m->locks) runtime·throw("stopm holding locks"); if(g->m->p) runtime·throw("stopm holding p"); if(g->m->spinning) { g->m->spinning = false; runtime·xadd(&runtime·sched.nmspinning, -1); }retry: runtime·lock(&runtime·sched.lock); // 將m插入到空閑m隊列中,統一管理 mput(g->m); runtime·unlock(&runtime·sched.lock); // 在這裡被掛起,阻塞在m->park上,位於lock_futex.go runtime·notesleep(&g->m->park); // 從掛起被喚醒後開始執行 runtime·noteclear(&g->m->park); if(g->m->helpgc) { runtime·gchelper(); g->m->helpgc = 0; g->m->mcache = nil; goto retry; } // m->nextp是什麼? acquirep(g->m->nextp); g->m->nextp = nil;}
那麼說到這裡,其實很多事情都一目了然,當一個M從系統調用返回後,通過各種方式想找到可以託付的P(找前夫—>找閑漢),求之不得最終只能將自己掛起,等待下次系統中有空閑的P的時候被喚醒。
sysmon
前面我們重點講了一個m是如何陷入系統調用和如何返回的心酸之路。我們忽略了p的感情,因為他才是真正的受害者,它被剝奪了m,從此無人理會它嗷嗷待哺的孩子們(g),並且狀態還被變成了Psyscall,相當於貼上了屌絲標籤,別無他法,只能等待陷入系統調用的m返回,再續前緣。 當然,這樣做是不合理的,因為如果m進入系統調用後樂不思蜀,那P的孩子們都得餓死,這在現實社會中可以發生,但在數字世界裡是決不允許的。 OK,組織絕對不會忽略這種情況的,於是,保姆(管家)出現了,它就是sysmon線程,這是一個特殊的m,專門監控系統狀態。 sysmon周期性醒來,並且遍歷所有的p,如果發現有Psyscall狀態的p並且已經處於該狀態超過一定時間了,那就不管那個負心的前妻,再次p安排一個m,這樣p內的任務又可以得到處理了。
func sysmon() { ...... retake(now); ......}// 我們只摘取了sysmon中與P處理相關的代碼分析:static uint32retake(int64 now){ uint32 i, s, n; int64 t; P *p; Pdesc *pd; n = 0; // 遍歷所有的P,根據其狀態作相應處理,我們只關注Psyscall for(i = 0; i < runtime·gomaxprocs; i++) { p = runtime·allp[i]; if(p==nil) continue; pd = &pdesc[i]; s = p->status; if(s == Psyscall) { t = p->syscalltick; if(pd->syscalltick != t) { pd->syscalltick = t; pd->syscallwhen = now; continue; } if(p->runqhead == p->runqtail && runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0 && pd->syscallwhen + 10*1000*1000 > now) continue; incidlelocked(-1); // 因為需要將P重新安排m,所以狀態轉化為Pidle if(runtime·cas(&p->status, s, Pidle)) { n++; handoffp(p); } incidlelocked(1); ......}
找到了處於Psyscall狀態的P後,繼續判斷它等待的時間是否已經太長,如果是這樣,就準備拋棄原來的還陷入syscall的m,調用handoff(p),開始為p準備新生活。
我們接下來仔細分析下p是怎麼過上新生活的,handoffp無非就是找一個新的m,將m與該p綁定,接下來將由m繼續執行該p內的g。
handoffp()找到的新的m可能是別人以前的m(私生活好混亂)。由於這裡獲得的m是處於idle狀態,處於wait狀態(在stopm()中被sleep的),在這裡會通過startm()來喚醒它。被喚醒的m繼續執行它被阻塞的下一條語句:
stopm() { ...... // 從掛起被喚醒後開始執行 runtime·noteclear(&g->m->park); if(g->m->helpgc) { runtime·gchelper(); g->m->helpgc = 0; g->m->mcache = nil; goto retry; } // 將M和P綁定 acquirep(g->m->nextp); g->m->nextp = nil;}// 由於m在sleep前的調用路徑是exitsyscall0() –> stopm(),從stopm()中返回至exitsyscall0後,執行接下來的語句func exitsyscall0(gp *g) { _g_ := getg() ...... stopm() // m繼續run起來後,執行一次schedule // 找到m->p裡面可運行的g並執行 schedule() // Never returns. }// One round of scheduler: find a runnable goroutine and execute it. // Never returns. func schedule() { _g_ := getg() ...... if gp == nil { gp, inheritTime = runqget(_g_.m.p.ptr()) if gp != nil && _g_.m.spinning { throw("schedule: spinning with local work") } } if gp == nil { gp, inheritTime = findrunnable() resetspinning() } if gp.lockedm != nil { // Hands off own p to the locked m, // then blocks waiting for a new p. startlockedm(gp) goto top } // 執行該gp execute(gp, inheritTime)}
推薦閱讀: