原創文章,歡迎轉載,轉載請註明出處,謝謝。
0. 前言
上一講 介紹了 Go 程式初始化的過程,這一講繼續往下看,進入排程器的初始化過程。
接著上一講的執行過程,省略一些不相關的程式碼,執行到 runtime/asm_amd64.s:rt0_go:343L:
(dlv) si
asm_amd64.s:343 0x45431c* 8b442418 mov eax, dword ptr [rsp+0x18] // [rsp+0x18] 儲存的是 argc 的值,eax = argc
asm_amd64.s:344 0x454320 890424 mov dword ptr [rsp], eax // 將 argc 移到 rsp,[rsp] = argc
asm_amd64.s:345 0x454323 488b442420 mov rax, qword ptr [rsp+0x20] // [rsp+0x20] 儲存的是 argv 的值,rax = [rsp+0x20]
asm_amd64.s:346 0x454328 4889442408 mov qword ptr [rsp+0x8], rax // 將 argv 移到 [rsp+0x8],[rsp+0x8] = argv
asm_amd64.s:347 0x45432d e88e2a0000 call $runtime.args // 呼叫 runtime.args 處理棧上的 argc 和 argv
asm_amd64.s:348 0x454332 e8c9280000 call $runtime.osinit // 呼叫 runtime.osinit 初始化系統核心數
asm_amd64.s:349 0x454337 e8e4290000 call $runtime.schedinit
上述指令呼叫 runtime.args
處理函式引數,接著呼叫 runtime.osinit
初始化系統核心數。runtime.osinit
在 runtime.os_linux.go 中定義:
func osinit() {
ncpu = getproccount()
physHugePageSize = getHugePageSize()
osArchInit()
}
runtime.osinit
主要初始化系統核心數 ncpu
,該核心是邏輯核心數。
接著進入到本文的正題排程器初始化 runtime.schedinit
函式。
1. 排程器初始化
排程器初始化的程式碼在 runtime.schedinit:
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
// step1: 從 TLS 中獲取當前執行執行緒的 goroutine,gp = m0.tls[0] = g0
gp := getg()
// step2: 設定最大執行緒數
sched.maxmcount = 10000
// step3: 初始化執行緒,這裡初始化的是執行緒 m0
mcommoninit(gp.m, -1)
// step4: 呼叫 procresize 建立 Ps
procs := ncpu
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}
省略了函式中不相關的程式碼。
首先,step1 呼叫 getg()
獲取當前執行緒執行的 goroutine。runtime 中隨處可見 getg()
,它是一個內聯的彙編函式,用於直接從當前執行緒的暫存器或棧 TLS 中獲取當前執行緒執行的 goroutine。Go runtime 會為每個執行緒(作業系統執行緒或 Go 執行時執行緒)維護一個 g 的指標,表示當前執行緒正在執行的 goroutine。
直觀的分析,get()
的彙編實現類似於以下內容:
TEXT runtime·getg(SB), NOSPLIT, $0
MOVQ TLS, AX // 從執行緒區域性儲存 (Thread Local Storage) 獲取 g
MOVQ g(AX), BX // 把 g 的值移動到 BX 暫存器
RET
獲取到當前執行 goroutine 之後,在 step3 呼叫 mcommoninit 初始化執行 goroutine 的執行緒:
func mcommoninit(mp *m, id int64) {
// 獲取執行緒的 goroutine,這裡獲取的是 g0
gp := getg()
...
// 對全域性變數 sched 加鎖
lock(&sched.lock)
// 設定 mp 的 id
if id >= 0 {
mp.id = id
} else {
mp.id = mReserveID()
}
// 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)) // allm = &m0
unlock(&sched.lock)
}
mcommoninit
函式會為 mp
設定 id,並且將 mp 和全域性變數 allm 關聯。更新記憶體分佈如下圖:
繼續執行到 step4 procresize
函式,它是 schedinit
的重點:
func procresize(nprocs int32) *p {
// old = gomaxprocs = 0
old := gomaxprocs
if old < 0 || nprocs <= 0 {
throw("procresize: invalid arg")
}
// procresize 會根據新的 nprocs 調整 P 的數量,這裡不做調整,跳過
if nprocs > int32(len(allp)) {
...
}
// 初始化 P
for i := old; i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
}
// 初始化新建立的 P
pp.init(i)
// 將新建立的 P 和全域性變數 allp 關聯
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp)) // allp[i] = &pp
}
...
}
procresize
函式比較長,這裡分段介紹。
首先建立 P,接著呼叫 init
初始化建立的 P:
func (pp *p) init(id int32) {
pp.id = id
pp.status = _Pgcstop // _Pgcstop = 3
...
}
新建立的 P 的 id 是迴圈的索引 i,狀態是 _Pgcstop。接著,將建立的 P 和全域性變數 allp 進行關聯。
接著看 procresize
函式:
func procresize(nprocs int32) *p {
// gp = g0
gp := getg()
// 判斷執行的 goroutine 執行緒是否繫結到 P 上
// 如果有,並且是有效的 P,則繼續繫結;如果沒有,進入 else 邏輯;
if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {
// continue to use the current P
gp.m.p.ptr().status = _Prunning
gp.m.p.ptr().mcache.prepareForSweep()
} else {
...
gp.m.p = 0 // 初始化 gp.m.p = 0
pp := allp[0] // 從 allp 中拿第一個 P
pp.m = 0 // 設定 P 的 m 等於 0
pp.status = _Pidle // 更新 P 的狀態為 _Pidle(0)
acquirep(pp) // 關聯 P 和 m
...
}
}
acquirep()
函式將 P 和當前的執行緒 m 繫結,如下:
func acquirep(pp *p) {
wirep(pp)
...
}
func wirep(pp *p) {
// gp = g0
gp := getg()
// 如果當前執行緒已經繫結了 P 則丟擲異常
if gp.m.p != 0 {
throw("wirep: already in go")
}
// 如果當前 P 已經繫結 m,並且 P 的狀態不等於 _Pidle 則丟擲異常
if pp.m != 0 || pp.status != _Pidle {
id := int64(0)
if pp.m != 0 {
id = pp.m.ptr().id
}
print("wirep: p->m=", pp.m, "(", id, ") p->status=", pp.status, "\n")
throw("wirep: invalid p state")
}
gp.m.p.set(pp) // 繫結當前執行緒 m 的 P 到 pp,這裡是 g0.m.p = allp[0]
pp.m.set(gp.m) // 繫結 P 的 m 到當前執行緒,這裡是 allp[0].m = m0
pp.status = _Prunning // 如果 P 繫結到 m,意味著 P 可以排程 g 線上程上執行了。這裡設定 P 的狀態為 _Prunning(1)
}
根據上述分析,更新記憶體分佈如下圖:
(這裡我們的 nprocs = 3,所以圖中 len(allp) = 3)
到此還沒有結束。繼續看 procresize
:
func procresize(nprocs int32) *p {
...
// runnablePs 儲存可執行的 Ps
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
pp := allp[i]
// 如果 P 是當前執行緒繫結的 P 則跳過
if gp.m.p.ptr() == pp {
continue
}
// 將 P 的狀態設為 _Pidle(0),表示當前 P 是空閒的
pp.status = _Pidle
// runqempty 判斷 P 中的本地執行佇列是否是空佇列
// 如果是空,表明 P 中不存在 goroutine
if runqempty(pp) {
pidleput(pp, now) // 如果是空,將 P 和全域性變數 sched 繫結,執行緒可以透過 sched 找到空閒狀態的 P
} else {
pp.m.set(mget()) // 如果不為空,呼叫 mget() 獲取空閒的執行緒 m。並且將 P.m 繫結到該執行緒
pp.link.set(runnablePs) // 將 P 的 link 指向 runnablePs,表明 P 是可執行的
runnablePs = pp // 將 runnablePs 指向 P,呼叫者透過 runnalbePs 拿到可執行的 P
}
}
...
return runnablePs
}
最後的一段就是對 allp 中沒有繫結到當前執行緒的 P 做處理。首先,設定 P 的狀態為 _Pidle(0),接著呼叫 runqempty 判斷當前執行緒的本地執行佇列是否為空:
// runqempty reports whether pp has no Gs on its local run queue.
// It never returns true spuriously.
func runqempty(pp *p) bool {
// Defend against a race where 1) pp has G1 in runqnext but runqhead == runqtail,
// 2) runqput on pp kicks G1 to the runq, 3) runqget on pp empties runqnext.
// Simply observing that runqhead == runqtail and then observing that runqnext == nil
// does not mean the queue is empty.
for {
head := atomic.Load(&pp.runqhead)
tail := atomic.Load(&pp.runqtail)
runnext := atomic.Loaduintptr((*uintptr)(unsafe.Pointer(&pp.runnext)))
if tail == atomic.Load(&pp.runqtail) {
return head == tail && runnext == 0
}
}
}
這裡 P 中的 runq 儲存的是本地執行佇列。P 的 runqhead 指向 runq 佇列(實際是陣列) 的頭,runqtail 指向 runq 隊尾。
P 中的 runnext 指向下一個執行的 goroutine,它的優先順序是最高的。可以參考 runqempty
中的註釋去看為什麼判斷空佇列要這麼寫。
如果 P 中無可執行的 goroutine,則呼叫 pidleput
將 P 新增到全域性變數 sched 中:
func pidleput(pp *p, now int64) int64 {
...
pp.link = sched.pidle // P.link = shced.pidle
sched.pidle.set(pp) // shced.pidle = P
sched.npidle.Add(1) // sched.npidle 表示空間的 P 數量
...
return now
}
這裡我們的 nprocs = 3
,初始化只有一個 allp[0] 是 _Prunning 的,其餘兩個 Ps 是 _Pidle 狀態。更新記憶體分佈如下圖:
2. 小結
好了,到這裡我們的排程器初始化邏輯基本介紹完了。下一講,將繼續分析 main gouroutine 的建立。