Golang源碼學習:調度邏輯(一)初始化

本文所使用的Golang為1.14,dlv為1.4.0。

源程式碼

package main

import "fmt"

func main() {
	fmt.Println("Hello")
}

開始調試

root@xiamin:~/study# dlv debug test.go
Type 'help' for list of commands.
(dlv) l
> _rt0_amd64_linux() /root/go/src/runtime/rt0_linux_amd64.s:8 (PC: 0x465800)
Warning: debugging optimized function
     3:	// license that can be found in the LICENSE file.
     4:
     5:	#include "textflag.h"
     6:
     7:	TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
=>   8:		JMP	_rt0_amd64(SB)
     9:
    10:	TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
    11:		JMP	_rt0_amd64_lib(SB)

可以看到最開始是從_rt0_amd64_linux執行,然後直接跳轉到_rt0_amd64。執行si進入_rt0_amd64。

(dlv) si
> _rt0_amd64() /root/go/src/runtime/asm_amd64.s:15 (PC: 0x461c20)
Warning: debugging optimized function
    10:	// _rt0_amd64 is common startup code for most amd64 systems when using
    11:	// internal linking. This is the entry point for the program from the
    12:	// kernel for an ordinary -buildmode=exe program. The stack holds the
    13:	// number of arguments and the C-style argv.
    14:	TEXT _rt0_amd64(SB),NOSPLIT,$-8
=>  15:		MOVQ	0(SP), DI	// argc,將參數個數存入DI
    16:		LEAQ	8(SP), SI	// argv,參數數組的地址存入SI
    17:		JMP	runtime·rt0_go(SB)

繼續執行,runtime.rt0_go() /root/go/src/runtime/asm_amd64.s:89 (PC: 0x461c30)

runtime.rt0_go

runtime.rt0_go中程式碼較多,但我們只關注與調度相關的。

TEXT runtime·rt0_go(SB),NOSPLIT,$0
        // 忽略處理命令行參數相關

        // 為全局變數g0設置一些棧相關的屬性
        MOVQ	$runtime·g0(SB), DI		// 將全局變數g0的存入DI
	LEAQ	(-64*1024+104)(SP), BX		// bx = SP-(64*1024+104),g0的棧幀大小
	MOVQ	BX, g_stackguard0(DI)		// g0.stackguard0 = bx
	MOVQ	BX, g_stackguard1(DI)		// g0.stackguard1 = bx
	MOVQ	BX, (g_stack+stack_lo)(DI)	// g0.stack.lo = bx    棧的低地址
	MOVQ	SP, (g_stack+stack_hi)(DI)	// g0.stack.hi = sp    棧的高地址

        // 忽略獲取cpu型號等相關與cgo初始化

        // 執行緒本地存儲(tls)相關設置
        LEAQ	runtime·m0+m_tls(SB), DI	// di = &m0.tls
	CALL	runtime·settls(SB)		// 設置tls,下面有詳細分析

        // 驗證tls是否生效:通過tls設置一個數值,然後m0.tls[0]獲取,與設置的值對比。
	get_tls(BX)				// 獲取fs地址到bx
	MOVQ	$0x123, g(BX)			// 反編譯後 mov qword ptr fs:[0xfffffff8], 0x123,表示設置fs-8地址中的內容為0x123,其實就是m0.tls[0]的地址。
	MOVQ	runtime·m0+m_tls(SB), AX	// ax = m0.tls[0]
	CMPQ	AX, $0x123			// 比較
	JEQ 2(PC)
	CALL	runtime·abort(SB)

	// m0.tls[0] = &g0;  g0與m0相互綁定
	get_tls(BX)			// 獲取fs地址到bx
	LEAQ	runtime·g0(SB), CX	// cx = &g0
	MOVQ	CX, g(BX)		// m0.tls[0] = &g0
	LEAQ	runtime·m0(SB), AX	// ax = &m0
	MOVQ	CX, m_g0(AX)		// m0.g0 = &g0
	MOVQ	AX, g_m(CX)		// g0.m = &m0

        // 忽略copy argc和argv的程式碼
	CALL	runtime·args(SB)	// 命令行參數相關,暫不關心
	CALL	runtime·osinit(SB)	// 設置全局變數ncpu(cpu個數),全局變數physHugePageSize
	CALL	runtime·schedinit(SB)	// 調度器初始化

        // 調用runtime·newproc創建goroutine,指向函數為runtime·main
	MOVQ	$runtime·mainPC(SB), AX	// runtime·mainPC就是runtime·main
	PUSHQ	AX			// newproc的第二個參數,也就是goroutine要執行的函數。
	PUSHQ	$0			// newproc的第一個參數,表示要傳入runtime·main中參數的大小,此處為0。
	// 創建 main goroutine。非main goroutine也是此方法創建。
	// go編譯會將語句 go foo() 編譯為 runtime·newproc(SB) 並傳入參數。
	CALL	runtime·newproc(SB)	
	POPQ	AX
	POPQ	AX

	CALL	runtime·mstart(SB)	// 進入調度循環
	CALL	runtime·abort(SB)	// mstart應該永不返回,如果返回,則是程式出現錯誤了。
	RET
	MOVQ	$runtime·debugCallV1(SB), AX
	RET
