在Go中有一個特殊的執行緒,它不與其他任何P進行繫結。在一個死迴圈之中不停的執行一系列的監控操作,透過這些監控操作來更好的服務於整個Go程序,它就是——sysmon監控執行緒。
你可能會好奇它的作用,這裡簡單總結一下:
釋放閒置超過5分鐘的span物理記憶體
超過2分鐘沒有垃圾回收,強制啟動垃圾回收
將長時間沒有處理的netpoll結果新增到任務佇列
向長時間執行的G任務發起搶佔排程
收回因syscall而長時間阻塞的P
因此可以看出,sysmon執行緒就像監工一樣,監控著整個程序的狀態。你會不會跟我一樣好奇這個執行緒是怎麼啟動起來的,一起來追溯吧。
1。 準備工作
Go原始碼:v1。16。5
IDE:goland
作業系統:Centos
知識儲備:瞭解Go啟動過程,見筆者文章《Go程式啟動過程的一次追溯》
Go的啟動過程大概分為三個階段
1。 Go程式的引導過程
2。 runtime的啟動以及初始化過程(runtime。main)
3。 執行使用者程式碼(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的程式碼 // 在系統棧內生成一個新的M來啟動sysmon atomic。Store(&sched。sysmonStarting, 1) systemstack(func() { newm(sysmon, nil, -1) }) } 。。。}// 建立一個新的系統執行緒// 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:nowritebarrierrecfunc newm(fn func(), _p_ *p, id int64) { // 獲取GPM中M結構體,並進行部分欄位的初始化 // allocm方法非常重要!!! // 該方法獲取並初始化M的結構體,還在M裡面設定了系統執行緒將要執行的方法fn,這裡是sysmon mp := allocm(_p_, fn, id) 。。。 // M在Go中屬於使用者態程式碼中的一個結構體,跟系統執行緒是一對一的關係 // 每個系統執行緒怎麼執行程式碼,從哪裡開始執行,則是由M的結構體中引數來指明 // 建立GPM中結構體M結構體之後,開始建立對應的底層系統執行緒 newm1(mp)}// 給M分配一個系統執行緒// 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:yeswritebarrierrecfunc allocm(_p_ *p, fn func(), id int64) *m { 。。。 // 建立新的M,並且進行一些初始化操作 mp := new(m) // M 的執行方法, 在runtime。mstart()方法中最終呼叫fn mp。mstartfn = fn 。。。}// 楷書建立系統執行緒的邏輯func newm1(mp *m) { 。。。 // !!!建立系統執行緒!!! newosproc(mp) 。。。}
runtime/os_linux。go
// 透過clone建立系統執行緒// May run with m。p==nil, so write barriers are not allowed。//go:nowritebarrierfunc newosproc(mp *m) { 。。。 // Disable signals during clone, so that the new thread starts // with signals disabled。 It will enable them in minit。 // // 注意: // 第5個引數 mstart 是在 runtime。mstart ret := clone(cloneFlags, stk, unsafe。Pointer(mp), unsafe。Pointer(mp。g0), unsafe。Pointer(funcPC(mstart))) 。。。}//go:noescape//clone沒有具體方法體,具體實現使用匯編編寫func clone(flags int32, stk, mp, gp, fn unsafe。Pointer) int32
clone()函式在linux系統中,用來建立輕量級程序
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的地址 。。。 // 判斷是父程序,則直接返回 // 子程序則跳到 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, 呼叫 fn,即 runtime。mstart MOVD R12, R0 // R12中存放的是fn的地址 BL (R0) // BL是一個跳轉指令,跳轉到fn。。。
runtime。proc。go
// mstart是一個M的執行入口// 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:nowritebarrierrecfunc mstart() { 。。。 mstart1() 。。。}// 開始執行M的具體方法func mstart1() { _g_ := getg() 。。。 // M中mstartfn指向 runtime。sysmon, 即 fn = runtime。sysmon if fn := _g_。m。mstartfn; fn != nil { // 即:執行 runtime。sysmon // sysmon方法是一個死迴圈,所以說執行sysmon的執行緒會一直在這裡 fn() } 。。。}
最終執行的sysmon方法
// Always runs without a P, so write barriers are not allowed。////go:nowritebarrierrecfunc sysmon() { 。。。 for { 。。。 // 獲取超過10ms的netpoll結果 // // 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發起搶佔排程 // // retake P‘s blocked in syscalls // and preempt long running G’s if retake(now) != 0 { idle = 0 } else { idle++ } // 檢查是否需要強制執行垃圾回收 // 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) } 。。。 } 。。。}
總結
由以上可知,sysmon執行緒的建立過程經過幾個階段:
建立M結構體,對該結構初始化並繫結系統執行緒將要執行的方法sysmon
為M建立對應的底層系統執行緒(不同的作業系統生成方式不同)
引導系統執行緒從mstart方法開始執行sysmon邏輯(sysmon方法是死迴圈)
sysmon執行緒啟動之後就進入監控整個Go程序的邏輯中,至於sysmon都做了些什麼,有機會再一起探討。