Golang源碼學習:調度邏輯(四)系統調用

Linux系統調用

概念:系統調用為用戶態進程提供了硬體的抽象介面。並且是用戶空間訪問內核的唯一手段,除異常和陷入外,它們是內核唯一的合法入口。保證系統的安全和穩定。

調用號:在Linux中,每個系統調用被賦予一個獨一無二的系統調用號。當用戶空間的進程執行一個系統調用時,會使用調用號指明系統調用。

syscall指令:因為用戶程式碼特權級較低,無權訪問需要最高特權級才能訪問的內核地址空間的程式碼和數據。所以需要特殊指令,在golang中是syscall。

參數設置

x86-64中通過syscall指令執行系統調用的參數設置

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

Golang中調用系統調用

給個簡單的例子。

package main

import (
	"fmt"
	"os"
)

func main() {
	f, _ := os.Open("read.go")
	buf := make([]byte, 1000)
	f.Read(buf)
	fmt.Printf("%s", buf)
}

通過 IDE 跟蹤得到調用路徑:

os/file.go:(*File).Read() -> os/file_unix.go:(*File).read() -> internal/poll/fd_unix.go:(*File).pfd.Read()

->syscall/syscall_unix.go:Read() -> syscall/zsyscall_linux_amd64.go:read() -> syscall/syscall_unix.go:Syscall()

// syscall/zsyscall_linux_amd64.go
func read(fd int, p []byte) (n int, err error) {
        ......
	r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p)))
        ......
}

可以看到 f.Read(buf) 最終調用了 syscall/syscall_unix.go 文件中的 Syscall 函數。我們忽略中間的具體執行邏輯。

SYS_READ 定義的是 read 的系統調用號,定義在 syscall/zsysnum_linux_amd64.go。

package syscall

const (
	SYS_READ                   = 0
	SYS_WRITE                  = 1
	SYS_OPEN                   = 2
	SYS_CLOSE                  = 3
	SYS_STAT                   = 4
	SYS_FSTAT                  = 5
        ......
)

Syscall系列函數

雖然在上面看到了 Syscall 函數,但執行系統調用的防止並不知道它一個。它們的定義如下:

// src/syscall/syscall_unix.go

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

Syscall 與 Syscall6 的區別:只是參數個數的不同,其他都相同。

Syscall 與 RawSyscall 的區別:Syscall 開始會調用 runtime·entersyscall ,結束時會調用 runtime·exitsyscall;而 RawSyscall 沒有。這意味著 Syscall 是受調度器控制的,RawSyscall不受。因此 RawSyscall 可能會造成阻塞。

下面來看一下源程式碼:

// src/syscall/asm_linux_amd64.s
// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

TEXT ·Syscall(SB),NOSPLIT,$0-56
	CALL	runtime·entersyscall(SB)	// 進入系統調用
        // 準備參數,執行系統調用
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	trap+0(FP), AX			// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001		// 對比返回結果
	JLS	ok
	MOVQ	$-1, r1+32(FP)
	MOVQ	$0, r2+40(FP)
	NEGQ	AX
	MOVQ	AX, err+48(FP)
	CALL	runtime·exitsyscall(SB)		// 退出系統調用
	RET
ok:
	MOVQ	AX, r1+32(FP)
	MOVQ	DX, r2+40(FP)
	MOVQ	$0, err+48(FP)
	CALL	runtime·exitsyscall(SB)		// 退出系統調用
	RET

// func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
TEXT ·Syscall6(SB),NOSPLIT,$0-80
	CALL	runtime·entersyscall(SB)
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	a4+32(FP), R10
	MOVQ	a5+40(FP), R8
	MOVQ	a6+48(FP), R9
	MOVQ	trap+0(FP), AX	// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok6
	MOVQ	$-1, r1+56(FP)
	MOVQ	$0, r2+64(FP)
	NEGQ	AX
	MOVQ	AX, err+72(FP)
	CALL	runtime·exitsyscall(SB)
	RET
ok6:
	MOVQ	AX, r1+56(FP)
	MOVQ	DX, r2+64(FP)
	MOVQ	$0, err+72(FP)
	CALL	runtime·exitsyscall(SB)
	RET

