Go調(diào)度器學(xué)習(xí)之系統(tǒng)調(diào)用詳解
0. 簡介
上篇博客,我們分析了Go調(diào)度器中的搶占策略,這篇,我們將分析一下,在系統(tǒng)調(diào)用時發(fā)生的調(diào)度行為。
1. 系統(tǒng)調(diào)用
下面,我們將以一個簡單的文件打開的系統(tǒng)調(diào)用,來分析一下Go調(diào)度器在系統(tǒng)調(diào)用時做了什么。
1.1 場景
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
f, err := os.Open("./file")
if err != nil {
panic(err)
}
defer f.Close()
content, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
fmt.Println(string(content))
}
如上簡單的代碼,讀取一個名為file的本地文件,然后打印其數(shù)據(jù),我們通過匯編代碼來分析一下其調(diào)用過程:
$ go build -gcflags "-N -l" -o main main.go
$ objdump -d main >> main.i
可以發(fā)現(xiàn),在main.i中,從main.main函數(shù),對于文件Open操作的調(diào)用關(guān)系為:main.main -> os.Open -> os.openFile -> os.openFileNolog -> syscall.openat -> syscall.Syscall6.abi0 -> runtime.entersyscall.abi0,而Syscall6的匯編如下:
TEXT ·Syscall6(SB),NOSPLIT,$0-80
CALL runtime·entersyscall(SB)
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ a4+32(FP), R10
MOVQ a5+40(FP), R8
MOVQ a6+48(FP), R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS ok6
MOVQ $-1, r1+56(FP)
MOVQ $0, r2+64(FP)
NEGQ AX
MOVQ AX, err+72(FP)
CALL runtime·exitsyscall(SB)
RET
ok6:
MOVQ AX, r1+56(FP)
MOVQ DX, r2+64(FP)
MOVQ $0, err+72(FP)
CALL runtime·exitsyscall(SB)
RET
1.2 陷入系統(tǒng)調(diào)用
可以發(fā)現(xiàn),系統(tǒng)調(diào)用最終會進入到runtime.entersyscall函數(shù):
func entersyscall() {
reentersyscall(getcallerpc(), getcallersp())
}
runtime.entersyscall函數(shù)會調(diào)用runtime.reentersyscall:
func reentersyscall(pc, sp uintptr) {
_g_ := getg()
// Disable preemption because during this function g is in Gsyscall status,
// but can have inconsistent g->sched, do not let GC observe it.
_g_.m.locks++
// Entersyscall must not call any function that might split/grow the stack.
// (See details in comment above.)
// Catch calls that might, by replacing the stack guard with something that
// will trip any stack check and leaving a flag to tell newstack to die.
_g_.stackguard0 = stackPreempt
_g_.throwsplit = true
// Leave SP around for GC and traceback.
save(pc, sp) // 保存pc和sp
_g_.syscallsp = sp
_g_.syscallpc = pc
casgstatus(_g_, _Grunning, _Gsyscall)
if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
systemstack(func() {
print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
throw("entersyscall")
})
}
if trace.enabled {
systemstack(traceGoSysCall)
// systemstack itself clobbers g.sched.{pc,sp} and we might
// need them later when the G is genuinely blocked in a
// syscall
save(pc, sp)
}
if atomic.Load(&sched.sysmonwait) != 0 {
systemstack(entersyscall_sysmon)
save(pc, sp)
}
if _g_.m.p.ptr().runSafePointFn != 0 {
// runSafePointFn may stack split if run on this stack
systemstack(runSafePointFn)
save(pc, sp)
}
// 一下解綁P和M
_g_.m.syscalltick = _g_.m.p.ptr().syscalltick
_g_.sysblocktraced = true
pp := _g_.m.p.ptr()
pp.m = 0
_g_.m.oldp.set(pp) // 存儲一下舊P
_g_.m.p = 0
atomic.Store(&pp.status, _Psyscall)
if sched.gcwaiting != 0 {
systemstack(entersyscall_gcwait)
save(pc, sp)
}
_g_.m.locks--
}
可以發(fā)現(xiàn),runtime.reentersyscall除了做一些保障性的工作外,最重要的是做了以下三件事:
- 保存當(dāng)前
goroutine的PC和棧指針SP的內(nèi)容; - 將當(dāng)前
goroutine的狀態(tài)置為_Gsyscall; - 將當(dāng)前P的狀態(tài)置為
_Psyscall,并解綁P和M,讓當(dāng)前M陷入內(nèi)核的系統(tǒng)調(diào)用中,P被釋放,可以被其他找工作的M找到并且執(zhí)行剩下的goroutine。
1.3 從系統(tǒng)調(diào)用恢復(fù)
func exitsyscall() {
_g_ := getg()
_g_.m.locks++ // see comment in entersyscall
if getcallersp() > _g_.syscallsp {
throw("exitsyscall: syscall frame is no longer valid")
}
_g_.waitsince = 0
oldp := _g_.m.oldp.ptr() // 拿到開始存儲的舊P
_g_.m.oldp = 0
if exitsyscallfast(oldp) {
if trace.enabled {
if oldp != _g_.m.p.ptr() || _g_.m.syscalltick != _g_.m.p.ptr().syscalltick {
systemstack(traceGoStart)
}
}
// There's a cpu for us, so we can run.
_g_.m.p.ptr().syscalltick++
// We need to cas the status and scan before resuming...
casgstatus(_g_, _Gsyscall, _Grunning)
...
return
}
...
// Call the scheduler.
mcall(exitsyscall0)
// Scheduler returned, so we're allowed to run now.
// Delete the syscallsp information that we left for
// the garbage collector during the system call.
// Must wait until now because until gosched returns
// we don't know for sure that the garbage collector
// is not running.
_g_.syscallsp = 0
_g_.m.p.ptr().syscalltick++
_g_.throwsplit = false
}其中,exitsyscallfast函數(shù)有以下個分支:
- 如果舊的P還沒有被其他M占用,依舊處于
_Psyscall狀態(tài),那么直接通過wirep函數(shù)獲取這個P,返回true; - 如果舊的P被占用了,那么調(diào)用
exitsyscallfast_pidle去獲取空閑的P來執(zhí)行,返回true; - 如果沒有空閑的P,則返回false;
//go:nosplit
func exitsyscallfast(oldp *p) bool {
_g_ := getg()
// Freezetheworld sets stopwait but does not retake P's.
if sched.stopwait == freezeStopWait {
return false
}
// 如果上一個P沒有被其他M占用,還處于_Psyscall狀態(tài),那么直接通過wirep函數(shù)獲取此P
// Try to re-acquire the last P.
if oldp != nil && oldp.status == _Psyscall && atomic.Cas(&oldp.status, _Psyscall, _Pidle) {
// There's a cpu for us, so we can run.
wirep(oldp)
exitsyscallfast_reacquired()
return true
}
// Try to get any other idle P.
if sched.pidle != 0 {
var ok bool
systemstack(func() {
ok = exitsyscallfast_pidle()
if ok && trace.enabled {
if oldp != nil {
// Wait till traceGoSysBlock event is emitted.
// This ensures consistency of the trace (the goroutine is started after it is blocked).
for oldp.syscalltick == _g_.m.syscalltick {
osyield()
}
}
traceGoSysExit(0)
}
})
if ok {
return true
}
}
return false
}當(dāng)exitsyscallfast函數(shù)返回false后,則會調(diào)用exitsyscall0函數(shù)去處理:
func exitsyscall0(gp *g) {
casgstatus(gp, _Gsyscall, _Grunnable)
dropg() // 因為當(dāng)前m沒有找到p,所以先解開g和m
lock(&sched.lock)
var _p_ *p
if schedEnabled(gp) {
_p_ = pidleget() // 還是嘗試找一下有沒有空閑的p
}
var locked bool
if _p_ == nil { // 如果還是沒有空閑p,那么把g扔到全局隊列去等待調(diào)度
globrunqput(gp)
// Below, we stoplockedm if gp is locked. globrunqput releases
// ownership of gp, so we must check if gp is locked prior to
// committing the release by unlocking sched.lock, otherwise we
// could race with another M transitioning gp from unlocked to
// locked.
locked = gp.lockedm != 0
} else if atomic.Load(&sched.sysmonwait) != 0 {
atomic.Store(&sched.sysmonwait, 0)
notewakeup(&sched.sysmonnote)
}
unlock(&sched.lock)
if _p_ != nil { // 如果找到了空閑p,那么就去執(zhí)行,這個分支永遠不會返回
acquirep(_p_)
execute(gp, false) // Never returns.
}
if locked {
// Wait until another thread schedules gp and so m again.
//
// N.B. lockedm must be this M, as this g was running on this M
// before entersyscall.
stoplockedm()
execute(gp, false) // Never returns.
}
stopm() // 這里還是沒有找到空閑p的條件,停止這個m,因為沒有p,所以m應(yīng)該要開始找工作了
schedule() // Never returns. // 通過schedule函數(shù)進行調(diào)度
}exitsyscall0函數(shù)還是會嘗試找一個空閑的P,沒有的話就把goroutine扔到全局隊列,然后停止這個M,并且調(diào)用schedule函數(shù)等待調(diào)度;如果找到了空閑P,則會利用這個P去執(zhí)行此goroutine。
2. 小結(jié)
通過以上分析,可以發(fā)現(xiàn)goroutine有關(guān)系統(tǒng)調(diào)用的調(diào)度還是比較簡單的:
- 在發(fā)生系統(tǒng)調(diào)用時會將此
goroutine設(shè)置為_Gsyscall狀態(tài); - 并將P設(shè)置為
_Psyscall狀態(tài),并且解綁M和P,使得這個P可以去執(zhí)行其他的goroutine,而M就陷入系統(tǒng)內(nèi)核調(diào)用中了; - 當(dāng)該M從內(nèi)核調(diào)用中恢復(fù)到用戶態(tài)時,會優(yōu)先去獲取原來的舊P,如果該舊P還未被其他M占用,則利用該P繼續(xù)執(zhí)行本
goroutine; - 如果沒有獲取到舊P,那么會嘗試去P的空閑列表獲取一個P來執(zhí)行;
- 如果空閑列表中沒有獲取到P,就會把
goroutine扔到全局隊列中,等到繼續(xù)執(zhí)行。
可以發(fā)現(xiàn),如果系統(tǒng)發(fā)生著很頻繁的系統(tǒng)調(diào)用,很可能會產(chǎn)生很多的M,在IO密集型的場景下,甚至?xí)l(fā)生線程數(shù)超過10000的panic事件。而Go團隊為此也進行了很多努力,下一節(jié)我們將介紹的網(wǎng)絡(luò)輪詢器將介紹,至少在網(wǎng)絡(luò)IO密集型場景,Go SDK是怎么優(yōu)化的。
以上就是Go調(diào)度器學(xué)習(xí)之系統(tǒng)調(diào)用詳解的詳細內(nèi)容,更多關(guān)于Go調(diào)度器 系統(tǒng)調(diào)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go結(jié)構(gòu)體指針引發(fā)的值傳遞思考分析
這篇文章主要為大家介紹了Go結(jié)構(gòu)體指針引發(fā)的值傳遞思考分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12
Go語言利用Unmarshal解析json字符串的實現(xiàn)
本文主要介紹了Go語言利用Unmarshal解析json字符串的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
Golang并發(fā)讀取文件數(shù)據(jù)并寫入數(shù)據(jù)庫的項目實踐
本文主要介紹了Golang并發(fā)讀取文件數(shù)據(jù)并寫入數(shù)據(jù)庫的項目實踐,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Golang實現(xiàn)解析JSON的三種方法總結(jié)
這篇文章主要為大家詳細介紹了Golang實現(xiàn)解析JSON的三種方法,文中的示例代碼講解詳細,對我們學(xué)習(xí)了解JSON有一定幫助,需要的可以參考一下2022-09-09

