開發

曹春暉:談一談 Go 和 Syscall

桔妹導讀:syscall 是語言與系統交互的唯一手段,理解 Go 語言中的 syscall,本文可以幫助讀者理解 Go 語言怎么與系統打交道,同時了解底層 runtime 在 syscall 優化方面的一些小心思,從而更為深入地理解 Go 語言。

—————

閱讀索引

  • 概念
  • 入口
  • 系統調用管理
  • runtime 中的 SYSCALL
  • 和調度的交互
    • entersyscall
    • exitsyscallfast
    • exitsyscall
    • entersyscallblock
    • entersyscallblock_handoff
    • entersyscall_sysmon
    • entersyscall_gcwait
  • 總結

概念

入口

syscall 有下面幾個入口,在 syscall/asm_linux_amd64.s 中。

1func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
2
3func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
4
5func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
6
7func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
8

這些函數的實現都是匯編,按照 linux 的 syscall 調用規范,我們只要在匯編中把參數依次傳入寄存器,并調用 SYSCALL 指令即可進入內核處理邏輯,系統調用執行完畢之后,返回值放在 RAX 中:

Syscall 和 Syscall6 的區別只有傳入參數不一樣:

 1// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
2TEXT ·Syscall(SB),NOSPLIT,$0-56
3    CALL    runtime·entersyscall(SB)
4    MOVQ    a1+8(FP), DI
5    MOVQ    a2+16(FP), SI
6    MOVQ    a3+24(FP), DX
7    MOVQ    $0, R10
8    MOVQ    $0, R8
9    MOVQ    $0, R9
10    MOVQ    trap+0(FP), AX    // syscall entry
11    SYSCALL
12    // 0xfffffffffffff001 是 linux MAX_ERRNO 取反 轉無符號,http://lxr.free-electrons.com/source/include/linux/err.h#L17
13    CMPQ    AX, $0xfffffffffffff001
14    JLS    ok
15    MOVQ    $-1, r1+32(FP)
16    MOVQ    $0, r2+40(FP)
17    NEGQ    AX
18    MOVQ    AX, err+48(FP)
19    CALL    runtime·exitsyscall(SB)
20    RET
21ok:
22    MOVQ    AX, r1+32(FP)
23    MOVQ    DX, r2+40(FP)
24    MOVQ    $0, err+48(FP)
25    CALL    runtime·exitsyscall(SB)
26    RET
27
28// func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
29TEXT ·Syscall6(SB),NOSPLIT,$0-80
30    CALL    runtime·entersyscall(SB)
31    MOVQ    a1+8(FP), DI
32    MOVQ    a2+16(FP), SI
33    MOVQ    a3+24(FP), DX
34    MOVQ    a4+32(FP), R10
35    MOVQ    a5+40(FP), R8
36    MOVQ    a6+48(FP), R9
37    MOVQ    trap+0(FP), AX    // syscall entry
38    SYSCALL
39    CMPQ    AX, $0xfffffffffffff001
40    JLS    ok6
41    MOVQ    $-1, r1+56(FP)
42    MOVQ    $0, r2+64(FP)
43    NEGQ    AX
44    MOVQ    AX, err+72(FP)
45    CALL    runtime·exitsyscall(SB)
46    RET
47ok6:
48    MOVQ    AX, r1+56(FP)
49    MOVQ    DX, r2+64(FP)
50    MOVQ    $0, err+72(FP)
51    CALL    runtime·exitsyscall(SB)
52    RET

兩個函數沒什么大區別,為啥不用一個呢?個人猜測,Go 的函數參數都是棧上傳入,可能是為了節省一點??臻g。。在正常的 Syscall 操作之前會通知 runtime,接下來我要進行 syscall 操作了 runtime·entersyscall ,退出時會調用 runtime·exitsyscall 。

 1// func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
2TEXT ·RawSyscall(SB),NOSPLIT,$0-56
3    MOVQ    a1+8(FP), DI
4    MOVQ    a2+16(FP), SI
5    MOVQ    a3+24(FP), DX
6    MOVQ    $0, R10
7    MOVQ    $0, R8
8    MOVQ    $0, R9
9    MOVQ    trap+0(FP), AX    // syscall entry
10    SYSCALL
11    CMPQ    AX, $0xfffffffffffff001
12    JLS    ok1
13    MOVQ    $-1, r1+32(FP)
14    MOVQ    $0, r2+40(FP)
15    NEGQ    AX
16    MOVQ    AX, err+48(FP)
17    RET
18ok1:
19    MOVQ    AX, r1+32(FP)
20    MOVQ    DX, r2+40(FP)
21    MOVQ    $0, err+48(FP)
22    RET
23
24// func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
25TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
26    MOVQ    a1+8(FP), DI
27    MOVQ    a2+16(FP), SI
28    MOVQ    a3+24(FP), DX
29    MOVQ    a4+32(FP), R10
30    MOVQ    a5+40(FP), R8
31    MOVQ    a6+48(FP), R9
32    MOVQ    trap+0(FP), AX    // syscall entry
33    SYSCALL
34    CMPQ    AX, $0xfffffffffffff001
35    JLS    ok2
36    MOVQ    $-1, r1+56(FP)
37    MOVQ    $0, r2+64(FP)
38    NEGQ    AX
39    MOVQ    AX, err+72(FP)
40    RET
41ok2:
42    MOVQ    AX, r1+56(FP)
43    MOVQ    DX, r2+64(FP)
44    MOVQ    $0, err+72(FP)
45    RET

RawSyscall 和 Syscall 的區別也非常微小,就只是在進入 Syscall 和退出的時候沒有通知 runtime,這樣 runtime 理論上是沒有辦法通過調度把這個 g 的 m 的 p 調度走的,所以如果用戶代碼使用了 RawSyscall 來做一些阻塞的系統調用,是有可能阻塞其它的 g 的,下面是官方開發的原話:

Yes, if you call RawSyscall you may block other goroutines from running. The system monitor may start them up after a while, but I think there are cases where it won’t. I would say that Go programs should always call Syscall. RawSyscall exists to make it slightly more efficient to call system calls that never block, such as getpid. But it’s really an internal mechanism.

 1// func gettimeofday(tv *Timeval) (err uintptr)
2TEXT ·gettimeofday(SB),NOSPLIT,$0-16
3    MOVQ    tv+0(FP), DI
4    MOVQ    $0, SI
5    MOVQ    runtime·__vdso_gettimeofday_sym(SB), AX
6    CALL    AX
7
8    CMPQ    AX, $0xfffffffffffff001
9    JLS    ok7
10    NEGQ    AX
11    MOVQ    AX, err+8(FP)
12    RET
13ok7:
14    MOVQ    $0, err+8(FP)
15    RET

系統調用管理

先是系統調用的定義文件:

1/syscall/syscall_linux.go

可以把系統調用分為三類:

  • 阻塞系統調用
  • 非阻塞系統調用
  • wrapped 系統調用

阻塞系統調用會定義成下面這樣的形式:

1//sys   Madvise(b []byte, advice int) (err error)

非阻塞系統調用:

1//sysnb    EpollCreate(size int) (fd int, err error)

然后,根據這些注釋,mksyscall.pl 腳本會生成對應的平臺的具體實現。mksyscall.pl 是一段 perl 腳本,感興趣的同學可以自行查看,這里就不再贅述了。

看看阻塞和非阻塞的系統調用的生成結果:

 1func Madvise(b []byte, advice int) (err error) {
2    var _p0 unsafe.Pointer
3    if len(b) > 0 {
4        _p0 = unsafe.Pointer(&b[0])
5    } else {
6        _p0 = unsafe.Pointer(&_zero)
7    }
8    _, _, e1 := Syscall(SYS_MADVISE, uintptr(_p0), uintptr(len(b)), uintptr(advice))
9    if e1 != 0 {
10        err = errnoErr(e1)
11    }
12    return
13}
14
15func EpollCreate(size int) (fd int, err error) {
16    r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE, uintptr(size), 0, 0)
17    fd = int(r0)
18    if e1 != 0 {
19        err = errnoErr(e1)
20    }
21    return
22}

