追溯Go中sysmon的啟動過程
在Go中有一個特殊的線程,它不與其他任何P進(jìn)行綁定。在一個死循環(huán)之中不停的執(zhí)行一系列的監(jiān)控操作,通過這些監(jiān)控操作來更好的服務(wù)于整個Go進(jìn)程,它就是——sysmon監(jiān)控線程。
你可能會好奇它的作用,這里簡單總結(jié)一下:
- 釋放閑置超過5分鐘的span物理內(nèi)存
- 超過2分鐘沒有垃圾回收,強(qiáng)制啟動垃圾回收
- 將長時間沒有處理的netpoll結(jié)果添加到任務(wù)隊列
- 向長時間執(zhí)行的G任務(wù)發(fā)起搶占調(diào)度
- 收回因syscall而長時間阻塞的P
因此可以看出,sysmon線程就像監(jiān)工一樣,監(jiān)控著整個進(jìn)程的狀態(tài)。你會不會跟我一樣好奇這個線程是怎么啟動起來的,一起來追溯吧。
1. 準(zhǔn)備工作
- Go源碼:v1.16.5
- IDE:goland
- 操作系統(tǒng):Centos
- 知識儲備:了解Go啟動過程,見筆者文章《Go程序啟動過程的一次追溯》
Go的啟動過程大概分為三個階段:
- Go程序的引導(dǎo)過程
- runtime的啟動以及初始化過程(runtime.main)
- 執(zhí)行用戶代碼(main.main)
2. sysmon啟動過程追溯
由Go的啟動過程大概可以猜出來,sysmon的啟動過程在runtime的啟動以及初始化過程之中。所以,我們從runtime.main開始一步步的追溯代碼,來尋找sysmon的啟動步驟。
runtime/proc.go
- func main() {
- ...
- if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
- // For runtime_syscall_doAllThreadsSyscall, we
- // register sysmon is not ready for the world to be
- // stopped.
- // !!! 找到了 啟動sysmon的代碼
- // 在系統(tǒng)棧內(nèi)生成一個新的M來啟動sysmon
- atomic.Store(&sched.sysmonStarting, 1)
- systemstack(func() {
- newm(sysmon, nil, -1)
- })
- }
- ...
- }
- // 創(chuàng)建一個新的系統(tǒng)線程
- // Create a new m. It will start off with a call to fn, or else the scheduler.
- // fn needs to be static and not a heap allocated closure.
- // May run with m.p==nil, so write barriers are not allowed.
- //
- // id is optional pre-allocated m ID. Omit by passing -1.
- //go:nowritebarrierrec
- func newm(fn func(), _p_ *p, id int64) {
- // 獲取GPM中M結(jié)構(gòu)體,并進(jìn)行部分字段的初始化
- // allocm方法非常重要?。?!
- // 該方法獲取并初始化M的結(jié)構(gòu)體,還在M里面設(shè)置了系統(tǒng)線程將要執(zhí)行的方法fn,這里是sysmon
- mp := allocm(_p_, fn, id)
- ...
- // M在Go中屬于用戶態(tài)代碼中的一個結(jié)構(gòu)體,跟系統(tǒng)線程是一對一的關(guān)系
- // 每個系統(tǒng)線程怎么執(zhí)行代碼,從哪里開始執(zhí)行,則是由M的結(jié)構(gòu)體中參數(shù)來指明
- // 創(chuàng)建GPM中結(jié)構(gòu)體M結(jié)構(gòu)體之后,開始創(chuàng)建對應(yīng)的底層系統(tǒng)線程
- newm1(mp)
- }
- // 給M分配一個系統(tǒng)線程
- // Allocate a new m unassociated with any thread.
- // Can use p for allocation context if needed.
- // fn is recorded as the new m's m.mstartfn.
- // id is optional pre-allocated m ID. Omit by passing -1.
- //
- // This function is allowed to have write barriers even if the caller
- // isn't because it borrows _p_.
- //
- //go:yeswritebarrierrec
- func allocm(_p_ *p, fn func(), id int64) *m {
- ...
- // 創(chuàng)建新的M,并且進(jìn)行一些初始化操作
- mp := new(m)
- // M 的執(zhí)行方法, 在runtime.mstart()方法中最終調(diào)用fn
- mp.mstartfn = fn
- ...
- }
- // 楷書創(chuàng)建系統(tǒng)線程的邏輯
- func newm1(mp *m) {
- ...
- // ?。?!創(chuàng)建系統(tǒng)線程!??!
- newosproc(mp)
- ...
- }
runtime/os_linux.go
- // 通過clone創(chuàng)建系統(tǒng)線程
- // May run with m.p==nil, so write barriers are not allowed.
- //go:nowritebarrier
- func newosproc(mp *m) {
- ...
- // Disable signals during clone, so that the new thread starts
- // with signals disabled. It will enable them in minit.
- //
- // 注意:
- // 第5個參數(shù) mstart 是在 runtime.mstart
- ret := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(funcPC(mstart)))
- ...
- }
- //go:noescape
- //clone沒有具體方法體,具體實現(xiàn)使用匯編編寫
- func clone(flags int32, stk, mp, gp, fn unsafe.Pointer) int32
clone()函數(shù)在linux系統(tǒng)中,用來創(chuàng)建輕量級進(jìn)程
runtime/sys_linux_arm64.s
- // 注意 這里的void (*fn)(void) 就是 runtime.mstart 方法的地址入口
- //
- // int64 clone(int32 flags, void *stk, M *mp, G *gp, void (*fn)(void));
- TEXT runtime·clone(SB),NOSPLIT|NOFRAME,$0
- ...
- // Copy mp, gp, fn off parent stack for use by child.
- MOVD mp+16(FP), R10
- MOVD gp+24(FP), R11
- MOVD fn+32(FP), R12 // R12寄存器存儲fn的地址
- ...
- // 判斷是父進(jìn)程,則直接返回
- // 子進(jìn)程則跳到 child
- // In parent, return.
- CMP ZR, R0
- BEQ child
- MOVW R0, ret+40(FP)
- RET
- child:
- // In child, on new stack.
- MOVD -32(RSP), R10
- MOVD $1234, R0
- CMP R0, R10
- BEQ good
- ...
- good:
- ...
- CMP $0, R10
- BEQ nog
- CMP $0, R11
- BEQ nog
- ...
- nog:
- // Call fn, 調(diào)用 fn,即 runtime.mstart
- MOVD R12, R0 // R12中存放的是fn的地址
- BL (R0) // BL是一個跳轉(zhuǎn)指令,跳轉(zhuǎn)到fn
- ...
runtime.proc.go
- // mstart是一個M的執(zhí)行入口
- // mstart is the entry-point for new Ms.
- //
- // This must not split the stack because we may not even have stack
- // bounds set up yet.
- //
- // May run during STW (because it doesn't have a P yet), so write
- // barriers are not allowed.
- //
- //go:nosplit
- //go:nowritebarrierrec
- func mstart() {
- ...
- mstart1()
- ...
- }
- // 開始執(zhí)行M的具體方法
- func mstart1() {
- _g_ := getg()
- ...
- // M中mstartfn指向 runtime.sysmon, 即 fn = runtime.sysmon
- if fn := _g_.m.mstartfn; fn != nil {
- // 即:執(zhí)行 runtime.sysmon
- // sysmon方法是一個死循環(huán),所以說執(zhí)行sysmon的線程會一直在這里
- fn()
- }
- ...
- }
最終執(zhí)行的sysmon方法
- // Always runs without a P, so write barriers are not allowed.
- //
- //go:nowritebarrierrec
- func sysmon() {
- ...
- for {
- ...
- // 獲取超過10ms的netpoll結(jié)果
- //
- // poll network if not polled for more than 10ms
- lastpoll := int64(atomic.Load64(&sched.lastpoll))
- if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
- atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
- list := netpoll(0) // non-blocking - returns list of goroutines
- if !list.empty() {
- // Need to decrement number of idle locked M's
- // (pretending that one more is running) before injectglist.
- // Otherwise it can lead to the following situation:
- // injectglist grabs all P's but before it starts M's to run the P's,
- // another M returns from syscall, finishes running its G,
- // observes that there is no work to do and no other running M's
- // and reports deadlock.
- incidlelocked(-1)
- injectglist(&list)
- incidlelocked(1)
- }
- }
- ...
- // 搶奪syscall長時間阻塞的P,向長時間阻塞的P發(fā)起搶占調(diào)度
- //
- // retake P's blocked in syscalls
- // and preempt long running G's
- if retake(now) != 0 {
- idle = 0
- } else {
- idle++
- }
- // 檢查是否需要強(qiáng)制執(zhí)行垃圾回收
- // check if we need to force a GC
- if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
- lock(&forcegc.lock)
- forcegc.idle = 0
- var list gList
- list.push(forcegc.g)
- injectglist(&list)
- unlock(&forcegc.lock)
- }
- ...
- }
- ...
- }
總結(jié)
由以上可知,sysmon線程的創(chuàng)建過程經(jīng)過幾個階段:
- 創(chuàng)建M結(jié)構(gòu)體,對該結(jié)構(gòu)初始化并綁定系統(tǒng)線程將要執(zhí)行的方法sysmon
- 為M創(chuàng)建對應(yīng)的底層系統(tǒng)線程(不同的操作系統(tǒng)生成方式不同)
- 引導(dǎo)系統(tǒng)線程從mstart方法開始執(zhí)行sysmon邏輯(sysmon方法是死循環(huán))
sysmon線程啟動之后就進(jìn)入監(jiān)控整個Go進(jìn)程的邏輯中,至于sysmon都做了些什么,有機(jī)會再一起探討。