Golang System call
这篇文章主要是分析在golang里面用户态进行系统调用时候的一些原理,主要关注点将会放在system call与scheduler之间的关联。
1.入口
系统调用的入口根据不同系统有不同实现,对于AMD64, Linux环境是:syscall/asm_linux_amd64.s
函数声明如下:
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
这些函数的实现都是汇编,按照 linux 的 syscall 调用规范,我们只要在汇编中把参数依次传入寄存器,并调用 SYSCALL 指令即可进入内核处理逻辑,系统调用执行完毕之后,返回值放在 RAX 中:
Syscall 和 Syscall6 的区别只有传入参数不一样, 具体源码与实现请看golang的开源源码。
这里只列出Syscall和RawSyscall的源码:
//Syscall
TEXT ·Syscall(SB),NOSPLIT,$0-56
CALL runtime·entersyscall(SB)
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS ok
MOVQ $-1, r1+32(FP)
MOVQ $0, r2+40(FP)
NEGQ AX
MOVQ AX, err+48(FP)
CALL runtime·exitsyscall(SB)
RET
ok:
MOVQ AX, r1+32(FP)
MOVQ DX, r2+40(FP)
MOVQ $0, err+48(FP)
CALL runtime·exitsyscall(SB)
RET
//RawSyscall
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS ok1
MOVQ $-1, r1+32(FP)
MOVQ $0, r2+40(FP)
NEGQ AX
MOVQ AX, err+48(FP)
RET
ok1:
MOVQ AX, r1+32(FP)
MOVQ DX, r2+40(FP)
MOVQ $0, err+48(FP)
RET
Syscall和RawSyscall的实现比较典型,可以看到这两个实现最主要的区别在于:
Syscall在进入系统调用的时候,调用了runtime·entersyscall(SB)函数,在结束系统调用的时候调用了runtime·exitsyscall(SB)。做到进入和退出syscall的时候通知runtime。
这两个函数runtime·entersyscall和runtime·exitsyscall的实现在proc.go文件里面。其实在runtime·entersyscall函数里面,通知系统调用时候,是会将g的M的P解绑,P可以去继续获取M执行其余的g,这样提升效率。
所以如果用户代码使用了 RawSyscall 来做一些阻塞的系统调用,是有可能阻塞其它的 g 的。RawSyscall 只是为了在执行那些一定不会阻塞的系统调用时,能节省两次对 runtime 的函数调用消耗。
runtime·entersyscall和runtime·exitsyscall这两个函数也是与scheduler交互的地方,后面会对源码进行分析。
2.系统调用管理
系统调用的定义文件: /syscall/syscall_linux.go
可以把系统调用分为三类:
- 阻塞系统调用
- 非阻塞系统调用非阻塞系统调用
- wrapped 系统调用
阻塞系统调用会定义成下面这样的形式:
//sys Madvise(b []byte, advice int) (err error)
非阻塞系统调用:
//sysnb EpollCreate(size int) (fd int, err error)
然后,根据这些注释,mksyscall.pl 脚本会生成对应的平台的具体实现。mksyscall.pl 是一段 perl 脚本,感兴趣的同学可以自行查看,这里就不再赘述了。
看看阻塞和非阻塞的系统调用的生成结果:
func Madvise(b []byte, advice int) (err error) {
var _p0 unsafe.Pointer
if len(b) > 0 {
_p0 = unsafe.Pointer(&b[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
_, _, e1 := Syscall(SYS_MADVISE, uintptr(_p0), uintptr(len(b)), uintptr(advice))
if e1 != 0 {
err = errnoErr(e1)
}
return
}
func EpollCreate(size int) (fd int, err error) {
r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE, uintptr(size), 0, 0)
fd = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
标记为 sys(阻塞)的系统调用使用的是 Syscall 或者 Syscall6,标记为 sysnb(非阻塞) 的系统调用使用的是 RawSyscall 或 RawSyscall6。
wrapped 的系统调用是怎么一回事呢?
func Rename(oldpath string, newpath string) (err error) {
return Renameat(_AT_FDCWD, oldpath, _AT_FDCWD, newpath)
}
可能是觉得系统调用的名字不太好,或者参数太多,我们就简单包装一下。没啥特别的。