Golang原始碼學習:排程邏輯(四)系統呼叫

蝦敏四把刀發表於2020-05-27

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)。

相關文章