顯然,標記為 sys 的系統調用使用的是 Syscall 或者 Syscall6,標記為 sysnb 的系統調用使用的是 RawSyscall 或 RawSyscall6。

wrapped 的系統調用是怎么一回事呢?

1func Rename(oldpath string, newpath string) (err error) {
2    return Renameat(_AT_FDCWD, oldpath, _AT_FDCWD, newpath)
3}

可能是覺得系統調用的名字不太好,或者參數太多,我們就簡單包裝一下。沒啥特別的。

runtime 中的 SYSCALL

除了上面提到的阻塞非阻塞和 wrapped syscall,runtime 中還定義了一些 low-level 的 syscall,這些是不暴露給用戶的。

提供給用戶的 syscall 庫,在使用時,會使 goroutine 和 p 分別進入 Gsyscall 和 Psyscall 狀態。但 runtime 自己封裝的這些 syscall 無論是否阻塞,都不會調用 entersyscall 和 exitsyscall。雖說是 “low-level” 的 syscall。

不過和暴露給用戶的 syscall 本質是一樣的。這些代碼在 runtime/sys_linux_amd64.s中,舉個具體的例子:

 1TEXT runtime·write(SB),NOSPLIT,$0-28
2    MOVQ    fd+0(FP), DI
3    MOVQ    p+8(FP), SI
4    MOVL    n+16(FP), DX
5    MOVL    $SYS_write, AX
6    SYSCALL
7    CMPQ    AX, $0xfffffffffffff001
8    JLS    2(PC)
9    MOVL    $-1, AX
10    MOVL    AX, ret+24(FP)
11    RET
12
13TEXT runtime·read(SB),NOSPLIT,$0-28
14    MOVL    fd+0(FP), DI
15    MOVQ    p+8(FP), SI
16    MOVL    n+16(FP), DX
17    MOVL    $SYS_read, AX
18    SYSCALL
19    CMPQ    AX, $0xfffffffffffff001
20    JLS    2(PC)
21    MOVL    $-1, AX
22    MOVL    AX, ret+24(FP)
23    RET

下面是所有 runtime 另外定義的 syscall 列表:

 1#define SYS_read        0
2#define SYS_write        1
3#define SYS_open        2
4#define SYS_close        3
5#define SYS_mmap        9
6#define SYS_munmap        11
7#define SYS_brk         12
8#define SYS_rt_sigaction    13
9#define SYS_rt_sigprocmask    14
10#define SYS_rt_sigreturn    15
11#define SYS_access        21
12#define SYS_sched_yield     24
13#define SYS_mincore        27
14#define SYS_madvise        28
15#define SYS_setittimer        38
16#define SYS_getpid        39
17#define SYS_socket        41
18#define SYS_connect        42
19#define SYS_clone        56
20#define SYS_exit        60
21#define SYS_kill        62
22#define SYS_fcntl        72
23#define SYS_getrlimit        97
24#define SYS_sigaltstack     131
25#define SYS_arch_prctl        158
26#define SYS_gettid        186
27#define SYS_tkill        200
28#define SYS_futex        202
29#define SYS_sched_getaffinity    204
30#define SYS_epoll_create    213
31#define SYS_exit_group        231
32#define SYS_epoll_wait        232
33#define SYS_epoll_ctl        233
34#define SYS_pselect6        270
35#define SYS_epoll_create1    291

這些 syscall 理論上都是不會在執行期間被調度器剝離掉 p 的,所以執行成功之后 goroutine 會繼續執行,而不像用戶的 goroutine 一樣,若被剝離 p 會進入等待隊列。

和調度的交互

既然要和調度交互,那友好地通知我要 syscall 了: entersyscall,我完事了: exitsyscall。

所以這里的交互指的是用戶代碼使用 syscall 庫時和調度器的交互。runtime 里的 syscall 不走這套流程。

