Go語言goroutine排程器初始化

G1733發表於2024-06-11

1、排程器初始化

呼叫點:src/runtime/asm_amd64.s:349 -> CALL runtime·schedinit(SB)

runtime/proc.go : 526

func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
   
    //getg函式在原始碼中沒有對應的定義,由編譯器插入類似下面兩行程式碼
    //get_tls(CX)
    //MOVQ g(CX), BX; BX存器裡面現在放的是當前g結構體物件的地址
    _g_ := getg() // _g_ = &g0

    ......

    //設定最多啟動10000個作業系統執行緒,也是最多10000個M
    sched.maxmcount = 10000

    ......
   
    mcommoninit(_g_.m) //初始化m0,因為從前面的程式碼我們知道g0->m = &m0

    ......

    sched.lastpoll = uint64(nanotime())
    procs := ncpu  //系統中有多少核,就建立和初始化多少個p結構體物件
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n //如果環境變數指定了GOMAXPROCS,則建立指定數量的p
    }
    if procresize(procs) != nil {//建立和初始化全域性變數allp
        throw("unknown runnable goroutine during bootstrap")
    }

    ......
}

前面我們已經看到,g0的地址已經被設定到了執行緒本地儲存之中,schedinit透過getg函式(getg函式是編譯器實現的,我們在原始碼中是找不到其定義的)從執行緒本地儲存中獲取當前正在執行的g,這裡獲取出來的是g0,然後呼叫mcommoninit函式對m0(g0.m)進行必要的初始化,對m0初始化完成之後呼叫procresize初始化系統需要用到的p結構體物件,按照go語言官方的說法,p就是processor的意思,它的數量決定了最多可以有都少個goroutine同時並行執行。schedinit函式除了初始化m0和p,還設定了全域性變數sched的maxmcount成員為10000,限制最多可以建立10000個作業系統執行緒出來工作。

這裡我們需要重點關注一下mcommoninit如何初始化m0以及procresize函式如何建立和初始化p結構體物件。首先我們深入到mcommoninit函式中一探究竟。這裡需要注意的是不只是初始化的時候會執行該函式,在程式執行過程中如果建立了工作執行緒,也會執行它,所以我們會在函式中看到加鎖和檢查執行緒數量是否已經超過最大值等相關的程式碼。

runtime/proc.go : 596

func mcommoninit(mp *m) {
    _g_ := getg() //初始化過程中_g_ = g0

    // g0 stack won't make sense for user (and is not necessary unwindable).
    if _g_ != _g_.m.g0 {  //函式呼叫棧traceback,不需要關心
        callers(1, mp.createstack[:])
    }

    lock(&sched.lock)
    if sched.mnext+1 < sched.mnext {
        throw("runtime: thread ID overflow")
    }
    mp.id = sched.mnext
    sched.mnext++
    checkmcount() //檢查已建立系統執行緒是否超過了數量限制(10000)

    //random初始化
    mp.fastrand[0] = 1597334677 * uint32(mp.id)
    mp.fastrand[1] = uint32(cputicks())
    if mp.fastrand[0]|mp.fastrand[1] == 0 {
        mp.fastrand[1] = 1
    }

    //建立用於訊號處理的gsignal,只是簡單的從堆上分配一個g結構體物件,然後把棧設定好就返回了
    mpreinit(mp)
    if mp.gsignal != nil {
        mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
    }

    //把m掛入全域性連結串列allm之中
    // Add to allm so garbage collector doesn't free g->m
    // when it is just in a register or thread-local storage.
    mp.alllink = allm

    // NumCgoCall() iterates over allm w/o schedlock,
    // so we need to publish it safely.
    atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
    unlock(&sched.lock)

    // Allocate memory to hold a cgo traceback if the cgo call crashes.
    if iscgo || GOOS == "solaris" || GOOS == "windows" {
        mp.cgoCallers = new(cgoCallers)
    }
}

從這個函式的原始碼可以看出,這裡並未對m0做什麼關於排程相關的初始化,所以可以簡單的認為這個函式只是把m0放入全域性連結串列allm之中就返回了。