DATA	runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL	runtime·mainPC(SB),RODATA,$8

runtime·settls 設置執行緒本地存儲

runtime·settls中通過調用arch_prctl系統調用設置FS來實現執行緒本地存儲。

通過syscall指令調用系統調用

  • rax存放系統調用號,調用返回值也會放在rax中
  • 當系統調用參數小於等於6個時,參數則須按順序放到暫存器 rdi,rsi,rdx,r10,r8,r9中。
  • 如果系統調用的參數數量大於6個,需將參數保存在一塊連續的記憶體中,並將地址存入rbx中。

新建非m0的m時也會通過runtime·clone調用此函數。

TEXT runtime·settls(SB),NOSPLIT,$32
        // 此時di = &m.tls[0]
	ADDQ	$8, DI			// ELF 需要使用 -8(FS),di+=8,執行完此指令後 di = &m.tls[1]
	MOVQ	DI, SI			// 將地址移動到si中,作為系統調用的第二個參數
	MOVQ	$0x1002, DI		// ARCH_SET_FS表示設置FS,作為系統調用的第一個參數
	MOVQ	$SYS_arch_prctl, AX	// rax存儲系統調用號
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001	// 比較返回結果
	JLS	2(PC)
	MOVL	$0xf1, 0xf1  // crash
	RET

runtime.schedinit 調度初始化

runtime.schedinit中包含了很多功能的初始化,本文暫且分析與調度相關的

func schedinit() {
        
	_g_ := getg()		// 未找到getg()的源程式碼,通過注釋得知getg()返回當前g,此處 _g_為&g0
        ..........
	sched.maxmcount = 10000	// m的最大數量為10000
        ..........
	mcommoninit(_g_.m)	// 此處_g_.m即為m0,對m0的一些初始化工作,下面詳細分析
	..........
	// 獲取要初始化的p的數量,默認與cpu個數相同,如果指定了GOMAXPROCS,則為GOMAXPROCS
	procs := ncpu
	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
		procs = n
	}
        // 初始化allp並為allp中的元素初始化、賦值等,詳見下方
	if procresize(procs) != nil {
		throw("unknown runnable goroutine during bootstrap")
	}
        ..........
}

schedinit->mcommoninit

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 {
		callers(1, mp.createstack[:])    // 調用棧相關
	}

	lock(&sched.lock)
	if sched.mnext+1 < sched.mnext {
		throw("runtime: thread ID overflow")
	}
	mp.id = sched.mnext	// 設置m的id
	sched.mnext++		// 加1,以後分配給下一個m
	checkmcount()		// 檢查非空閑數量的m是否超過了10000

        // rand相關
	mp.fastrand[0] = uint32(int64Hash(uint64(mp.id), fastrandseed))
	mp.fastrand[1] = uint32(int64Hash(uint64(cputicks()), ^fastrandseed))
	if mp.fastrand[0]|mp.fastrand[1] == 0 {
		mp.fastrand[1] = 1
	}

        // 新建一個32k棧大小的g,賦值給m0.gsignal。並使 m0.gsignal.m = *m0
	mpreinit(mp)
	if mp.gsignal != nil {
		mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
	}
        
        // 下面兩步將mp放入全局變數allm中,allm是個鏈表
	mp.alllink = allm
	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)
	}
}