▎entersyscall

 1// syscall 庫和 cgo 調用的標準入口
2//go:nosplit
3func entersyscall() {
4    reentersyscall(getcallerpc(), getcallersp())
5}
6
7//go:nosplit
8func reentersyscall(pc, sp uintptr) {
9    _g_ := getg()
10
11    // 需要禁止 g 的搶占
12    _g_.m.locks++
13
14    // entersyscall 中不能調用任何會導致棧增長/分裂的函數
15    _g_.stackguard0 = stackPreempt
16    // 設置 throwsplit,在 newstack 中,如果發現 throwsplit 是 true
17    // 會直接 crash
18    // 下面的代碼是 newstack 里的
19    // if thisg.m.curg.throwsplit {
20    //     throw("runtime: stack split at bad time")
21    // }
22    _g_.throwsplit = true
23
24    // Leave SP around for GC and traceback.
25    // 保存現場,在 syscall 之后會依據這些數據恢復現場
26    save(pc, sp)
27    _g_.syscallsp = sp
28    _g_.syscallpc = pc
29    casgstatus(_g_, _Grunning, _Gsyscall)
30    if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
31        systemstack(func() {
32            print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
33            throw("entersyscall")
34        })
35    }
36
37    if atomic.Load(&sched.sysmonwait) != 0 {
38        systemstack(entersyscall_sysmon)
39        save(pc, sp)
40    }
41
42    if _g_.m.p.ptr().runSafePointFn != 0 {
43        // runSafePointFn may stack split if run on this stack
44        systemstack(runSafePointFn)
45        save(pc, sp)
46    }
47
48    _g_.m.syscalltick = _g_.m.p.ptr().syscalltick
49    _g_.sysblocktraced = true
50    _g_.m.mcache = nil
51    _g_.m.p.ptr().m = 0
52    atomic.Store(&_g_.m.p.ptr().status, _Psyscall)
53    if sched.gcwaiting != 0 {
54        systemstack(entersyscall_gcwait)
55        save(pc, sp)
56    }
57
58    _g_.m.locks--
59}

可以看到,進入 syscall 的 G 是鐵定不會被搶占的。

exitsyscall

 1// g 已經退出了 syscall
2// 需要準備讓 g 在 cpu 上重新運行
3// 這個函數只會在 syscall 庫中被調用,在 runtime 里用的 low-level syscall
4// 不會用到
5// 不能有 write barrier,因為 P 可能已經被偷走了
6//go:nosplit
7//go:nowritebarrierrec
8func exitsyscall(dummy int32) {
9    _g_ := getg()
10
11    _g_.m.locks++ // see comment in entersyscall
12    if getcallersp(unsafe.Pointer(&dummy)) > _g_.syscallsp {
13        // throw calls print which may try to grow the stack,
14        // but throwsplit == true so the stack can not be grown;
15        // use systemstack to avoid that possible problem.
16        systemstack(func() {
17            throw("exitsyscall: syscall frame is no longer valid")
18        })
19    }
20
21    _g_.waitsince = 0
22    oldp := _g_.m.p.ptr()
23    if exitsyscallfast() {
24        if _g_.m.mcache == nil {
25            systemstack(func() {
26                throw("lost mcache")
27            })
28        }
29        // 目前有 p,可以運行
30        _g_.m.p.ptr().syscalltick++
31        // 把 g 的狀態修改回 running
32        casgstatus(_g_, _Gsyscall, _Grunning)
33
34        // 垃圾收集未在運行(因為我們這段邏輯在執行)
35        // 所以清理掉 syscallsp 是安全的
36        _g_.syscallsp = 0
37        _g_.m.locks--
38        if _g_.preempt {
39            // 防止在 newstack 中清理掉 preemption 標記
40            _g_.stackguard0 = stackPreempt
41        } else {
42            // 否則恢復在 entersyscall/entersyscallblock 中破壞掉的正常的 _StackGuard
43            _g_.stackguard0 = _g_.stack.lo + _StackGuard
44        }
45        _g_.throwsplit = false
46        return
47    }
48
49    _g_.sysexitticks = 0
50    _g_.m.locks--
51
52    // 調用 scheduler
53    mcall(exitsyscall0)
54
55    if _g_.m.mcache == nil {
56        systemstack(func() {
57            throw("lost mcache")
58        })
59    }
60
61    // 調度器返回了,所以我們可以清理掉在 syscall 期間為垃圾收集器
62    // 準備的 syscallsp 信息了
63    // 需要一直等待到 gosched 返回,我們不確定垃圾收集器是不是在運行
64    _g_.syscallsp = 0
65    _g_.m.p.ptr().syscalltick++
66    _g_.throwsplit = false
67}

