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

Linux系統調用

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

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

syscall指令:由於用戶代碼特權級較低,無權訪問須要最高特權級才能訪問的內核地址空間的代碼和數據。因此須要特殊指令,在golang中是syscall。golang

參數設置

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 跟蹤獲得調用路徑:ui

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 函數。咱們忽略中間的具體執行邏輯。this

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

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 的區別:只是參數個數的不一樣,其餘都相同。unix

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

相關文章
相關標籤/搜索