m0完成基本的初始化後,繼續呼叫procresize建立和初始化p結構體物件,在這個函式里面會建立指定個數(根據cpu核數或環境變數確定)的p結構體物件放在全變數allp裡, 並把m0和allp[0]繫結在一起,因此當這個函式執行完成之後就有

m0.p = allp[0]
allp[0].m = &m0

到此m0, g0, 和m需要的p完全關聯在一起了。

初始化allp

下面我們來看procresize函式,考慮到初始化完成之後使用者程式碼還可以透過 GOMAXPROCS()函式呼叫它重新建立和初始化p結構體物件,而在執行過程中再動態的調整p牽涉到的問題比較多,所以這個函式的處理比較複雜,但如果只考慮初始化,相對來說要簡單很多,所以這裡只保留了初始化時會執行的程式碼:

runtime/proc.go : 3902

func procresize(nprocs int32) *p {
    old := gomaxprocs //系統初始化時 gomaxprocs = 0

    ......

    // Grow allp if necessary.
    if nprocs > int32(len(allp)) { //初始化時 len(allp) == 0
        // Synchronize with retake, which could be running
        // concurrently since it doesn't run on a P.
        lock(&allpLock)
        if nprocs <= int32(cap(allp)) {
            allp = allp[:nprocs]
        } else { //初始化時進入此分支,建立allp 切片
            nallp := make([]*p, nprocs)
            // Copy everything up to allp's cap so we
            // never lose old allocated Ps.
            copy(nallp, allp[:cap(allp)])
            allp = nallp
        }
        unlock(&allpLock)
    }

    // initialize new P's
    //迴圈建立nprocs個p並完成基本初始化
    for i := int32(0); i < nprocs; i++ {
        pp := allp[i]
        if pp == nil {
            pp = new(p)//呼叫記憶體分配器從堆上分配一個struct p
            pp.id = i
            pp.status = _Pgcstop
            ......
            atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
        }

        ......
    }

    ......

    _g_ := getg()  // _g_ = g0
    if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {//初始化時m0->p還未初始化,所以不會執行這個分支
        // continue to use the current P
        _g_.m.p.ptr().status = _Prunning
        _g_.m.p.ptr().mcache.prepareForSweep()
    } else {//初始化時執行這個分支
        // release the current P and acquire allp[0]
        if _g_.m.p != 0 {//初始化時這裡不執行
            _g_.m.p.ptr().m = 0
        }
        _g_.m.p = 0
        _g_.m.mcache = nil
        p := allp[0]
        p.m = 0
        p.status = _Pidle
        acquirep(p) //把p和m0關聯起來,其實是這兩個strct的成員相互賦值
        if trace.enabled {
            traceGoStart()
        }
    }
   
    //下面這個for 迴圈把所有空閒的p放入空閒連結串列
    var runnablePs *p
    for i := nprocs - 1; i >= 0; i-- {
        p := allp[i]
        if _g_.m.p.ptr() == p {//allp[0]跟m0關聯了,所以是不能放任
            continue
        }
        p.status = _Pidle
        if runqempty(p) {//初始化時除了allp[0]其它p全部執行這個分支,放入空閒連結串列
            pidleput(p)
        } else {
            ......
        }
    }

    ......
   
    return runnablePs
}

這個函式程式碼比較長,但並不複雜,這裡總結一下這個函式的主要流程:

  1. 使用make([]*p, nprocs)初始化全域性變數allp,即allp = make([]*p, nprocs)

  2. 迴圈建立並初始化nprocs個p結構體物件並依次儲存在allp切片之中

  3. 把m0和allp[0]繫結在一起,即m0.p = allp[0], allp[0].m = m0

  4. 把除了allp[0]之外的所有p放入到全域性變數sched的pidle空閒佇列之中

procresize函式執行完後,排程器相關的初始化工作就基本結束了,這時整個排程器相關的各組成部分之間的聯絡如下圖所示:

原文

相關文章