追溯Go中sysmon的啟動過程

在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都做了些什麼,有機會再一起探討。