Go 語言中無處不在的系統調用

什么是系統調用

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 的定義,系統調用是程序向操作系統內核請求服務的過程,通常包含硬件相關的服務(例如訪問硬盤),創建新進程等。系統調用提供了一個進程和操作系統之間的接口。

Syscall 意義

內核提供用戶空間程序與內核空間進行交互的一套標準接口,這些接口讓用戶態程序能受限訪問硬件設備,比如申請系統資源,操作設備讀寫,創建新進程等。用戶空間發生請求,內核空間負責執行,這些接口便是用戶空間和內核空間共同識別的橋梁,這里提到兩個字“受限”,是由于為了保證內核穩定性,而不能讓用戶空間程序隨意更改系統,必須是內核對外開放的且滿足權限的程序才能調用相應接口。

在用戶空間和內核空間之間,有一個叫做 Syscall (系統調用, system call)的中間層,是連接用戶態和內核態的橋梁。這樣即提高了內核的安全型,也便于移植,只需實現同一套接口即可。如Linux系統,用戶空間通過向內核空間發出 Syscall 指令,產生軟中斷,從而讓程序陷入內核態,執行相應的操作。對于每個系統調用都會有一個對應的系統調用號。

安全性與穩定性:內核駐留在受保護的地址空間,用戶空間程序無法直接執行內核代碼,也無法訪問內核數據,必須通過系統調用。

Go 語言系統調用的實現

系統調用的流程如下


系統調用.png

入口

源碼基于 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 的區別只是參數的個數不一樣,Syscall 和 RawSyscall 的區別是,前者調用了 runtime 庫的進入和退出系統調用函數,通知運行時進行一些操作, 分別是CALL runtime·entersyscall(SB) 和 CALL runtime·exitsyscall(SB)。RawSyscall 只是為了在執行那些一定不會阻塞的系統調用時,能節省兩次對 runtime 的函數調用消耗。假如 RawSyscall 執行了 阻塞的系統調用,由于未調用 entersyscall 函數,當前 G 的狀態還是 running 狀態,只能等待 sysmon 系統監控的 retake 函數來檢測運行時間是否超過閾值(10-20ms),即發送信號搶占調度。

系統調用管理

Go 定義了如下幾種系統調用
1、阻塞式系統調用,注釋類似這種 //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、非阻塞式系統調用,注釋類似這種 //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、包裝版系統調用,系統調用名字太難記了,給封裝換個名

// 源碼位于,src/syscall/syscall_linux.go

func Chmod(path string, mode uint32) (err error) {
    return Fchmodat(_AT_FDCWD, path, mode, 0)
}

4、runtime 庫內部用匯編也封裝了一些系統調用的執行函數,無論阻塞與否都不會調用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

系統調用和調度模型的交互

其實很簡單,就是在發出 SYSCALL 之前調用 runtime.entersyscall(),系統調用返回之后調用 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

    // 保存執行現場,用于 syscall 之后恢復執行
    save(pc, sp)
    _g_.syscallsp = sp
    _g_.syscallpc = pc
    // 修改 G 的狀態 _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")
        })
    }

    // 競態相關,忽略
    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 注意這個會在退出系統調用時快速恢復時使用
    _g_.m.oldp.set(pp)
    _g_.m.p = 0
    // 原子修改 P 的 狀態為 _Psyscall
    atomic.Store(&pp.status, _Psyscall)
    if sched.gcwaiting != 0 {
        systemstack(entersyscall_gcwait)
        save(pc, sp)
    }

    _g_.m.locks--
}

進入系統調用之前大體執行的流程就是這些,保存執行現場,用于 syscall 之后恢復執行,修改 G 和 P 的狀態為_Gsyscall、_Psyscall,解綁 P 和 M,注意這里的 GMP 狀態,Go 發起 syscall 的時候執行該 G 的 M 會阻塞然后被OS調度走,P 什么也不干,sysmon 最慢要10-20ms才能發現這個阻塞。這里在我之前的文章有寫,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 這個在進入系統調用前設置的,順便置為 0
    oldp := _g_.m.oldp.ptr()
    _g_.m.oldp = 0
    // 嘗試快速退出系統調用
    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 的狀態 _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 進入調度循環
    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. 嘗試獲取進入系統調用之前就使用的那個 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 的狀態 為 _Prunning 狀態
        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 的狀態
    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 的狀態
    _p_.status = _Prunning
}

//go:nowritebarrierrec
func exitsyscall0(gp *g) {
    _g_ := getg()

    // 修改 G 的狀態 _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 了 直接執行了 然后還是調度循環
        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 停止放到閑置列表直到有新的任務執行
    schedule() // Never returns.
}

退出系統調用就很單純,各種找 P 來執行 syscall 之后的邏輯,如果實在沒有 P 就修改 G 的狀態為 _Grunnable 放到全局隊列等待調度,順便調用 stopm() 將 M 停止放到閑置列表直到有新的任務執行。

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 包提供的系統調用可以通過 entersyscall 和 exitsyscall 和 runtime 保持互動,讓調度模型充分發揮作用,runtime 包自己實現的 syscall 保留了自己的特權,在執行自己的邏輯的時候,我的 P 不會被調走,這樣保證了在 Go 自己“底層”使用的這些 syscall 返回之后都能被立刻處理。

所以同樣是 epollwait,runtime 用的是不能被別人打斷的,你用的 syscall.EpollWait 那顯然是沒有這種特權的。
個人學習筆記,方便自己復習,有不對的地方歡迎評論哈!

參考資料

wiki
Linux系統調用(syscall)原理
系統調用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容