mcommoninit基本上就是做一些m0的初始化。

schedinit->procresize

// 傳入參數nprocs為期望的所有p的個數
func procresize(nprocs int32) *p {
	old := gomaxprocs // gomaxprocs在本方法的末尾會被更改
	if old < 0 || nprocs <= 0 {
		throw("procresize: invalid arg")
	}
	if trace.enabled {
		traceGomaxprocs(nprocs)
	}

	// 更新統計資訊
	now := nanotime()
	if sched.procresizetime != 0 {
		sched.totaltime += int64(old) * (now - sched.procresizetime)
	}
	sched.procresizetime = now

	// 初始化allp
	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,與現存的allp合併,然後將nallp賦值給全局變數allp
			nallp := make([]*p, nprocs)
			copy(nallp, allp[:cap(allp)])
			allp = nallp
		}
		unlock(&allpLock)
	}

	// 初始化新添加到allp中的元素
	for i := old; i < nprocs; i++ {
		pp := allp[i]
		if pp == nil {
			pp = new(p)
		}
		pp.init(i) // 會初始化p結構的屬性:id,status,mcache等
		atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))    // 賦值
	}

	_g_ := getg()
        // 初始化的時候 _g_.m.p = 0 所以走else
	if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {
		// continue to use the current P
		_g_.m.p.ptr().status = _Prunning
		_g_.m.p.ptr().mcache.prepareForSweep()
	} else {
		// 此處省略一些初始化時不會進入的程式碼
                
		_g_.m.p = 0
		_g_.m.mcache = nil
		p := allp[0]
		p.m = 0
		p.status = _Pidle
		acquirep(p)	// m.mcache = p.mcache;p和m相互綁定;p.status = _Prunning。下面有分析。
		if trace.enabled {
			traceGoStart()
		}
	}

	// 釋放未使用的p的資源,比如調用runtime.GOMAXPROCS(num),會調用procresize。
        // num小於當前p的數量時,會執行此處
	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]
		unlock(&allpLock)
	}

        // 將除了當前m綁定p的其餘allp中的都以鏈表形式存入sched.pidle中
	var runnablePs *p
	for i := nprocs - 1; i >= 0; i-- {
		p := allp[i]
		if _g_.m.p.ptr() == p {	// 是否是當前g.m的p
			continue
		}
		p.status = _Pidle
		if runqempty(p) {    
			pidleput(p)	// 將p放入到空閑列表中
		} else {
			p.m.set(mget())
			p.link.set(runnablePs)
			runnablePs = p
		}
	}
        // 這裡會更改gomaxprocs的值
	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
}

總結一下procresize的工作:

  • allp切片中p的數量小於期望p數量時,對allp進行擴容
  • 使用new創建p並調用p.init初始化剛擴容出的,init中為p分配id和mcache
  • 初始化時,調用acquirep使allp[0]與m0相互綁定,並且m.mcache = p.mcache,p.status = _Prunning
  • allp切片中p的數量大於期望p數量時,調用p.destroy釋放未使用的p的資源
  • 將除了allp[0]之外的p狀態設置為_Pidle並加入到全局空閑列表sched.pidle中
  • 更改gomaxprocs值為nprocs

acquirep(p)->wirep(_p_) :acquirep中的主要邏輯就是調用了wirep

func wirep(_p_ *p) {
	_g_ := getg()

	if _g_.m.p != 0 || _g_.m.mcache != nil {
		throw("wirep: already in go")
	}
	if _p_.m != 0 || _p_.status != _Pidle {
		id := int64(0)
		if _p_.m != 0 {
			id = _p_.m.ptr().id
		}
		print("wirep: p->m=", _p_.m, "(", id, ") p->status=", _p_.status, "\n")
		throw("wirep: invalid p state")
	}
	_g_.m.mcache = _p_.mcache	// p的mcache賦值給m.mcache
	_g_.m.p.set(_p_)		// 與下面的一行為 p和m相互綁定
	_p_.m.set(_g_.m)
	_p_.status = _Prunning		// 更改p的狀態
}
Tags: