Go 語言中無處不在的系統(tǒng)調用

什么是系統(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)調用的流程如下


系統(tǒng)調用.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 的區(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 那顯然是沒有這種特權的楼镐。
個人學習筆記,方便自己復習往枷,有不對的地方歡迎評論哈框产!

參考資料

wiki
Linux系統(tǒng)調用(syscall)原理
系統(tǒng)調用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市师溅,隨后出現的幾起案子茅信,更是在濱河造成了極大的恐慌,老刑警劉巖墓臭,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蘸鲸,死亡現場離奇詭異,居然都是意外死亡窿锉,警方通過查閱死者的電腦和手機酌摇,發(fā)現死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗡载,“玉大人窑多,你說我怎么就攤上這事⊥莨觯” “怎么了埂息?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我千康,道長享幽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任拾弃,我火速辦了婚禮值桩,結果婚禮上,老公的妹妹穿的比我還像新娘豪椿。我一直安慰自己奔坟,他們只是感情好,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布搭盾。 她就那樣靜靜地躺著咳秉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鸯隅。 梳的紋絲不亂的頭發(fā)上滴某,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音滋迈,去河邊找鬼。 笑死户誓,一個胖子當著我的面吹牛饼灿,可吹牛的內容都是我干的。 我是一名探鬼主播帝美,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼碍彭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悼潭?” 一聲冷哼從身側響起庇忌,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舰褪,沒想到半個月后皆疹,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡占拍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年略就,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晃酒。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡表牢,死狀恐怖,靈堂內的尸體忽然破棺而出贝次,到底是詐尸還是另有隱情崔兴,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站敲茄,受9級特大地震影響位谋,放射性物質發(fā)生泄漏。R本人自食惡果不足惜折汞,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一倔幼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爽待,春花似錦损同、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至何什,卻和暖如春组哩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背处渣。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工伶贰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人罐栈。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓黍衙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荠诬。 傳聞我的和親對象是個殘疾皇子琅翻,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容