博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
曹春晖:谈一谈 Go 和 Syscall
阅读量:5904 次
发布时间:2019-06-19

本文共 16199 字,大约阅读时间需要 53 分钟。

出品 | 滴滴技术

作者 | 曹春晖

前言: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)23func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)45func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)67func 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, R910    MOVQ    trap+0(FP), AX    // syscall entry11    SYSCALL12    // 0xfffffffffffff001 是 linux MAX_ERRNO 取反 转无符号,http://lxr.free-electrons.com/source/include/linux/err.h#L1713    CMPQ    AX, $0xfffffffffffff00114    JLS    ok15    MOVQ    $-1, r1+32(FP)16    MOVQ    $0, r2+40(FP)17    NEGQ    AX18    MOVQ    AX, err+48(FP)19    CALL    runtime·exitsyscall(SB)20    RET21ok:22    MOVQ    AX, r1+32(FP)23    MOVQ    DX, r2+40(FP)24    MOVQ    $0, err+48(FP)25    CALL    runtime·exitsyscall(SB)26    RET2728// func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)29TEXT ·Syscall6(SB),NOSPLIT,$0-8030    CALL    runtime·entersyscall(SB)31    MOVQ    a1+8(FP), DI32    MOVQ    a2+16(FP), SI33    MOVQ    a3+24(FP), DX34    MOVQ    a4+32(FP), R1035    MOVQ    a5+40(FP), R836    MOVQ    a6+48(FP), R937    MOVQ    trap+0(FP), AX    // syscall entry38    SYSCALL39    CMPQ    AX, $0xfffffffffffff00140    JLS    ok641    MOVQ    $-1, r1+56(FP)42    MOVQ    $0, r2+64(FP)43    NEGQ    AX44    MOVQ    AX, err+72(FP)45    CALL    runtime·exitsyscall(SB)46    RET47ok6: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 的函数参数都是栈上传入,可能是为了节省一点栈空间。。在正常的 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 entry10    SYSCALL11    CMPQ    AX, $0xfffffffffffff00112    JLS    ok113    MOVQ    $-1, r1+32(FP)14    MOVQ    $0, r2+40(FP)15    NEGQ    AX16    MOVQ    AX, err+48(FP)17    RET18ok1:19    MOVQ    AX, r1+32(FP)20    MOVQ    DX, r2+40(FP)21    MOVQ    $0, err+48(FP)22    RET2324// func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)25TEXT ·RawSyscall6(SB),NOSPLIT,$0-8026    MOVQ    a1+8(FP), DI27    MOVQ    a2+16(FP), SI28    MOVQ    a3+24(FP), DX29    MOVQ    a4+32(FP), R1030    MOVQ    a5+40(FP), R831    MOVQ    a6+48(FP), R932    MOVQ    trap+0(FP), AX    // syscall entry33    SYSCALL34    CMPQ    AX, $0xfffffffffffff00135    JLS    ok236    MOVQ    $-1, r1+56(FP)37    MOVQ    $0, r2+64(FP)38    NEGQ    AX39    MOVQ    AX, err+72(FP)40    RET41ok2: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    ok710    NEGQ    AX11    MOVQ    AX, err+8(FP)12    RET13ok7: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    return13}1415func 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    return22}复制代码