// func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	trap+0(FP), AX	// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok1
	MOVQ	$-1, r1+32(FP)
	MOVQ	$0, r2+40(FP)
	NEGQ	AX
	MOVQ	AX, err+48(FP)
	RET
ok1:
	MOVQ	AX, r1+32(FP)
	MOVQ	DX, r2+40(FP)
	MOVQ	$0, err+48(FP)
	RET

// func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
        ......
	RET

系統調用前函數(entersyscall -> reentersyscall)

在執行系統調用前調用 entersyscall 和 reentersyscall,reentersyscall的主要功能:

  1. 因為要開始系統調用,所以當前G和和P的狀態分別變為了 _Gsyscall 和 _Psyscall
  2. 而P不會等待M,所以P和M相互解綁
  3. 但是M會保留P到 m.oldp 中,在系統調用結束後嘗試與P重新綁定。

本節及後面會涉及到一些之前分析過的函數,這裡給出鏈接,就不重複分析了。

func entersyscall() {
	reentersyscall(getcallerpc(), getcallersp())
}
func reentersyscall(pc, sp uintptr) {
	_g_ := getg()
	_g_.m.locks++
	_g_.stackguard0 = stackPreempt
	_g_.throwsplit = true

	// Leave SP around for GC and traceback.
	save(pc, sp)
	_g_.syscallsp = sp
	_g_.syscallpc = pc
	casgstatus(_g_, _Grunning, _Gsyscall)	// 當前g的狀態由 _Grunning 改為 _Gsyscall
	......
	_g_.m.syscalltick = _g_.m.p.ptr().syscalltick
	_g_.sysblocktraced = true
	_g_.m.mcache = nil
	pp := _g_.m.p.ptr()
	pp.m = 0				// 當前 p 解綁 m
	_g_.m.oldp.set(pp)			// 將當前 p 賦值給 m.oldp。會在 exitsyscall 中用到。
	_g_.m.p = 0				// 當前 m 解綁 p
	atomic.Store(&pp.status, _Psyscall)	// 將當前 p 的狀態改為 _Psyscall
        ......
	_g_.m.locks--
}

系統調用退出後函數(exitsyscall)

主要功能是:

  1. 先嘗試綁定oldp,如果不允許,則綁定任意空閑P
  2. 未能綁定P,則解綁G和M;睡眠工作執行緒;重新調度。
func exitsyscall() {
	_g_ := getg()
        ......
	_g_.waitsince = 0
	oldp := _g_.m.oldp.ptr()	// reentersyscall 函數中存儲的P
	_g_.m.oldp = 0
	if exitsyscallfast(oldp) {	// 嘗試給當前M綁定個P,下有分析。綁定成功後執行 if 中的語句。
		_g_.m.p.ptr().syscalltick++
		casgstatus(_g_, _Gsyscall, _Grunning) // 更改G的狀態
		_g_.syscallsp = 0
		_g_.m.locks--
		if _g_.preempt {
			_g_.stackguard0 = stackPreempt
		} else {
			_g_.stackguard0 = _g_.stack.lo + _StackGuard
		}
		_g_.throwsplit = false
		return
	}
	......
	mcall(exitsyscall0)	// 下有分析
	......
}

嘗試為當前M綁定P(exitsyscallfast)

該函數的主要目的是嘗試為當前M綁定一個P,分為兩種情況。

第一:如果oldp(也就是當前M的元配)存在,並且狀態可以從 _Psyscall 變更到 _Pidle,則此P與M相互綁定,返回true。

第二:oldp條件不允許,則嘗試獲取任何空閑的P並與當前M綁定。具體實現是:exitsyscallfast_pidle 調用 pidleget,不為nil,則調用 acquirep。

func exitsyscallfast(oldp *p) bool {
	_g_ := getg()
	// 嘗試與oldp綁定
	if oldp != nil && oldp.status == _Psyscall && atomic.Cas(&oldp.status, _Psyscall, _Pidle) {
		// There's a cpu for us, so we can run.
		wirep(oldp)
		exitsyscallfast_reacquired()
		return true
	}
	// 嘗試獲取任何空閑的P
	if sched.pidle != 0 {
		var ok bool
		systemstack(func() {
			ok = exitsyscallfast_pidle()
                         ......
		})
		if ok {
			return true
		}
	}
	return false
}

