您好,登錄后才能下訂單哦!
本篇內容主要講解“Golang并發編程之調度器初始化的方法是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Golang并發編程之調度器初始化的方法是什么”吧!
在proc.go
和runtime.go
中有一些很重要的全局的變量,我們將其先列出來:
var ( m0 m // 代表第一個起來的線程,即主線程 g0 g // m0的g0,即 m0.g0 = &g0 allgs []*g // 保存所有的g allglen uintptr // 所有g的長度 allm *m // 保存所有的m gomaxprocs int32 // p的最大個數,默認等于 ncpu ncpu int32 // 程序啟動時,會調用osinit函數獲得此值 sched schedt // 調度器的結構體對象,全局僅此一份 )
程序初始化時,這些全局變量最開始都會被初始化為空值,然后隨著一些初始化函數的作用,這些變量才會開始被賦值。
package main import "fmt" func main() { fmt.Println("hello world") }
在項目根目錄下執行go build -gcflags "-N -l" -o main main.go
,-gcflags "-N -l"
是為了關閉編譯器的優化和函數內聯。然后我們使用gdb
調試代碼:
$ 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
,繼續打斷點b *0x45c220
進入,可以看到,程序代碼的入口就在/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.
進入代碼位置,可以看到,其第8行是調到_rt0_amd64(SB)
函數運行。
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
再全局搜索_rt0_amd64
,可以發現,在asm_amd64.s
中有如下代碼,最終會執行到runtime·rt0_go(SB)
代碼,在asm_amd64.s
中,我們可以找到runtime·rt0_go
代碼的實現,這也是匯編語言。
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
而rt0_go
函數會完成Go
程序啟動的所有初始化工作,這個函數比較長,也比較復雜,我們可以分段來看:
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)
以上一段我們不用深究,第四條指令調整棧(內核主線程棧)頂指針16字節對齊,然后存儲了argc
和argv
數組地址。
注意,此處提及的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
從以上代碼可以看出,系統為主線程m0
的g0
在系統線程的棧空間
開辟了一個大約有64KB大小的棧,地址范圍是SP-64*1024+104 ~ SP
。完成以上指令后,系統棧與g0
的關系大致如圖所示:
LEAQ runtime·m0+m_tls(SB), DI // DI=&m0.tls
CALL runtime·settls(SB) // 調用settls函數設置本地線程存儲
// 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
來設置本地線程存儲,后面一段是驗證設置是否成功。下面我們看下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 // 不會走到這里,這是Android系統的
#else
ADDQ $8, DI // ELF wants to use -8(FS) // 這之后,DI存放的就是m0.tls[1]的地址了
#endif
MOVQ DI, SI // 將DI值賦給SI,即m0.tls[1]的地址,作為系統調用的第二個參數
MOVQ $0x1002, DI // ARCH_SET_FS // DI是第一個參數,0x1002表示操作ARCH_SET_FS,這是個int類型的code,表示設置FS段寄存器為SI寄存器的值,即m0.tls[1]
MOVQ $SYS_arch_prctl, AX // 接下來就是系統調用了
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS 2(PC)
MOVL $0xf1, 0xf1 // crash
RET
上面代碼表明,通過arch_prctl
的系統調用,將FS段寄存器的值設置為了m0.tls[1]
的地址。操作系統在把線程調離CPU運行時會幫我們把所有寄存器中的值保存在內存中,調度線程起來運行時又會從內存中把這些寄存器的值恢復到CPU,這樣,在此之后,工作線程代碼就可以通過FS寄存器來找到m.tls
。從而,就實現了主線程與m0
之間的綁定。
為了讀懂以上代碼,我們需要知道的是,get_tls
和g
是宏實現,在runtime/go_tls.h
中,如下。所以我們知道,get_tls(r)
會將m0.tls
的地址賦給r
;而看了后面的操作,你就會明白,g(r)
則會取出對應的g
地址。
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
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
進行了深刻地綁定
在接下來的代碼中又是一些需求項的檢查,我們直接忽略,看以下代碼:
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) // 處理操作系統傳過來的參數和env,無需關心
CALL runtime·osinit(SB) // linux系統的osinit沒有做很多事,只是賦值了ncpu和物理頁大小
CALL runtime·schedinit(SB) // 調度器的初始化
調度器的初始化是在runtime.schedinit
函數中完成的,是用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函數源碼沒有定義,在編譯的時候由編譯器插入,類似下面的代碼 // get_tls(CX) // MOVQ g(CX), BX _g_ := getg() // 獲取的 _g_ = &g0 if raceenabled { _g_.racectx, raceprocctx0 = raceinit() } sched.maxmcount = 10000 // 操作系統線程個數最多為10000 // a lot of init ... // 初始化m0 mcommoninit(_g_.m, -1) // 一些其他設置,暫時忽略 ... sched.lastpoll = uint64(nanotime()) // p的數目確定 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
進行了一些數據的綁定,但是并沒有真正初始化m0
。所以在schedinit
函數中,我們有兩個重要的工作要做:
通過函數mcommoninit
初始化m0
;
通過函數procresize
初始化p
,初始化出來的p
的數目一般而言是系統的CPU核數(ncpu
),除非用戶設置了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) } // 創建用于信號處理的gsignal,只是簡單的從堆上分配一個g結構體對象,然后把棧設置好就返回了 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) } }
從函數可以看出,這里并未對傳入的m
做有關調度的初始化,可以簡單認為這個函數只是把m0
放到了全局鏈表allm
中后就返回了。
2.4.2 初始化allp
func procresize(nprocs int32) *p { ... old := gomaxprocs // 系統初始化的時候, gomaxprocs=0 if old < 0 || nprocs <= 0 { throw("procresize: invalid arg") } ... // 看看是否需要擴大allp,初始化時len(allp)=0,所以肯定會增長 // 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 { // 初始化時m0.p=0,所以不會進這個分支 // 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關聯起來 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 }
其實,以上代碼可以總結如下:
使用make([]*p, nprocs)
初始化全局變量allp
,即allp = make([]*p, nprocs)
循環創建并初始化nprocs個p結構體對象并依次保存在allp
切片之中
把m0
和allp[0]
綁定在一起,即m0.p = allp[0]
, allp[0].m = m0
把除了allp[0]
之外的所有p
放入到全局變量sched
的pidle
空閑隊列之中
至此,整個調度器中各組件之間的關系如下圖所示:
到此,相信大家對“Golang并發編程之調度器初始化的方法是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。