什么是系統(tǒng)調用
In computing, a system call (commonly abbreviated to syscall) is the programmatic way in which a computer program requests a service from the kernel of the operating system on which it is executed. This may include hardware-related services (for example, accessing a hard disk drive), creation and execution of new processes, and communication with integral kernel services such as process scheduling. System calls provide an essential interface between a process and the operating system.
以上是 wiki 的定義,系統(tǒng)調用是程序向操作系統(tǒng)內核請求服務的過程搀擂,通常包含硬件相關的服務(例如訪問硬盤),創(chuàng)建新進程等函似。系統(tǒng)調用提供了一個進程和操作系統(tǒng)之間的接口唠雕。
Syscall 意義
內核提供用戶空間程序與內核空間進行交互的一套標準接口,這些接口讓用戶態(tài)程序能受限訪問硬件設備鄙信,比如申請系統(tǒng)資源,操作設備讀寫,創(chuàng)建新進程等脆烟。用戶空間發(fā)生請求,內核空間負責執(zhí)行房待,這些接口便是用戶空間和內核空間共同識別的橋梁邢羔,這里提到兩個字“受限”驼抹,是由于為了保證內核穩(wěn)定性,而不能讓用戶空間程序隨意更改系統(tǒng)拜鹤,必須是內核對外開放的且滿足權限的程序才能調用相應接口框冀。
在用戶空間和內核空間之間,有一個叫做 Syscall (系統(tǒng)調用, system call)的中間層敏簿,是連接用戶態(tài)和內核態(tài)的橋梁明也。這樣即提高了內核的安全型,也便于移植惯裕,只需實現同一套接口即可温数。如Linux系統(tǒng),用戶空間通過向內核空間發(fā)出 Syscall 指令蜻势,產生軟中斷撑刺,從而讓程序陷入內核態(tài),執(zhí)行相應的操作握玛。對于每個系統(tǒng)調用都會有一個對應的系統(tǒng)調用號够傍。
安全性與穩(wěn)定性:內核駐留在受保護的地址空間,用戶空間程序無法直接執(zhí)行內核代碼败许,也無法訪問內核數據王带,必須通過系統(tǒng)調用。
Go 語言系統(tǒng)調用的實現
系統(tǒng)調用的流程如下
入口
源碼基于 go1.15市殷,位于src/syscall/asm_linux_amd64愕撰,都是匯編實現的,從注釋可以看到函數簽名如下
func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
Syscall 和 Syscall6 的區(qū)別只是參數的個數不一樣醋寝,Syscall 和 RawSyscall 的區(qū)別是搞挣,前者調用了 runtime 庫的進入和退出系統(tǒng)調用函數,通知運行時進行一些操作音羞, 分別是CALL runtime·entersyscall(SB) 和 CALL runtime·exitsyscall(SB)囱桨。RawSyscall 只是為了在執(zhí)行那些一定不會阻塞的系統(tǒng)調用時,能節(jié)省兩次對 runtime 的函數調用消耗嗅绰。假如 RawSyscall 執(zhí)行了 阻塞的系統(tǒng)調用舍肠,由于未調用 entersyscall 函數,當前 G 的狀態(tài)還是 running 狀態(tài)窘面,只能等待 sysmon 系統(tǒng)監(jiān)控的 retake 函數來檢測運行時間是否超過閾值(10-20ms),即發(fā)送信號搶占調度翠语。
系統(tǒng)調用管理
Go 定義了如下幾種系統(tǒng)調用
1、阻塞式系統(tǒng)調用财边,注釋類似這種 //sys 肌括,編譯完調用的是 Syscall 或 Syscall6
// 源碼位于,src/syscall/syscall_linux.go
//sys unlinkat(dirfd int, path string, flags int) (err error)
// 源碼位于酣难,src/syscall/zsyscall_linux_amd64.go
func unlinkat(dirfd int, path string, flags int) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(path)
if err != nil {
return
}
_, _, e1 := Syscall(SYS_UNLINKAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags))
if e1 != 0 {
err = errnoErr(e1)
}
return
}
2谍夭、非阻塞式系統(tǒng)調用黑滴,注釋類似這種 //sysnb,編譯完調用的是 RawSyscall 或 RawSyscall6
// 源碼位于紧索,src/syscall/syscall_linux.go
//sysnb EpollCreate1(flag int) (fd int, err error)
// 源碼位于袁辈,src/syscall/zsyscall_linux_amd64.go
func EpollCreate1(flag int) (fd int, err error) {
r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE1, uintptr(flag), 0, 0)
fd = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
3、包裝版系統(tǒng)調用齐板,系統(tǒng)調用名字太難記了吵瞻,給封裝換個名
// 源碼位于,src/syscall/syscall_linux.go
func Chmod(path string, mode uint32) (err error) {
return Fchmodat(_AT_FDCWD, path, mode, 0)
}
4甘磨、runtime 庫內部用匯編也封裝了一些系統(tǒng)調用的執(zhí)行函數,無論阻塞與否都不會調用runtime.entersyscall() 和 runtime.exitsyscall()
// 源碼位于眯停,src/runtime/sys_linux_amd64.s
TEXT runtime·write1(SB),NOSPLIT,$0-28
MOVQ fd+0(FP), DI
MOVQ p+8(FP), SI
MOVL n+16(FP), DX
MOVL $SYS_write, AX
SYSCALL
MOVL AX, ret+24(FP)
RET
TEXT runtime·read(SB),NOSPLIT,$0-28
MOVL fd+0(FP), DI
MOVQ p+8(FP), SI
MOVL n+16(FP), DX
MOVL $SYS_read, AX
SYSCALL
MOVL AX, ret+24(FP)
RET
系統(tǒng)調用和調度模型的交互
其實很簡單济舆,就是在發(fā)出 SYSCALL 之前調用 runtime.entersyscall(),系統(tǒng)調用返回之后調用 runtime.exitsyscall()莺债,通知運行時進行調度滋觉。
entersyscall
// Standard syscall entry used by the go syscall library and normal cgo calls.
// syscall 庫和 cgo 調用的標準入口
// This is exported via linkname to assembly in the syscall package.
//
//go:nosplit
//go:linkname entersyscall
func entersyscall() {
reentersyscall(getcallerpc(), getcallersp())
}
reentersyscall
//go:nosplit
func reentersyscall(pc, sp uintptr) {
_g_ := getg()
// 禁止搶占
_g_.m.locks++
// entersyscall 中不能調用任何會導致棧增長/分裂的函數
// 通過修改 stackguard0 跳過棧檢查 修改 throwsplit 可以使 runtime.newstack() 直接 panic
_g_.stackguard0 = stackPreempt
_g_.throwsplit = true
// 保存執(zhí)行現場,用于 syscall 之后恢復執(zhí)行
save(pc, sp)
_g_.syscallsp = sp
_g_.syscallpc = pc
// 修改 G 的狀態(tài) _Grunning -> _Gsyscall
casgstatus(_g_, _Grunning, _Gsyscall)
// 檢查當前 G 的棧是否異常 比 G 棧的低地址還低 高地址還高 都是異常的 直接 panic
if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
systemstack(func() {
print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
throw("entersyscall")
})
}
// 競態(tài)相關齐邦,忽略
if trace.enabled {
systemstack(traceGoSysCall)
// systemstack itself clobbers g.sched.{pc,sp} and we might
// need them later when the G is genuinely blocked in a
// syscall
save(pc, sp)
}
if atomic.Load(&sched.sysmonwait) != 0 {
systemstack(entersyscall_sysmon)
save(pc, sp)
}
if _g_.m.p.ptr().runSafePointFn != 0 {
// runSafePointFn may stack split if run on this stack
systemstack(runSafePointFn)
save(pc, sp)
}
_g_.m.syscalltick = _g_.m.p.ptr().syscalltick
_g_.sysblocktraced = true
// 解綁 P 和 M 通過設置 pp.m = 0 , _g_.m.p = 0
pp := _g_.m.p.ptr()
pp.m = 0
// 將當前的 P 設置到 m 的 oldp 注意這個會在退出系統(tǒng)調用時快速恢復時使用
_g_.m.oldp.set(pp)
_g_.m.p = 0
// 原子修改 P 的 狀態(tài)為 _Psyscall
atomic.Store(&pp.status, _Psyscall)
if sched.gcwaiting != 0 {
systemstack(entersyscall_gcwait)
save(pc, sp)
}
_g_.m.locks--
}
進入系統(tǒng)調用之前大體執(zhí)行的流程就是這些椎侠,保存執(zhí)行現場,用于 syscall 之后恢復執(zhí)行措拇,修改 G 和 P 的狀態(tài)為_Gsyscall我纪、_Psyscall,解綁 P 和 M丐吓,注意這里的 GMP 狀態(tài)浅悉,Go 發(fā)起 syscall 的時候執(zhí)行該 G 的 M 會阻塞然后被OS調度走,P 什么也不干券犁,sysmon 最慢要10-20ms才能發(fā)現這個阻塞术健。這里在我之前的文章有寫,Go語言調度模型G粘衬、M荞估、P的數量多少合適?稚新,可以看看 GO 調度器的遲鈍勘伺。
exitsyscall
//go:nosplit
//go:nowritebarrierrec
//go:linkname exitsyscall
func exitsyscall() {
_g_ := getg()
// 禁止搶占
_g_.m.locks++ // see comment in entersyscall
// 檢查棧合法
if getcallersp() > _g_.syscallsp {
throw("exitsyscall: syscall frame is no longer valid")
}
_g_.waitsince = 0
// 取出 oldp 這個在進入系統(tǒng)調用前設置的,順便置為 0
oldp := _g_.m.oldp.ptr()
_g_.m.oldp = 0
// 嘗試快速退出系統(tǒng)調用
if exitsyscallfast(oldp) {
if trace.enabled {
if oldp != _g_.m.p.ptr() || _g_.m.syscalltick != _g_.m.p.ptr().syscalltick {
systemstack(traceGoStart)
}
}
// There's a cpu for us, so we can run.
_g_.m.p.ptr().syscalltick++
// We need to cas the status and scan before resuming...原子修改 G 的狀態(tài) _Gsyscall -> _Grunning
casgstatus(_g_, _Gsyscall, _Grunning)
// Garbage collector isn't running (since we are),
// so okay to clear syscallsp.
_g_.syscallsp = 0
_g_.m.locks--
// 恢復 G 的棧信息枷莉, stackguard0 和 throwsplit 是在 entersyscall 那里改的
if _g_.preempt {
// restore the preemption request in case we've cleared it in newstack
_g_.stackguard0 = stackPreempt
} else {
// otherwise restore the real _StackGuard, we've spoiled it in entersyscall/entersyscallblock
_g_.stackguard0 = _g_.stack.lo + _StackGuard
}
_g_.throwsplit = false
if sched.disable.user && !schedEnabled(_g_) {
// Scheduling of this goroutine is disabled.
Gosched()
}
return
}
_g_.sysexitticks = 0
if trace.enabled {
// Wait till traceGoSysBlock event is emitted.
// This ensures consistency of the trace (the goroutine is started after it is blocked).
for oldp != nil && oldp.syscalltick == _g_.m.syscalltick {
osyield()
}
// We can't trace syscall exit right now because we don't have a P.
// Tracing code can invoke write barriers that cannot run without a P.
// So instead we remember the syscall exit time and emit the event
// in execute when we have a P.
_g_.sysexitticks = cputicks()
}
_g_.m.locks--
// Call the scheduler. 切換到 g0 棧 調用 schedule 進入調度循環(huán)
mcall(exitsyscall0)
// Scheduler returned, so we're allowed to run now.
// Delete the syscallsp information that we left for
// the garbage collector during the system call.
// Must wait until now because until gosched returns
// we don't know for sure that the garbage collector
// is not running.
_g_.syscallsp = 0
_g_.m.p.ptr().syscalltick++
_g_.throwsplit = false
}
//go:nosplit
func exitsyscallfast(oldp *p) bool {
_g_ := getg()
// Freezetheworld sets stopwait but does not retake P's.
if sched.stopwait == freezeStopWait {
return false
}
// Try to re-acquire the last P. 嘗試獲取進入系統(tǒng)調用之前就使用的那個 P
if oldp != nil && oldp.status == _Psyscall && atomic.Cas(&oldp.status, _Psyscall, _Pidle) {
// There's a cpu for us, so we can run. 剛好之前的 P 還在(沒有被 sysmon 中被搶占) 就可以直接運行了
// wirep 就是將 M 和 P 綁定娇昙,修改 p 的狀態(tài) 為 _Prunning 狀態(tài)
wirep(oldp)
// 計數,忽略
exitsyscallfast_reacquired()
return true
}
// Try to get any other idle P. 之前 P 沒有獲取到笤妙,就嘗試獲取其他閑置的 P
if sched.pidle != 0 {
var ok bool
systemstack(func() {
// exitsyscallfast_pidle() 會檢查空閑的 P 列表 如果存在就調用 acquirep() -> wirep()冒掌,綁定好 M 和 P 并返回 true
ok = exitsyscallfast_pidle()
if ok && trace.enabled {
if oldp != nil {
// Wait till traceGoSysBlock event is emitted.
// This ensures consistency of the trace (the goroutine is started after it is blocked).
for oldp.syscalltick == _g_.m.syscalltick {
osyield()
}
}
traceGoSysExit(0)
}
})
if ok {
return true
}
}
return false
}
// wirep is the first step of acquirep, which actually associates the
// current M to _p_. This is broken out so we can disallow write
// barriers for this part, since we don't yet have a P.
//
//go:nowritebarrierrec
//go:nosplit
func wirep(_p_ *p) {
_g_ := getg()
if _g_.m.p != 0 {
throw("wirep: already in go")
}
// 檢查 p 不存在 m噪裕,并檢查要獲取的 p 的狀態(tài)
if _p_.m != 0 || _p_.status != _Pidle {
id := int64(0)
if _p_.m != 0 {
id = _p_.m.ptr().id
}
print("wirep: p->m=", _p_.m, "(", id, ") p->status=", _p_.status, "\n")
throw("wirep: invalid p state")
}
// 將 p 綁定到 m,p 和 m 互相引用
_g_.m.p.set(_p_)
_p_.m.set(_g_.m)
// 修改 p 的狀態(tài)
_p_.status = _Prunning
}
//go:nowritebarrierrec
func exitsyscall0(gp *g) {
_g_ := getg()
// 修改 G 的狀態(tài) _Gsyscall -> _Grunnable
casgstatus(gp, _Gsyscall, _Grunnable)
dropg()
lock(&sched.lock)
var _p_ *p
if schedEnabled(_g_) {
_p_ = pidleget()
}
if _p_ == nil {
// 沒有 P 放到全局隊列 等調度
globrunqput(gp)
} else if atomic.Load(&sched.sysmonwait) != 0 {
atomic.Store(&sched.sysmonwait, 0)
notewakeup(&sched.sysmonnote)
}
unlock(&sched.lock)
if _p_ != nil {
// 有 P 就用這個 P 了 直接執(zhí)行了 然后還是調度循環(huán)
acquirep(_p_)
execute(gp, false) // Never returns.
}
if _g_.m.lockedg != 0 {
// Wait until another thread schedules gp and so m again.
// 設置了 LockOsThread 的 g 的特殊邏輯
stoplockedm()
execute(gp, false) // Never returns.
}
stopm() // 將 M 停止放到閑置列表直到有新的任務執(zhí)行
schedule() // Never returns.
}
退出系統(tǒng)調用就很單純股毫,各種找 P 來執(zhí)行 syscall 之后的邏輯膳音,如果實在沒有 P 就修改 G 的狀態(tài)為 _Grunnable 放到全局隊列等待調度,順便調用 stopm() 將 M 停止放到閑置列表直到有新的任務執(zhí)行铃诬。
entersyscallblock
和 entersyscall() 一樣祭陷,已經明確知道是阻塞的 syscall,不用等 sysmon 去搶占 P 直接調用entersyscallblock_handoff -> handoffp(releasep())趣席,直接就把 p 交出來了
// The same as entersyscall(), but with a hint that the syscall is blocking.
//go:nosplit
func entersyscallblock() {
...
systemstack(entersyscallblock_handoff)
...
}
func entersyscallblock_handoff() {
if trace.enabled {
traceGoSysCall()
traceGoSysBlock(getg().m.p.ptr())
}
handoffp(releasep())
}
總結兵志,syscall 包提供的系統(tǒng)調用可以通過 entersyscall 和 exitsyscall 和 runtime 保持互動,讓調度模型充分發(fā)揮作用宣肚,runtime 包自己實現的 syscall 保留了自己的特權想罕,在執(zhí)行自己的邏輯的時候,我的 P 不會被調走霉涨,這樣保證了在 Go 自己“底層”使用的這些 syscall 返回之后都能被立刻處理按价。
所以同樣是 epollwait,runtime 用的是不能被別人打斷的笙瑟,你用的 syscall.EpollWait 那顯然是沒有這種特權的楼镐。
個人學習筆記,方便自己復習往枷,有不對的地方歡迎評論哈框产!