M解綁G,重新調度(mcall(exitsyscall0))

func exitsyscall0(gp *g) {
	_g_ := getg()	// g0
	casgstatus(gp, _Gsyscall, _Grunnable)
	dropg()	// 解綁 gp 與 M
	lock(&sched.lock)
	var _p_ *p
	if schedEnabled(_g_) {
		_p_ = pidleget()
	}
	if _p_ == nil {
		globrunqput(gp)	// 未獲取到空閑P,將gp放入sched.runq
	} else if atomic.Load(&sched.sysmonwait) != 0 {
		atomic.Store(&sched.sysmonwait, 0)
		notewakeup(&sched.sysmonnote)
	}
	unlock(&sched.lock)
	if _p_ != nil {
		acquirep(_p_)
		execute(gp, false) // 有P,與當前M綁定,執行gp,進入調度循環。
	}
	if _g_.m.lockedg != 0 {
		// Wait until another thread schedules gp and so m again.
		stoplockedm()
		execute(gp, false) // Never returns.
	}
	stopm()		// 沒有新工作之前停止M的執行。睡眠工作執行緒。在獲得P並且喚醒之後會繼續執行
	schedule()	// 能走到這裡說明M以獲得P,並且被喚醒,可以尋找一個G,繼續調度了。
}

exitsyscall0 -> stopm

主要內容是將 M 放回 sched.midle,並通過futex系統調用掛起執行緒。

func stopm() {
	_g_ := getg()

	if _g_.m.locks != 0 {
		throw("stopm holding locks")
	}
	if _g_.m.p != 0 {
		throw("stopm holding p")
	}
	if _g_.m.spinning {
		throw("stopm spinning")
	}

	lock(&sched.lock)
	mput(_g_.m)		// M 放回 sched.midle
	unlock(&sched.lock)
	notesleep(&_g_.m.park)	// notesleep->futexsleep->runtime.futex->futex系統調用。
	noteclear(&_g_.m.park)
	acquirep(_g_.m.nextp.ptr())
	_g_.m.nextp = 0
}

總結

在系統調用之前調用:entersyscall

  • 更改P和G的狀態為_Psyscall和_Gsyscall
  • 解綁P和M
  • 將P存入m.oldp

在系統調用之後調用:exitsyscall

  • exitsyscallfast:嘗試為當前M綁定一個P,成功了會return退出exitsyscall。

    • 如果oldp符合條件則wirep
    • 否則嘗試獲取任何空閑的P並與當前M綁定
  • exitsyscall0:進入調度循環

    • 更改gp狀態為_Grunnable
    • dropg解綁gp和M
    • 嘗試獲取p,獲取到則acquirep綁定P和M;execute進入調度循環。
    • 未獲取到則globrunqput將gp放入sched.runq;stopm將M放入sched.midle、掛起工作執行緒;此M被喚醒後schedule進入調度循環。

不太恰當的比喻

背景設定

角色:家長(M)與房子(P)和孩子們(G)。
規則:家長必須要在房子里才能撫養孩子們(運行)。但房子並不固定屬於某個家長,孩子也並不固定屬於某個家長。

出門打獵:

家長張三要帶著一個孩子(m.curg)小明出去打獵(syscall),他們就離家出走(_Gsyscall/_Psyscall)了,家長和房子就互相斷了歸屬,但是他們還留著(m.oldp)房子的地址(天字一號房)。

打獵期間:

這期間其他沒有房子的家長(李四)看到天字一號沒有家長,可能會佔據這個房子,並且撫養房子里的孩子。

打完回家:

家長帶小明打獵回來後,如果天字一號沒有被其他家長佔據,那麼繼續原來的生活(P和M綁定,P/G變為_Prunning/_Grunning)。
如果天字一號被李四佔據,那麼張三會尋找任何一個空閑房子(可能李四也是這麼丟的房子吧)。繼續原來的生活。
但是,如果張三沒有找到任何一個房子,那麼張三就要和小明分離了(dropg),小明被放到孤兒院(globrunqput)等待領養,張三被放在養老院(mput)睡覺(futex系統調用)。

張三的命運:

可能有一天有房子空出來了,張三被放在房子里,然後喚醒,繼續撫養孩子(schedule)。

Tags: