Golang并發(fā)編程之調(diào)度器初始化詳解
0. 簡介
上一篇博客簡單介紹了GMP模型,這一篇我們介紹一下Go調(diào)度器的初始化過程,也就是在main.main函數(shù)運(yùn)行之前所做的事情。
1. 一些全局變量
在proc.go和runtime.go中有一些很重要的全局的變量,我們將其先列出來:
var ( m0 m // 代表第一個(gè)起來的線程,即主線程 g0 g // m0的g0,即 m0.g0 = &g0 allgs []*g // 保存所有的g allglen uintptr // 所有g(shù)的長度 allm *m // 保存所有的m gomaxprocs int32 // p的最大個(gè)數(shù),默認(rèn)等于 ncpu ncpu int32 // 程序啟動(dòng)時(shí),會(huì)調(diào)用osinit函數(shù)獲得此值 sched schedt // 調(diào)度器的結(jié)構(gòu)體對(duì)象,全局僅此一份 )
程序初始化時(shí),這些全局變量最開始都會(huì)被初始化為空值,然后隨著一些初始化函數(shù)的作用,這些變量才會(huì)開始被賦值。
2. main函數(shù)之前
package main
import "fmt"
func main() {
fmt.Println("hello world")
}在項(xiàng)目根目錄下執(zhí)行go build -gcflags "-N -l" -o main main.go,-gcflags "-N -l"是為了關(guān)閉編譯器的優(yōu)化和函數(shù)內(nèi)聯(lián)。然后我們使用gdb調(diào)試代碼:
$ gdb main
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
...
(gdb) info files
Symbols from "/home/chenyiguo/smb_share/go_routine_test/main".
Local exec file:
`/home/chenyiguo/smb_share/go_routine_test/main', file type elf64-x86-64.
Entry point: 0x45c220
0x0000000000401000 - 0x000000000047e357 is .text
0x000000000047f000 - 0x00000000004b3ecc is .rodata
0x00000000004b4060 - 0x00000000004b4538 is .typelink
0x00000000004b4540 - 0x00000000004b4598 is .itablink
0x00000000004b4598 - 0x00000000004b4598 is .gosymtab
0x00000000004b45a0 - 0x000000000050ce10 is .gopclntab
0x000000000050d000 - 0x000000000050d020 is .go.buildinfo
0x000000000050d020 - 0x000000000051d600 is .noptrdata
0x000000000051d600 - 0x0000000000524e10 is .data
0x0000000000524e20 - 0x0000000000553d28 is .bss
0x0000000000553d40 - 0x00000000005590a0 is .noptrbss
0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
可以看到,程序入口地址是0x45c220,繼續(xù)打斷點(diǎn)b *0x45c220進(jìn)入,可以看到,程序代碼的入口就在/usr/local/go/src/runtime/rt0_linux_amd64.s的第8行。
(gdb) b *0x45c220
Breakpoint 1 at 0x45c220: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
進(jìn)入代碼位置,可以看到,其第8行是調(diào)到_rt0_amd64(SB)函數(shù)運(yùn)行。
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
再全局搜索_rt0_amd64,可以發(fā)現(xiàn),在asm_amd64.s中有如下代碼,最終會(huì)執(zhí)行到runtime·rt0_go(SB)代碼,在asm_amd64.s中,我們可以找到runtime·rt0_go代碼的實(shí)現(xiàn),這也是匯編語言。
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
而rt0_go函數(shù)會(huì)完成Go程序啟動(dòng)的所有初始化工作,這個(gè)函數(shù)比較長,也比較復(fù)雜,我們可以分段來看:
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
// copy arguments forward on an even stack
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(5*8), SP // 3args 2auto
ANDQ $~15, SP
MOVQ AX, 24(SP)
MOVQ BX, 32(SP)
以上一段我們不用深究,第四條指令調(diào)整棧(內(nèi)核主線程棧)頂指針16字節(jié)對(duì)齊,然后存儲(chǔ)了argc和argv數(shù)組地址。
2.1 初始化g0
注意,此處提及的g0是全局變量g0,即主線程m0的m0.g0。
// create istack out of the given (operating system) stack. // _cgo_init may update stackguard. MOVQ $runtime·g0(SB), DI // g0的地址存放在DI寄存器 LEAQ (-64*1024+104)(SP), BX // BX=SP-64*1024+104 MOVQ BX, g_stackguard0(DI) // g0.stackguard0=SP-64*1024+104 MOVQ BX, g_stackguard1(DI) // g0.stackguard1=SP-64*1024+104 MOVQ BX, (g_stack+stack_lo)(DI) // g0.stack.lo=SP-64*1024+104 MOVQ SP, (g_stack+stack_hi)(DI) // g0.stack.hi=SP
從以上代碼可以看出,系統(tǒng)為主線程m0的g0在系統(tǒng)線程的??臻g開辟了一個(gè)大約有64KB大小的棧,地址范圍是SP-64*1024+104 ~ SP。完成以上指令后,系統(tǒng)棧與g0的關(guān)系大致如圖所示:

2.2 主線程與m0的綁定
LEAQ runtime·m0+m_tls(SB), DI // DI=&m0.tls
CALL runtime·settls(SB) // 調(diào)用settls函數(shù)設(shè)置本地線程存儲(chǔ)
// store through it, to make sure it works
get_tls(BX)
MOVQ $0x123, g(BX)
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
CALL runtime·abort(SB)
前面兩段代碼,前兩條指令通過runtime·settls來設(shè)置本地線程存儲(chǔ),后面一段是驗(yàn)證設(shè)置是否成功。下面我們看下runtime·settls到底做了什么。
// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
#ifdef GOOS_android
// Android stores the TLS offset in runtime·tls_g.
SUBQ runtime·tls_g(SB), DI // 不會(huì)走到這里,這是Android系統(tǒng)的
#else
ADDQ $8, DI // ELF wants to use -8(FS) // 這之后,DI存放的就是m0.tls[1]的地址了
#endif
MOVQ DI, SI // 將DI值賦給SI,即m0.tls[1]的地址,作為系統(tǒng)調(diào)用的第二個(gè)參數(shù)
MOVQ $0x1002, DI // ARCH_SET_FS // DI是第一個(gè)參數(shù),0x1002表示操作ARCH_SET_FS,這是個(gè)int類型的code,表示設(shè)置FS段寄存器為SI寄存器的值,即m0.tls[1]
MOVQ $SYS_arch_prctl, AX // 接下來就是系統(tǒng)調(diào)用了
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS 2(PC)
MOVL $0xf1, 0xf1 // crash
RET
上面代碼表明,通過arch_prctl的系統(tǒng)調(diào)用,將FS段寄存器的值設(shè)置為了m0.tls[1]的地址。操作系統(tǒng)在把線程調(diào)離CPU運(yùn)行時(shí)會(huì)幫我們把所有寄存器中的值保存在內(nèi)存中,調(diào)度線程起來運(yùn)行時(shí)又會(huì)從內(nèi)存中把這些寄存器的值恢復(fù)到CPU,這樣,在此之后,工作線程代碼就可以通過FS寄存器來找到m.tls。從而,就實(shí)現(xiàn)了主線程與m0之間的綁定。
為了讀懂以上代碼,我們需要知道的是,get_tls和g是宏實(shí)現(xiàn),在runtime/go_tls.h中,如下。所以我們知道,get_tls(r)會(huì)將m0.tls的地址賦給r;而看了后面的操作,你就會(huì)明白,g(r)則會(huì)取出對(duì)應(yīng)的g地址。
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
2.3 m0和g0的綁定
ok:
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ runtime·g0(SB), CX // CX=&g0
MOVQ CX, g(BX) // m0.tls[0]=&g0
LEAQ runtime·m0(SB), AX // AX=&m0
// save m->g0 = g0
MOVQ CX, m_g0(AX) // m0.g0=&g0
// save m0 to g0->m
MOVQ AX, g_m(CX) // g0.m = m0
就這樣,將g0和m0進(jìn)行了深刻地綁定

2.4 調(diào)度器的初始化
在接下來的代碼中又是一些需求項(xiàng)的檢查,我們直接忽略,看以下代碼:
MOVL 24(SP), AX // copy argc // AX=argc
MOVL AX, 0(SP) // argc放到棧頂
MOVQ 32(SP), AX // copy argv // AX=argv
MOVQ AX, 8(SP) // argv放到SP+8的位置
CALL runtime·args(SB) // 處理操作系統(tǒng)傳過來的參數(shù)和env,無需關(guān)心
CALL runtime·osinit(SB) // linux系統(tǒng)的osinit沒有做很多事,只是賦值了ncpu和物理頁大小
CALL runtime·schedinit(SB) // 調(diào)度器的初始化
調(diào)度器的初始化是在runtime.schedinit函數(shù)中完成的,是用go代碼寫的。
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
// a lot of lock init
...
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
// getg函數(shù)源碼沒有定義,在編譯的時(shí)候由編譯器插入,類似下面的代碼
// get_tls(CX)
// MOVQ g(CX), BX
_g_ := getg() // 獲取的 _g_ = &g0
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit()
}
sched.maxmcount = 10000 // 操作系統(tǒng)線程個(gè)數(shù)最多為10000
// a lot of init
...
// 初始化m0
mcommoninit(_g_.m, -1)
// 一些其他設(shè)置,暫時(shí)忽略
...
sched.lastpoll = uint64(nanotime())
// p的數(shù)目確定
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
// 初始化p
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
...
}從上面我們可以看出,雖然在匯編代碼中將m0與g0進(jìn)行了一些數(shù)據(jù)的綁定,但是并沒有真正初始化m0。所以在schedinit函數(shù)中,我們有兩個(gè)重要的工作要做:
- 通過函數(shù)
mcommoninit初始化m0; - 通過函數(shù)
procresize初始化p,初始化出來的p的數(shù)目一般而言是系統(tǒng)的CPU核數(shù)(ncpu),除非用戶設(shè)置了GOMAXPROCS。
2.4.1 初始化m0
// Pre-allocated ID may be passed as 'id', or omitted by passing -1.
func mcommoninit(mp *m, id int64) {
_g_ := getg() // _g_ = &g0
// g0 stack won't make sense for user (and is not necessary unwindable).
if _g_ != _g_.m.g0 {
callers(1, mp.createstack[:])
}
lock(&sched.lock)
if id >= 0 {
mp.id = id
} else {
mp.id = mReserveID() // 初次從mReserveID()獲取到的id=0
}
// random初始化,用于竊取 G
lo := uint32(int64Hash(uint64(mp.id), fastrandseed))
hi := uint32(int64Hash(uint64(cputicks()), ^fastrandseed))
if lo|hi == 0 {
hi = 1
}
// Same behavior as for 1.17.
// TODO: Simplify ths.
if goarch.BigEndian {
mp.fastrand = uint64(lo)<<32 | uint64(hi)
} else {
mp.fastrand = uint64(hi)<<32 | uint64(lo)
}
// 創(chuàng)建用于信號(hào)處理的gsignal,只是簡單的從堆上分配一個(gè)g結(jié)構(gòu)體對(duì)象,然后把棧設(shè)置好就返回了
mpreinit(mp)
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
}
// 把m0掛入全局鏈表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 == "illumos" || GOOS == "windows" {
mp.cgoCallers = new(cgoCallers)
}
}從函數(shù)可以看出,這里并未對(duì)傳入的m做有關(guān)調(diào)度的初始化,可以簡單認(rèn)為這個(gè)函數(shù)只是把m0放到了全局鏈表allm中后就返回了。
2.4.2 初始化allp
func procresize(nprocs int32) *p {
...
old := gomaxprocs // 系統(tǒng)初始化的時(shí)候, gomaxprocs=0
if old < 0 || nprocs <= 0 {
throw("procresize: invalid arg")
}
...
// 看看是否需要擴(kuò)大allp,初始化時(shí)len(allp)=0,所以肯定會(huì)增長
// Grow allp if necessary.
if nprocs > int32(len(allp)) {
// 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 {
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
}
if maskWords <= int32(cap(idlepMask)) {
idlepMask = idlepMask[:maskWords]
timerpMask = timerpMask[:maskWords]
} else {
nidlepMask := make([]uint32, maskWords)
// No need to copy beyond len, old Ps are irrelevant.
copy(nidlepMask, idlepMask)
idlepMask = nidlepMask
ntimerpMask := make([]uint32, maskWords)
copy(ntimerpMask, timerpMask)
timerpMask = ntimerpMask
}
unlock(&allpLock)
}
// 初始化這些P
// initialize new P's
for i := old; i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
}
pp.init(i)
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
_g_ := getg() // _g_ = g0
if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs { // 初始化時(shí)m0.p=0,所以不會(huì)進(jìn)這個(gè)分支
// 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].
//
// We must do this before destroying our current P
// because p.destroy itself has write barriers, so we
// need to do that from a valid P.
if _g_.m.p != 0 {
if trace.enabled {
// Pretend that we were descheduled
// and then scheduled again to keep
// the trace sane.
traceGoSched()
traceProcStop(_g_.m.p.ptr())
}
_g_.m.p.ptr().m = 0
}
_g_.m.p = 0
p := allp[0]
p.m = 0
p.status = _Pidle
acquirep(p) // 把p和m0關(guān)聯(lián)起來
if trace.enabled {
traceGoStart()
}
}
// g.m.p is now set, so we no longer need mcache0 for bootstrapping.
mcache0 = nil
// release resources from unused P's
for i := nprocs; i < old; i++ {
p := allp[i]
p.destroy()
// can't free P itself because it can be referenced by an M in syscall
}
// Trim allp.
if int32(len(allp)) != nprocs {
lock(&allpLock)
allp = allp[:nprocs]
idlepMask = idlepMask[:maskWords]
timerpMask = timerpMask[:maskWords]
unlock(&allpLock)
}
// 將所有的空閑的p放入空閑鏈表
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
p := allp[i]
if _g_.m.p.ptr() == p {
continue
}
p.status = _Pidle
if runqempty(p) {
pidleput(p)
} else {
p.m.set(mget())
p.link.set(runnablePs)
runnablePs = p
}
}
stealOrder.reset(uint32(nprocs))
var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
return runnablePs
}其實(shí),以上代碼可以總結(jié)如下:
- 使用
make([]*p, nprocs)初始化全局變量allp,即allp = make([]*p, nprocs) - 循環(huán)創(chuàng)建并初始化nprocs個(gè)p結(jié)構(gòu)體對(duì)象并依次保存在
allp切片之中 - 把
m0和allp[0]綁定在一起,即m0.p = allp[0],allp[0].m = m0 - 把除了
allp[0]之外的所有p放入到全局變量sched的pidle空閑隊(duì)列之中
至此,整個(gè)調(diào)度器中各組件之間的關(guān)系如下圖所示:

到此這篇關(guān)于Golang并發(fā)編程之調(diào)度器初始化詳解的文章就介紹到這了,更多相關(guān)Golang調(diào)度器初始化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go語言實(shí)現(xiàn)mqtt協(xié)議的實(shí)踐
MQTT是一個(gè)基于客戶端-服務(wù)器的消息發(fā)布/訂閱傳輸協(xié)議。本文主要介紹了go語言實(shí)現(xiàn)mqtt協(xié)議的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
Golang Socket Server自定義協(xié)議的簡單實(shí)現(xiàn)方案
這篇文章主要介紹了Golang Socket Server自定義協(xié)議的簡單實(shí)現(xiàn)方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Go程序的init函數(shù)在什么時(shí)候執(zhí)行
在Go語言中,init?函數(shù)是一個(gè)特殊的函數(shù),它用于執(zhí)行程序的初始化任務(wù),本文主要介紹了Go程序的init函數(shù)在什么時(shí)候執(zhí)行,感興趣的可以了解一下2023-10-10
Go語言學(xué)習(xí)之函數(shù)的定義與使用詳解
這篇文章主要為大家詳細(xì)介紹Go語言中函數(shù)的定義與使用,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語言有一定幫助,需要的可以參考一下2022-04-04
golang?MySQL實(shí)現(xiàn)對(duì)數(shù)據(jù)庫表存儲(chǔ)獲取操作示例
這篇文章主要為大家介紹了golang?MySQL實(shí)現(xiàn)對(duì)數(shù)據(jù)庫表存儲(chǔ)獲取操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Go語言copy()實(shí)現(xiàn)切片復(fù)制
本文主要介紹了Go語言copy()實(shí)現(xiàn)切片復(fù)制,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
Golang中調(diào)用deepseekr1的教程詳解
這篇文章主要為大家詳細(xì)介紹了Golang中調(diào)用deepseekr1的相關(guān)教程,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解下2025-02-02