這里還調用了 exitsyscallfast 和 exitsyscall0。

exitsyscallfast

 1//go:nosplit
2func exitsyscallfast() bool {
3    _g_ := getg()
4
5    // Freezetheworld sets stopwait but does not retake P's.
6    if sched.stopwait == freezeStopWait {
7        _g_.m.mcache = nil
8        _g_.m.p = 0
9        return false
10    }
11
12    // Try to re-acquire the last P.
13    if _g_.m.p != 0 && _g_.m.p.ptr().status == _Psyscall && atomic.Cas(&_g_.m.p.ptr().status, _Psyscall, _Prunning) {
14        // There's a cpu for us, so we can run.
15        exitsyscallfast_reacquired()
16        return true
17    }
18
19    // Try to get any other idle P.
20    oldp := _g_.m.p.ptr()
21    _g_.m.mcache = nil
22    _g_.m.p = 0
23    if sched.pidle != 0 {
24        var ok bool
25        systemstack(func() {
26            ok = exitsyscallfast_pidle()
27        })
28        if ok {
29            return true
30        }
31    }
32    return false
33}

總之就是努力獲取一個 P 來執行 syscall 之后的邏輯。如果哪都沒有 P 可以給我們用,那就進入 exitsyscall0 了。

1mcall(exitsyscall0)

調用 exitsyscall0 時,會切換到 g0 棧。

exitsyscall0

1// 在 exitsyscallfast 中吃癟了,沒辦法,慢慢來
2// 把 g 的狀態設置成 runnable,先進 runq 等著
3//go:nowritebarrierrec
4func exitsyscall0(gp *g) {
5    _g_ := getg()
6
7    casgstatus(gp, _Gsyscall, _Grunnable)
8    dropg()
9    lock(&sched.lock)
10    _p_ := pidleget()
11    if _p_ == nil {
12        // 如果 P 被人偷跑了
13        globrunqput(gp)
14    } else if atomic.Load(&sched.sysmonwait) != 0 {
15        atomic.Store(&sched.sysmonwait, 0)
16        notewakeup(&sched.sysmonnote)
17    }
18    unlock(&sched.lock)
19    if _p_ != nil {
20        // 如果現在還有 p,那就用這個 p 執行
21        acquirep(_p_)
22        execute(gp, false) // Never returns.
23    }
24    if _g_.m.lockedg != 0 {
25        // 設置了 LockOsThread 的 g 的特殊邏輯
26        stoplockedm()
27        execute(gp, false) // Never returns.
28    }
29    stopm()
30    schedule() // Never returns.
31}

entersyscallblock

知道自己會 block,直接就把 p 交出來了。

 1// 和 entersyscall 一樣,就是會直接把 P 給交出去,因為知道自己是會阻塞的
