出品 | 滴滴技術
做者 | 曹春暉html
前言:syscall 是語言與系統交互的惟一手段,理解 Go 語言中的 syscall,本文能夠幫助讀者理解 Go 語言怎麼與系統打交道,同時瞭解底層 runtime 在 syscall 優化方面的一些當心思,從而更爲深刻地理解 Go 語言。linux
▎閱讀索引安全
▎概念app
▎入口electron
syscall 有下面幾個入口,在 syscall/asm_linux_amd64.s 中。socket
1 func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) 2 3 func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) 4 5 func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) 6 7 func 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 的函數參數都是棧上傳入,多是爲了節省一點棧空間。。在正常的 Syscall 操做以前會通知 runtime,接下來我要進行 syscall 操做了 runtime·entersyscall ,退出時會調用 runtime·exitsyscall 。ui
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 的,下面是官方開發的原話:this
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) 2 TEXT ·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 13 ok7: 14 MOVQ $0, err+8(FP) 15 RET
▎系統調用管理
先是系統調用的定義文件:
1/syscall/syscall_linux.go
能夠把系統調用分爲三類:
阻塞系統調用
非阻塞系統調用
wrapped 系統調用
阻塞系統調用會定義成下面這樣的形式:
1 //sys Madvise(b []byte, advice int) (err error)
而後,根據這些註釋,mksyscall.pl 腳本會生成對應的平臺的具體實現。mksyscall.pl 是一段 perl 腳本,感興趣的同窗能夠自行查看,這裏就再也不贅述了。
看看阻塞和非阻塞的系統調用的生成結果:
1 func 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 了。
1 mcall(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
1 func entersyscallblock_handoff() { 2 handoffp(releasep()) 3 }
比較簡單。
▎entersyscall_sysmon
1 func 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
1 func 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