曹春晖:谈一谈 Go 和 Syscall

  • 时间:
  • 浏览:5

桔妹导读:syscall 是语言与系统交互的唯一手段,理解 Go 语言中的 syscall,本文可不都还里能 帮助读者理解 Go 语言为什么在与系统打交道,同時 了解底层 runtime 在 syscall 优化方面的有些小心思,从而更为深入地理解 Go 语言。

—————

阅读索引

  • 概念
  • 入口
  • 系统调用管理
  • runtime 中的 SYSCALL
  • 和调度的交互
    • entersyscall
    • exitsyscallfast
    • exitsyscall
    • entersyscallblock
    • entersyscallblock_handoff
    • entersyscall_sysmon
    • entersyscall_gcwait
  • 总结

概念

入口

syscall 有下面2个入口,在 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-3000

300    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)

3000    MOVQ    $0, err+72(FP)

51    CALL    runtime·exitsyscall(SB)

52    RET

一个多函数没那此大区别,为什么在不让一个多呢?自己猜测,Go 的函数参数一定会栈上传入,是是因为着是为了节省有些栈空间。。在正常的 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-3000

26    MOVQ    a1+8(FP), DI

27    MOVQ    a2+16(FP), SI

28    MOVQ    a3+24(FP), DX

29    MOVQ    a4+32(FP), R10

300    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        300

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        3000

28#define SYS_futex        202

29#define SYS_sched_getaffinity    204

300#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)

300    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

3000    _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,可不都还里能 运行

300        _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

3000    _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    }

300

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

300        }

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()

300    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 {

300        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,开源爱好者。活跃在 Github 和各种技术社区。热衷于技术互怼。著有开源书 《Go 高级编程》

本文由

滴滴技术

发布在

ITPUB

,转载此文请保持文章删改性,并请附上文章来源(ITPUB)及本页链接。

原文链接:http://www.itpub.net/2019/04/23/163000/