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的主要功能:
- 因為要開始系統調用,所以當前G和和P的狀態分別變為了 _Gsyscall 和 _Psyscall
- 而P不會等待M,所以P和M相互解綁
- 但是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)
主要功能是:
- 先嘗試綁定oldp,如果不允許,則綁定任意空閑P
- 未能綁定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)。