2//go:nosplit
3func entersyscallblock(dummy int32) {
4    _g_ := getg()
5
6    _g_.m.locks++ // see comment in entersyscall
7    _g_.throwsplit = true
8    _g_.stackguard0 = stackPreempt // see comment in entersyscall
9    _g_.m.syscalltick = _g_.m.p.ptr().syscalltick
10    _g_.sysblocktraced = true
11    _g_.m.p.ptr().syscalltick++
12
13    // Leave SP around for GC and traceback.
14    pc := getcallerpc()
15    sp := getcallersp(unsafe.Pointer(&dummy))
16    save(pc, sp)
17    _g_.syscallsp = _g_.sched.sp
18    _g_.syscallpc = _g_.sched.pc
19    if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
20        sp1 := sp
21        sp2 := _g_.sched.sp
22        sp3 := _g_.syscallsp
23        systemstack(func() {
24            print("entersyscallblock inconsistent ", hex(sp1), " ", hex(sp2), " ", hex(sp3), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
25            throw("entersyscallblock")
26        })
27    }
28    casgstatus(_g_, _Grunning, _Gsyscall)
29    if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
30        systemstack(func() {
31            print("entersyscallblock inconsistent ", hex(sp), " ", hex(_g_.sched.sp), " ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
32            throw("entersyscallblock")
33        })
34    }
35
36    // 直接調用 entersyscallblock_handoff 把 p 交出來了
37    systemstack(entersyscallblock_handoff)
38
39    // Resave for traceback during blocked call.
40    save(getcallerpc(), getcallersp(unsafe.Pointer(&dummy)))
41
42    _g_.m.locks--
43}

這個函數只有一個調用方 notesleepg,這里就不再贅述了。

▎entersyscallblock_handoff

1func entersyscallblock_handoff() {
2    handoffp(releasep())
3}

比較簡單。

▎entersyscall_sysmon

1func entersyscall_sysmon() {
2    lock(&sched.lock)
3    if atomic.Load(&sched.sysmonwait) != 0 {
4        atomic.Store(&sched.sysmonwait, 0)
5        notewakeup(&sched.sysmonnote)
6    }
7    unlock(&sched.lock)
8}

entersyscall_gcwait

 1func entersyscall_gcwait() {
2    _g_ := getg()
3    _p_ := _g_.m.p.ptr()
4
5    lock(&sched.lock)
6    if sched.stopwait > 0 && atomic.Cas(&_p_.status, _Psyscall, _Pgcstop) {
7        _p_.syscalltick++
8        if sched.stopwait--; sched.stopwait == 0 {
9            notewakeup(&sched.stopnote)
10        }
11    }
12    unlock(&sched.lock)
13}

總結

提供給用戶使用的系統調用,基本都會通知 runtime,以 entersyscall,exitsyscall 的形式來告訴 runtime,在這個 syscall 阻塞的時候,由 runtime 判斷是否把 P 騰出來給其它的 M 用。解綁定指的是把 M 和 P 之間解綁,如果綁定被解除,在 syscall 返回時,這個 g 會被放入執行隊列 runq 中。

同時 runtime 又保留了自己的特權,在執行自己的邏輯的時候,我的 P 不會被調走,這樣保證了在 Go 自己“底層”使用的這些 syscall 返回之后都能被立刻處理。

所以同樣是 epollwait,runtime 用的是不能被別人打斷的,你用的 syscall.EpollWait 那顯然是沒有這種特權的。

END

參考資料如下

https://z.didi.cn/1HecgP

曹春暉
滴滴 | 資深工程師

網名 Xargin,開源愛好者?;钴S在 Github 和各種技術社區。熱衷于技術互懟。著有開源書 《Go 高級編程》

我還沒有學會寫個人說明!

持續可用與CAP理論 - 一個數據庫系統開發者的觀點

上一篇

ASC19激戰開啟:20支超算戰隊向最高榮譽發起沖擊

下一篇

你也可能喜歡

曹春暉:談一談 Go 和 Syscall

長按儲存圖像,分享給朋友

ITPUB 每周精要將以郵件的形式發放至您的郵箱


微信掃一掃

微信掃一掃
海天娱乐群 琼崖麻将系统让你输就输 江西多乐彩十一选五走势图 中国福利彩票快乐双彩基本 股票下载什么软件 精准三肖中内部公开 江苏7位数预测天降财神 股票面值与发行价格 一码大公开 富贵棋牌游戏官网下载 北京赛车app软件下载