本身涉及到的 go 程式碼 都是基於 go 1.23.0 版本
傳統 OS 執行緒
執行緒是 CPU 的最小排程單位,CPU 透過不斷切換執行緒來實現多工的併發。這會引發一些問題(對於使用者角度):
- 執行緒的建立和銷燬等是昂貴的,因為要不斷在使用者空間和核心空間切換。
- 執行緒的排程是由作業系統負責的,使用者無法控制。而作業系統又可能不知道執行緒已經 IO 阻塞,導致執行緒被排程,浪費 CPU 資源。
- 執行緒的棧是很大的,最新版 linux 預設是 8M,會引起記憶體浪費。
- ......
所以,最簡單的辦法就是複用執行緒,go 中使用的是 M:N 模型,即 M 個 OS 執行緒對應 N 個 任務。
GMP 模型
- G
goroutine, 一個 goroutine 代表一個任務。它有自己的棧空間,預設是 2K,棧空間可以動態增長。方式就是把舊的棧空間複製到新的棧空間,然後釋放舊的棧空間。它的棧是在 heap (對於 OS) 上分配的。
- M
machine, 一個 M 代表一個 OS 執行緒。
- P
processor, 一個 P 代表一個邏輯處理器,它維護了一個 goroutine 佇列。P 會把 goroutine 分配給 M,M 會執行 goroutine。預設的大小為 CPU 核心數。
資料結構
G
結構體在 src/runtime/runtime2.go
中定義,主要介紹一些重要的欄位:
type g struct {
// goroutine 的棧 兩個地址,分別是棧的起始地址和結束地址
stack stack
// 繫結的m
m *m
// goroutine 被排程走儲存的中間狀態
sched gobuf
// goroutine 的狀態
atomicstatus atomic.Uint32
}
type gobuf struct {
sp uintptr // stack pointer 棧指標
pc uintptr // program counter 程式要從哪裡開始執行
g guintptr // goroutine 的 指標
ctxt unsafe.Pointer // 儲存的上下文
ret uintptr // 返回地址
lr uintptr // link register
bp uintptr // base pointer 棧的基地址
}
goroutine 狀態
// defined constants
const (
// 未初始化
_Gidle = iota // 0
// 準備好了 可以被 P 排程
_Grunnable // 1
// 正在執行中
_Grunning // 2
// 正在執行系統呼叫
_Gsyscall // 3
// 正在等待 例如 channel network 等
_Gwaiting // 4
// 沒有被使用 為了相容性
_Gmoribund_unused // 5
// 未使用的 goroutine
// 1. 可能初始化了但是沒有被使用
// 2. 因為會複用未擴棧的 goroutine 所以也可能上次使用完了 還沒繼續使用
_Gdead // 6
// 沒有被使用 為了相容性
_Genqueue_unused // 7
// 棧擴容中
_Gcopystack // 8
// 被搶佔了 等待到 _Gwaiting
_Gpreempted // 9
// 用於 GC 掃描
_Gscan = 0x1000
_Gscanrunnable = _Gscan + _Grunnable // 0x1001
_Gscanrunning = _Gscan + _Grunning // 0x1002
_Gscansyscall = _Gscan + _Gsyscall // 0x1003
_Gscanwaiting = _Gscan + _Gwaiting // 0x1004
_Gscanpreempted = _Gscan + _Gpreempted // 0x1009
)
func malg(stacksize int32) *g {
newg := new(g)
// 分配 runtime 棧
if stacksize >= 0 {
stacksize = round2(stackSystem + stacksize)
systemstack(func() {
newg.stack = stackalloc(uint32(stacksize))
})
newg.stackguard0 = newg.stack.lo + stackGuard
newg.stackguard1 = ^uintptr(0)
*(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
}
return newg
}
狀態流轉:
- 如果 groutine 還未初始化,那麼狀態是
_Gidle
- 初始化完畢是
_Gdead
- 當被呼叫 go func() 時,狀態變為
_Grunnable
- 當被排程到 M 上執行時,狀態變為
_Grunning
- 執行完畢後,狀態變為
_Gdead
- 如果 goroutine 阻塞,狀態變為
_Gwaiting
等待阻塞完畢 狀態再變為_Grunnable
等待排程 - 如果 goroutine 被搶佔 (gc 要 STW 時),狀態變為
_Gpreempted
等待變成_Gwaiting
- 如果發生系統呼叫,狀態變為
_Gsyscall
如果很快完成(10ms) 狀態會變為_Grunning
繼續執行 否則會變為_Grunnable
等待排程 - 如果發生棧擴容,狀態變為
_Gcopystack
等待棧擴容完畢 狀態變為_Grunnable
等待排程
M
結構體在 src/runtime/runtime2.go
中定義,主要介紹一些重要的欄位:
type m struct {
g0 *g
// 暫存器上下文
morebuf gobuf
// tls 是執行緒本地儲存 用於儲存 M 相關的執行緒本地資料 包括當前 G 的引用等重要資訊
tls [tlsSlots]uintptr
// 現在正在執行的 goroutine
curg *g
// 1. 正常執行: p 有效
// 2. 系統呼叫前: p -> oldp
// 3. 系統呼叫中: p == nil
// 4. 系統呼叫返回: 嘗試重新獲取 oldp
p puintptr
nextp puintptr
oldp puintptr
}
M 的建立
func newm(fn func(), pp *p, id int64) {
// 禁止被搶佔
acquirem()
// 分配 M 結構體 並新增列表中
mp := allocm(pp, fn, id)
// 設定 nextP m 會盡量與之繫結
mp.nextp.set(pp)
mp.sigmask = initSigmask
// ...
// 建立 M
newm1(mp)
// 釋放 m 的鎖定狀態
releasem(getg().m)
}
func allocm(pp *p, fn func(), id int64) *m {
// ... 加鎖解鎖
// 如果當前 M 沒有繫結 P,臨時借用傳入的 P
if gp.m.p == 0 {
acquirep(pp) // temporarily borrow p for mallocs in this function
}
// 處理空閒的 M
if sched.freem != nil {
}
// 建立 M
mp := new(m)
mp.mstartfn = fn
mcommoninit(mp, id)
// 對於 cgo 或者特定的作業系統 使用系統分配的棧 否則使用 go runtime 的棧
if iscgo || mStackIsSystemAllocated() {
mp.g0 = malg(-1)
} else {
mp.g0 = malg(16384 * sys.StackGuardMultiplier)
}
mp.g0.m = mp
// 清理臨時借用的 P
if pp == gp.m.p.ptr() {
releasep()
}
return mp
}
// newm1 -> newosproc
func newosproc(mp *m) {
// 棧頂指標
stk := unsafe.Pointer(mp.g0.stack.hi)
// 訊號遮蔽
var oset sigset
sigprocmask(_SIG_SETMASK, &sigset_all, &oset)
// 重試的系統呼叫
ret := retryOnEAGAIN(func() int32 {
// 建立新執行緒 是彙編程式碼 可以找去看看
r := clone(cloneFlags, stk, unsafe.Pointer(mp), unsafe.Pointer(mp.g0), unsafe.Pointer(abi.FuncPCABI0(mstart)))
if r >= 0 {
return 0
}
return -r
})
// 恢復訊號
sigprocmask(_SIG_SETMASK, &oset, nil)
// ...
}
// mstart 啟動 M 是一個彙編程式碼
TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME|NOFRAME,$0
CALL runtime·mstart0(SB) // 呼叫 mstart0
RET // not reached
// mstart0 -> mstart1
func mstart1() {
gp := getg()
if gp != gp.m.g0 {
throw("bad runtime·mstart")
}
// 儲存排程資訊
gp.sched.g = guintptr(unsafe.Pointer(gp))
gp.sched.pc = getcallerpc()
gp.sched.sp = getcallersp()
// 初始化
asminit()
minit()
// 主執行緒初始一些東西
if gp.m == &m0 {
mstartm0()
}
// 排程
schedule()
}
// 初始化訊號 之後搶佔那塊會介紹
// 只有主執行緒需要初始化的原因是 其他執行緒是 clone 而來 而且包括了 _CLONE_SIGHAND 會繼承這些
func mstartm0() {
// ...
initsig(false)
}
g0: 一個特殊的 g 用於執行排程任務 它未使用 go runtime 的 stack 而是使用 os stack
流程大概為使用者態的 g -> g0 排程 -> 使用者的其他 g
P
結構體在 src/runtime/runtime2.go
中定義,主要介紹一些重要的欄位:
type p struct {
// p 的狀態
status uint32
// 分配記憶體使用 每個p 都有的目的是少加鎖
mcache *mcache
// 定長的 queue 用於儲存 goroutine
runqhead uint32
runqtail uint32
runq [256]guintptr
// 下個執行的 goroutine 主要用來快速排程 比如從 chan 讀取資料,把 g 放到 runnext 中 當完成讀取時 直接從 runnext 中取出來執行
runnext guintptr
}
狀態:
const (
// 空閒
_Pidle = iota
// 正在執行中
_Prunning
// 正在執行系統呼叫
_Psyscall
// GC 停止
_Pgcstop
// 死亡狀態
_Pdead
)
P的建立
// 程式啟動
TEXT main(SB),NOSPLIT,$-8
JMP runtime·rt0_go(SB)
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
// ...
CALL runtime·schedinit(SB)
func schedinit() {
// ...
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}
// nprocs 是 process 數 預設是 cpu 個數
func procresize(nprocs int32) *p {
// ...
// 擴容 allp 加入未初始化的 P
if nprocs > int32(len(allp)) {
// 。。。
}
// 初始化所有新建的 P
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))
}
// 處理 p 的狀態
gp := getg()
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 {
// ...
}
// g.m.p is now set, so we no longer need mcache0 for bootstrapping.
mcache0 = nil
// 清理多餘的 P
for i := nprocs; i < old; i++ {
pp := allp[i]
pp.destroy()
// can't free P itself because it can be referenced by an M in syscall
}
// 裁剪 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-- {
pp := allp[i]
if gp.m.p.ptr() == pp {
continue
}
pp.status = _Pidle
if runqempty(pp) {
pidleput(pp, now)
} else {
pp.m.set(mget())
pp.link.set(runnablePs)
runnablePs = pp
}
}
stealOrder.reset(uint32(nprocs))
var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
if old != nprocs {
// Notify the limiter that the amount of procs has changed.
gcCPULimiter.resetCapacity(now, nprocs)
}
return runnablePs
}
func (pp *p) init(id int32) {
// ...
// 分配 cache
if pp.mcache == nil {
if id == 0 {
if mcache0 == nil {
throw("missing mcache?")
}
// Use the bootstrap mcache0. Only one P will get
// mcache0: the one with ID 0.
pp.mcache = mcache0
} else {
pp.mcache = allocmcache()
}
}
// ...
}
func (pp *p) destroy() {
// 枷鎖 確保 stw
assertLockHeld(&sched.lock)
assertWorldStopped()
// 將本地佇列中的 goroutine 移到全域性佇列
for pp.runqhead != pp.runqtail {
pp.runqtail--
gp := pp.runq[pp.runqtail%uint32(len(pp.runq))].ptr()
globrunqputhead(gp)
}
if pp.runnext != 0 {
globrunqputhead(pp.runnext.ptr())
pp.runnext = 0
}
// ...
// 清理 span
systemstack(func() {
for i := 0; i < pp.mspancache.len; i++ {
// Safe to call since the world is stopped.
mheap_.spanalloc.free(unsafe.Pointer(pp.mspancache.buf[i]))
}
pp.mspancache.len = 0
lock(&mheap_.lock)
pp.pcache.flush(&mheap_.pages)
unlock(&mheap_.lock)
})
// 釋放 mcache
freemcache(pp.mcache)
pp.mcache = nil
// ...
}
全域性佇列
type schedt struct {
// 鎖
lock mutex
// m 相關配置
midle muintptr
// ...
// p 相關配置
pidle puintptr // idle p's
// ...
// g 佇列
runq gQueue
runqsize int32
// ...
// G 物件池
gFree struct {
lock mutex
stack gList // Gs with stacks
noStack gList // Gs without stacks
n int32
}
// ...
}
P 的空閒列表: M 獲取 P 的時候拿到
M 的空閒列表: 執行緒的建立於銷燬代價是很大的 為了複用性
排程
go 有三種進行到排程的方式:
- 使用者 goroutine 主動執行 runtime.Gosched() 會把當前 goroutine 放到佇列中等待排程
- 使用者 goroutine 阻塞,例如 channel 讀寫,網路 IO 等 會主動呼叫修改自己狀態並切換到 g0 執行排程任務
- go runtime 中有個 OS 執行緒 (名稱是 sysmon) 檢測到 goroutine 超時(上次執行到現在超過 10ms)那就會給執行緒發訊號 使其切換到 g0 執行排程任務
為什麼 sysmon 使用物理執行緒而不是 goroutine 呢?
因為所有 p 上正在執行的 g 都阻塞住了 比如 for {}
那麼其他的 g 永遠無法執行了包括負責檢測的 sysmon
主動排程
func Gosched() {
checkTimeouts()
mcall(gosched_m)
}
阻塞排程
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceReason traceBlockReason, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waitTraceBlockReason = traceReason
mp.waitTraceSkip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
mcall(park_m)
}
搶佔排程
// m 在 start 的時候會註冊一些訊號處理函式
func initsig(preinit bool) {
for i := uint32(0); i < _NSIG; i++ {
// ...
setsig(i, abi.FuncPCABIInternal(sighandler))
}
}
// sighandler -> doSigPreempt -> asyncPreempt (去彙編程式碼裡找) -> asyncPreempt2
func asyncPreempt2() {
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark)
} else {
mcall(gopreempt_m)
}
gp.asyncSafePoint = false
}
// sysmon 發訊號
// sysmon -> retake -> preemptone -> preemptM
func preemptM(mp *m) {
if mp.signalPending.CompareAndSwap(0, 1) {
// ...
signalM(mp, sigPreempt)
}
}
func signalM(mp *m, sig int) {
tgkill(getpid(), int(mp.procid), sig)
}
// 程式碼在在彙編裡 就是對執行緒傳送訊號 系統呼叫
func tgkill(tgid, tid, sig int)
排程程式碼
可以看到排程程式碼都是透過 mcall 呼叫的,mcall 會切換到 g0 執行排程任務 如果引數的函式不太一樣 但是都是處理一些狀態資訊等,最好都會執行到 schedule 函式。
func schedule() {
// 核心程式碼就是選一個 g 去執行
gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
execute(gp, inheritTime)
}
findRunnable:
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
// Try to schedule a GC worker.
if gcBlackenEnabled != 0 {
gp, tnow := gcController.findRunnableGCWorker(pp, now)
if gp != nil {
return gp, false, true
}
now = tnow
}
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp := globrunqget(pp, 1)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
// local runq
if gp, inheritTime := runqget(pp); gp != nil {
return gp, inheritTime, false
}
// global runq
if sched.runqsize != 0 {
lock(&sched.lock)
gp := globrunqget(pp, 0)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
if netpollinited() && netpollAnyWaiters() && sched.lastpoll.Load() != 0 {
if list, delta := netpoll(0); !list.empty() { // non-blocking
gp := list.pop()
injectglist(&list)
netpollAdjustWaiters(delta)
trace := traceAcquire()
casgstatus(gp, _Gwaiting, _Grunnable)
if trace.ok() {
trace.GoUnpark(gp, 0)
traceRelease(trace)
}
return gp, false, false
}
}
// Spinning Ms: steal work from other Ps.
//
// Limit the number of spinning Ms to half the number of busy Ps.
// This is necessary to prevent excessive CPU consumption when
// GOMAXPROCS>>1 but the program parallelism is low.
if mp.spinning || 2*sched.nmspinning.Load() < gomaxprocs-sched.npidle.Load() {
if !mp.spinning {
mp.becomeSpinning()
}
gp, inheritTime, tnow, w, newWork := stealWork(now)
if gp != nil {
// Successfully stole.
return gp, inheritTime, false
}
if newWork {
// There may be new timer or GC work; restart to
// discover.
goto top
}
now = tnow
if w != 0 && (pollUntil == 0 || w < pollUntil) {
// Earlier timer to wait for.
pollUntil = w
}
}
}
簡化了一下程式碼還是很多 價紹一些這個功能吧
- 優先執行 GC worker
- 每 61 次 從全域性佇列中獲取一個 g 去執行 作用是 防止所有 p 的本地佇列誰都非常多 導致全域性佇列的 g 餓死
- 從本地佇列中獲取一個 g 去執行 有限使用 runnext
- 從全域性佇列中獲取一個 g 去執行 並 load 一些到本地佇列
- 如果有網路 IO 準備好了 就從網路 IO 中獲取一個 g 去執行 (go 中網路 epoll_wait 正常情況下使用的阻塞模式)
- 從其他的 p 中偷取 g 去執行 (cas 保證資料安全)
execute:
func execute(gp *g, inheritTime bool) {
// 修改狀態
casgstatus(gp, _Grunnable, _Grunning)
// 執行
gogo(&gp.sched)
}
我的 arch 是 amd64 所以程式碼在 src/runtime/asm_amd64.s
中
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // 將 gobuf 指標載入到 BX 暫存器
MOVQ gobuf_g(BX), DX // 將 gobuf 中儲存的 g 指標載入到 DX
MOVQ 0(DX), CX // 檢查 g 不為 nil
JMP gogo<>(SB)
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX)
MOVQ DX, g(CX)
MOVQ DX, R14 // set the g register
// 恢復暫存器狀態 (sp ret bp ctxt) 執行
MOVQ gobuf_sp(BX), SP // restore SP
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
// 載入之後 清空 go 的 gobuf 結構體 為了給 gc 節省壓力
MOVQ $0, gobuf_sp(BX)
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
// 跳轉到儲存的 PC (程式執行到哪了) 去執行
MOVQ gobuf_pc(BX), BX
JMP BX
syscall
我的 arch 是 and64 作業系統是 linux 所以程式碼在 src/runtime/asm_linux_amd64.s
中
TEXT ·SyscallNoError(SB),NOSPLIT,$0-48
CALL runtime·entersyscall(SB)
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
MOVQ AX, r1+32(FP)
MOVQ DX, r2+40(FP)
CALL runtime·exitsyscall(SB)
RET
系統呼叫前執行這個函式:
func entersyscall() {
fp := getcallerfp()
reentersyscall(getcallerpc(), getcallersp(), fp)
}
func reentersyscall(pc, sp, bp uintptr) {
// 儲存暫存器資訊
save(pc, sp, bp)
gp.syscallsp = sp
gp.syscallpc = pc
gp.syscallbp = bp
// 修改 g 狀態
casgstatus(gp, _Grunning, _Gsyscall)
if sched.sysmonwait.Load() {
systemstack(entersyscall_sysmon)
save(pc, sp, bp)
}
if gp.m.p.ptr().runSafePointFn != 0 {
// runSafePointFn may stack split if run on this stack
systemstack(runSafePointFn)
save(pc, sp, bp)
}
gp.m.syscalltick = gp.m.p.ptr().syscalltick
pp := gp.m.p.ptr()
// 解綁 P 和 M 並設定 oldP 為當前 P 等待系統呼叫之後重新繫結
pp.m = 0
gp.m.oldp.set(pp)
gp.m.p = 0
// 修改 P 的狀態為 syscall
atomic.Store(&pp.status, _Psyscall)
if sched.gcwaiting.Load() {
systemstack(entersyscall_gcwait)
save(pc, sp, bp)
}
gp.m.locks--
}
系統呼叫後執行這個函式:
func exitsyscall() {
// 如果之前儲存的oldp不為空 那麼重新繫結
if exitsyscallfast(oldp) {
// 設定狀態為 runnable 並重新執行
casgstatus(gp, _Gsyscall, _Grunning)
if sched.disable.user && !schedEnabled(gp) {
// Scheduling of this goroutine is disabled.
Gosched()
}
return
}
// 切換到 g0 執行 exitsyscall0
mcall(exitsyscall0)
}
func exitsyscall0(gp *g) {
// 修改 g 狀態到 _Grunnable 讓重新可排程
casgstatus(gp, _Gsyscall, _Grunnable)
// 刪除 gm 的繫結
dropg()
lock(&sched.lock)
// 找個空閒的 p (狀態為 _Gidle) 與 M 繫結
var pp *p
if schedEnabled(gp) {
pp, _ = pidleget(0)
}
var locked bool
if pp == nil {
// 如果繫結失敗了 直接把 g 放到全域性佇列中
globrunqput(gp)
locked = gp.lockedm != 0
} else if sched.sysmonwait.Load() {
// 如果 sysmon 在等待 那麼喚醒它
sched.sysmonwait.Store(false)
notewakeup(&sched.sysmonnote)
}
unlock(&sched.lock)
// 如果找到 p 了 那麼就去執行
if pp != nil {
acquirep(pp)
execute(gp, false) // Never returns.
}
if locked {
// Wait until another thread schedules gp and so m again.
//
// N.B. lockedm must be this M, as this g was running on this M
// before entersyscall.
stoplockedm()
execute(gp, false) // Never returns.
}
// 如果沒有 P 給我這個 M 繫結的話 那麼把 M 休眠並加入到 schedlink 佇列中 做複用
stopm()
// 直到有新的 g 被排程到這個 M 上
schedule() // Never returns.
}
goroutine 切換通用暫存器問題
我們知道 goroutine 中的 gobuf 中只儲存了 sp pc bp 等暫存器資訊,但是 goroutine 切換的時候還有其他通用暫存器,如果中間丟失會引起結果不一致。那麼 go 中是怎麼儲存的呢?
goroutine 切換大體有兩種情況
- 在編譯階段知道 goroutine 可能交出控制權 比如 讀寫 channel 等待網路 系統呼叫等
- goroutine 被搶佔了 GC 超時等
對於第一種方式,在編譯階段知道後續會使用哪個暫存器和知道在哪裡可能會交出控制權,就會在後續儲存這些暫存器。
func test(a chan int, b, c int) int {
d := <-a
return d + b + c
}
// go tool compile -S main.go 的彙編程式碼
main.test STEXT size=105 args=0x18 locals=0x20 funcid=0x0 align=0x0
0x0000 00000 (./main.go:3) TEXT main.test(SB), ABIInternal, $32-24
// 棧檢查
0x0000 00000 (./main.go:3) CMPQ SP, 16(R14)
0x0004 00004 (./main.go:3) PCDATA $0, $-2
0x0004 00004 (./main.go:3) JLS 68
0x0006 00006 (./main.go:3) PCDATA $0, $-1
0x0006 00006 (./main.go:3) PUSHQ BP
0x0007 00007 (./main.go:3) MOVQ SP, BP
0x000a 00010 (./main.go:3) SUBQ $24, SP
// 除錯使用的
0x000e 00014 (./main.go:3) FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)
0x000e 00014 (./main.go:3) FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
0x000e 00014 (./main.go:3) FUNCDATA $5, main.test.arginfo1(SB)
0x000e 00014 (./main.go:3) FUNCDATA $6, main.test.argliveinfo(SB)
0x000e 00014 (./main.go:3) PCDATA $3, $1
// 把 BX CX 儲存到棧中
0x000e 00014 (./main.go:5) MOVQ BX, main.b+48(SP)
0x0013 00019 (./main.go:5) MOVQ CX, main.c+56(SP)
0x0018 00024 (./main.go:5) PCDATA $3, $2
// 初始化臨時變數 並 chanrecv1 接受 chan 資料
0x0018 00024 (./main.go:4) MOVQ $0, main..autotmp_5+16(SP)
0x0021 00033 (./main.go:4) LEAQ main..autotmp_5+16(SP), BX
0x0026 00038 (./main.go:4) PCDATA $1, $1
0x0026 00038 (./main.go:4) CALL runtime.chanrecv1(SB)
// 恢復接受 chan 之前入棧的暫存器
0x002b 00043 (./main.go:5) MOVQ main.b+48(SP), CX
0x0030 00048 (./main.go:5) ADDQ main..autotmp_5+16(SP), CX
0x0035 00053 (./main.go:5) MOVQ main.c+56(SP), DX
0x003a 00058 (./main.go:5) LEAQ (DX)(CX*1), AX
0x003e 00062 (./main.go:5) ADDQ $24, SP
0x0042 00066 (./main.go:5) POPQ BP
0x0043 00067 (./main.go:5) RET
0x0044 00068 (./main.go:5) NOP
// 處理擴容棧相關的程式碼
0x0044 00068 (./main.go:3) PCDATA $1, $-1
0x0044 00068 (./main.go:3) PCDATA $0, $-2
0x0044 00068 (./main.go:3) MOVQ AX, 8(SP)
0x0049 00073 (./main.go:3) MOVQ BX, 16(SP)
0x004e 00078 (./main.go:3) MOVQ CX, 24(SP)
0x0053 00083 (./main.go:3) CALL runtime.morestack_noctxt(SB)
0x0058 00088 (./main.go:3) PCDATA $0, $-1
0x0058 00088 (./main.go:3) MOVQ 8(SP), AX
0x005d 00093 (./main.go:3) MOVQ 16(SP), BX
0x0062 00098 (./main.go:3) MOVQ 24(SP), CX
0x0067 00103 (./main.go:3) JMP 0
func test2(a int, b, c int) int {
return a + b + c
}
main.test2 STEXT nosplit size=9 args=0x18 locals=0x0 funcid=0x0 align=0x0
0x0000 00000 (/home/zhy/code/test1/main.go:8) TEXT main.test2(SB), NOSPLIT|NOFRAME|ABIInternal, $0-24
0x0000 00000 (/home/zhy/code/test1/main.go:8) FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x0000 00000 (/home/zhy/code/test1/main.go:8) FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x0000 00000 (/home/zhy/code/test1/main.go:8) FUNCDATA $5, main.test2.arginfo1(SB)
0x0000 00000 (/home/zhy/code/test1/main.go:8) FUNCDATA $6, main.test2.argliveinfo(SB)
0x0000 00000 (/home/zhy/code/test1/main.go:8) PCDATA $3, $1
0x0000 00000 (/home/zhy/code/test1/main.go:9) LEAQ (BX)(AX*1), DX
0x0004 00004 (/home/zhy/code/test1/main.go:9) LEAQ (CX)(DX*1), AX
0x0008 00008 (/home/zhy/code/test1/main.go:9) RET
從上方可以看到 test2 函式沒有儲存 BX CX 暫存器,因為編譯器知道這個函式不會交出控制權,所以不需要儲存這些暫存器。如果呼叫函式不做引數入棧的話,只用暫存器的話效能會更好。
那如果是搶佔呢,編譯階段肯定是不知道會在哪被搶佔的,是怎麼恢復要使用的暫存器呢?
處理訊號的邏輯:
func doSigPreempt(gp *g, ctxt *sigctxt) {
// Check if this G wants to be preempted and is safe to
// preempt.
if wantAsyncPreempt(gp) {
if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
// Adjust the PC and inject a call to asyncPreempt.
ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
}
}
}
程式碼在 src/runtime/preempt_amd64.go
中
TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0
PUSHQ BP
MOVQ SP, BP
// Save flags before clobbering them
PUSHFQ
// obj doesn't understand ADD/SUB on SP, but does understand ADJSP
ADJSP $368
// But vet doesn't know ADJSP, so suppress vet stack checking
NOP SP
MOVQ AX, 0(SP)
MOVQ CX, 8(SP)
MOVQ DX, 16(SP)
MOVQ BX, 24(SP)
MOVQ SI, 32(SP)
MOVQ DI, 40(SP)
MOVQ R8, 48(SP)
MOVQ R9, 56(SP)
MOVQ R10, 64(SP)
MOVQ R11, 72(SP)
MOVQ R12, 80(SP)
MOVQ R13, 88(SP)
MOVQ R14, 96(SP)
MOVQ R15, 104(SP)
#ifdef GOOS_darwin
#ifndef hasAVX
CMPB internal∕cpu·X86+const_offsetX86HasAVX(SB), $0
JE 2(PC)
#endif
VZEROUPPER
#endif
MOVUPS X0, 112(SP)
MOVUPS X1, 128(SP)
MOVUPS X2, 144(SP)
MOVUPS X3, 160(SP)
MOVUPS X4, 176(SP)
MOVUPS X5, 192(SP)
MOVUPS X6, 208(SP)
MOVUPS X7, 224(SP)
MOVUPS X8, 240(SP)
MOVUPS X9, 256(SP)
MOVUPS X10, 272(SP)
MOVUPS X11, 288(SP)
MOVUPS X12, 304(SP)
MOVUPS X13, 320(SP)
MOVUPS X14, 336(SP)
MOVUPS X15, 352(SP)
CALL ·asyncPreempt2(SB)
MOVUPS 352(SP), X15
MOVUPS 336(SP), X14
MOVUPS 320(SP), X13
MOVUPS 304(SP), X12
MOVUPS 288(SP), X11
MOVUPS 272(SP), X10
MOVUPS 256(SP), X9
MOVUPS 240(SP), X8
MOVUPS 224(SP), X7
MOVUPS 208(SP), X6
MOVUPS 192(SP), X5
MOVUPS 176(SP), X4
MOVUPS 160(SP), X3
MOVUPS 144(SP), X2
MOVUPS 128(SP), X1
MOVUPS 112(SP), X0
MOVQ 104(SP), R15
MOVQ 96(SP), R14
MOVQ 88(SP), R13
MOVQ 80(SP), R12
MOVQ 72(SP), R11
MOVQ 64(SP), R10
MOVQ 56(SP), R9
MOVQ 48(SP), R8
MOVQ 40(SP), DI
MOVQ 32(SP), SI
MOVQ 24(SP), BX
MOVQ 16(SP), DX
MOVQ 8(SP), CX
MOVQ 0(SP), AX
ADJSP $-368
POPFQ
POPQ BP
RET
這段彙編程式碼很簡單,把各種暫存器儲存到棧中,然後呼叫 asyncPreempt2 函式,這個函式會恢復這些寋存器。
func asyncPreempt2() {
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark)
} else {
mcall(gopreempt_m)
}
gp.asyncSafePoint = false
}
這個程式碼就是開始交給 g0 去執行排程任務,當 goroutine 回來可以繼續執行的時候,會執行恢復暫存器的程式碼。