显然,标记为 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, AX10    MOVL    AX, ret+24(FP)11    RET1213TEXT runtime·read(SB),NOSPLIT,$0-2814    MOVL    fd+0(FP), DI15    MOVQ    p+8(FP), SI16    MOVL    n+16(FP), DX17    MOVL    $SYS_read, AX18    SYSCALL19    CMPQ    AX, $0xfffffffffffff00120    JLS    2(PC)21    MOVL    $-1, AX22    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    1410#define SYS_rt_sigreturn    1511#define SYS_access        2112#define SYS_sched_yield     2413#define SYS_mincore        2714#define SYS_madvise        2815#define SYS_setittimer        3816#define SYS_getpid        3917#define SYS_socket        4118#define SYS_connect        4219#define SYS_clone        5620#define SYS_exit        6021#define SYS_kill        6222#define SYS_fcntl        7223#define SYS_getrlimit        9724#define SYS_sigaltstack     13125#define SYS_arch_prctl        15826#define SYS_gettid        18627#define SYS_tkill        20028#define SYS_futex        20229#define SYS_sched_getaffinity    20430#define SYS_epoll_create    21331#define SYS_exit_group        23132#define SYS_epoll_wait        23233#define SYS_epoll_ctl        23334#define SYS_pselect6        27035#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()1011    // 需要禁止 g 的抢占12    _g_.m.locks++1314    // entersyscall 中不能调用任何会导致栈增长/分裂的函数15    _g_.stackguard0 = stackPreempt16    // 设置 throwsplit,在 newstack 中,如果发现 throwsplit 是 true17    // 会直接 crash18    // 下面的代码是 newstack 里的19    // if thisg.m.curg.throwsplit {20    //     throw("runtime: stack split at bad time")21    // }22    _g_.throwsplit = true2324    // Leave SP around for GC and traceback.25    // 保存现场,在 syscall 之后会依据这些数据恢复现场26    save(pc, sp)27    _g_.syscallsp = sp28    _g_.syscallpc = pc29    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    }3637    if atomic.Load(&sched.sysmonwait) != 0 {38        systemstack(entersyscall_sysmon)39        save(pc, sp)40    }4142    if _g_.m.p.ptr().runSafePointFn != 0 {43        // runSafePointFn may stack split if run on this stack44        systemstack(runSafePointFn)45        save(pc, sp)46    }4748    _g_.m.syscalltick = _g_.m.p.ptr().syscalltick49    _g_.sysblocktraced = true50    _g_.m.mcache = nil51    _g_.m.p.ptr().m = 052    atomic.Store(&_g_.m.p.ptr().status, _Psyscall)53    if sched.gcwaiting != 0 {54        systemstack(entersyscall_gcwait)55        save(pc, sp)56    }5758    _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()1011    _g_.m.locks++ // see comment in entersyscall12    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    }2021    _g_.waitsince = 022    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 的状态修改回 running32        casgstatus(_g_, _Gsyscall, _Grunning)3334        // 垃圾收集未在运行(因为我们这段逻辑在执行)35        // 所以清理掉 syscallsp 是安全的36        _g_.syscallsp = 037        _g_.m.locks--38        if _g_.preempt {39            // 防止在 newstack 中清理掉 preemption 标记40            _g_.stackguard0 = stackPreempt41        } else {42            // 否则恢复在 entersyscall/entersyscallblock 中破坏掉的正常的 _StackGuard43            _g_.stackguard0 = _g_.stack.lo + _StackGuard44        }45        _g_.throwsplit = false46        return47    }4849    _g_.sysexitticks = 050    _g_.m.locks--5152    // 调用 scheduler53    mcall(exitsyscall0)5455    if _g_.m.mcache == nil {56        systemstack(func() {57            throw("lost mcache")58        })59    }6061    // 调度器返回了,所以我们可以清理掉在 syscall 期间为垃圾收集器62    // 准备的 syscallsp 信息了63    // 需要一直等待到 gosched 返回,我们不确定垃圾收集器是不是在运行64    _g_.syscallsp = 065    _g_.m.p.ptr().syscalltick++66    _g_.throwsplit = false67}复制代码

这里还调用了 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 false10    }1112    // 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 true17    }1819    // Try to get any other idle P.20    oldp := _g_.m.p.ptr()21    _g_.m.mcache = nil22    _g_.m.p = 023    if sched.pidle != 0 {24        var ok bool25        systemstack(func() {26            ok = exitsyscallfast_pidle()27        })28        if ok {29            return true30        }31    }32    return false33}复制代码

总之就是努力获取一个 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().syscalltick10    _g_.sysblocktraced = true11    _g_.m.p.ptr().syscalltick++1213    // 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.sp18    _g_.syscallpc = _g_.sched.pc19    if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {20        sp1 := sp21        sp2 := _g_.sched.sp22        sp3 := _g_.syscallsp23        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    }3536    // 直接调用 entersyscallblock_handoff 把 p 交出来了37    systemstack(entersyscallblock_handoff)3839    // Resave for traceback during blocked call.40    save(getcallerpc(), getcallersp(unsafe.Pointer(&dummy)))4142    _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

参考资料如下

        

网名 Xargin,开源爱好者。活跃在 Github 和各种技术社区。热衷于技术互怼。著有开源书 《Go 高级编程》

转载于:https://juejin.im/post/5cdd56f6e51d456e8b07de5e

你可能感兴趣的文章
内置对象Array的原型对象中添加方法
查看>>
12行代码的相关节点
查看>>
6大设计原则
查看>>
Github简介
查看>>
存储过程—导出table数据为inser sqlt语句
查看>>
Windows 7下Maven3.0.3的安装
查看>>
CISCO2691的OSPF点对点密文测评测试
查看>>
POJ 1661 Help Jimmy(递推DP)
查看>>
Node.js 中文学习资料和教程导航
查看>>
查找(AVL平衡二叉树)
查看>>
Javascript函数调用的四种模式
查看>>
用 Asterisk 搭建自己的免费 VoIP 服务器
查看>>
lua笔记二 赋值语句
查看>>
Android 中 Internal Storage 和 External Storage 的区别
查看>>
移动端拖拽(模块化开发,触摸事件,webpack)
查看>>
spring配置和注解事务同时存在导致的事务嵌套
查看>>
AE要素选择(点选和拉框选择)
查看>>
AJAX-初学AJAX本地环境配置
查看>>
VSCode调试配置
查看>>
前端MVC学习总结(三)——AngularJS服务、路由、内置API、jQueryLite
查看>>