在看一些其它语言实现的基础工具时,时而发现其中有我们需要的某项特殊功能。究其源码,一般会看到两种底层实现:汇编、系统调用。这里的系统调用就是我们今天的主角了。
系统调用
系统调用在操作系统中占有重要的地位,是内核对外交互的门户,为我们提供了与底层资源交互的相对简单、安全的方式,给我们提供了一种在用户态、内核态切换的手段。
我们写的程序,通常是跑在用户态的,它对应 CPU 的 Ring 3 保护级别,而内核运行在 Ring 0 级别,拥有更高的权限。相应的,内核的代码可以运行一些用户态代码无法运行的 CPU 特权指令,实现一些用户态的代码做不到的事情,比如:控制进程的运行,使用驱动操作机器上的硬件。内核将部分自己实现的功能进行封装, 形成相对统一、方便的接口给我们进行调用,这些接口就是系统调用。
通常,我们使用某些特殊指令来通知内核去执行这些系统调用的对应代码,如:Int 0x80、sysenter、syscall。内核收到这些指令后会根据我们进程给出的参数,执行对应的功能。这时,我们的进程也会从用户态切换到内核态。
Golang 中 syscall 的实现
syscall
Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
从外观观察,可以知道它们可以按支持的参数个数分成两类:
SyscallRawSyscallSyscall6RawSyscall6
SyscallRawSyscall
Syscall
Syscall
// func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.
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
</pre>
这段汇编中,主要执行了 6 个步骤:
runtime.entersyscallruntime.exitsyscall
RawSyscall
RawSyscallSyscallruntime.entersyscallruntime.exitsyscallRawSyscall
提到阻塞就不得不解释下,系统调用可以分两种:快系统调用、慢系统调用。快系统调指的是不会造成阻塞的系统调用,如:获取 pid。相应的,慢系统指的就是会造成阻塞的系统调用,如:读写磁盘、网络。虽然平时可能感觉这些慢系统调用也执行的很快,但它们的速度相比 CPU 还是太慢,在某些情形下,这个速度还会被放慢很多,甚至出现假死(hang)的情况。
RawSyscall
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 ann internal mechanism.
syscall 库的生成
.s.go.sh.pl
gcc
执行系统调用
linuxamd64
常用系统调用
Golang 的 syscall 库已经对常用系统调用进行了封装,我们只需要调用相应的函数,并传入相应的参数就可以等着执行完成,给我们返回需要的结果了。
等等,这里需要我们要传入对应的参数,还有多个返回值,这些参数该怎么填,各个返回值又是什么含义呢?很可惜,syscall 库并没有对这些内容做必要的介绍,也就是说我们需要自行寻找一个资料,提供对每个系统调用进行详细描述的相对权威的描述。
manman
man
mmap
mmap
mmap
mmap
func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error)
man
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
这下,两边就能够对应上了,让我们来了解一下各个参数的具体定义:
mmap
同样,返回值也可以对应得上,不过,在形式上进行了一些转变,需要进行理解和翻译:
*addrvoid
我们来试一下,根据这个文档写出实现代码:
func main() {
f, err := os.OpenFile("mmap.bin", os.O_RDWR|os.O_CREATE, 0644)
if nil != err {
log.Fatalln(err)
}
// extend file
if _, err := f.WriteAt([]byte{byte(0)}, 1<<8); nil != err {
log.Fatalln(err)
}
data, err := syscall.Mmap(int(f.Fd()), 0, 1<<8, syscall.PROT_WRITE, syscall.MAP_SHARED)
if nil != err {
log.Fatalln(err)
}
if err := f.Close(); nil != err {
log.Fatalln(err)
}
for i, v := range []byte("hello syscall") {
data[i] = v
}
if err := syscall.Munmap(data); nil != err {
log.Fatalln(err)
}
}
mmap.binhexdump -C mmap.bin
任意系统调用
man
资料少不代表没有,golang 的资料找不到,不妨找一找 C/C++ 相关的实践,也可以直接去看执行了该系统调用的开源项目的源码。甚至,在极端情况下,我们可以直接查看该系统调用对应的内核源码。这里推荐使用 https://syscalls.kernelgrok.com 来快速定位具体系统调用的在内核源码中的具体位置。不过,这些收集资料的方式,对我们的操作系统知识、C 系语言源码的阅读能力要求较高。
syscall.Syscall
SYS_manuintptr
gotty
window := struct {
row uint16
col uint16
x uint16
y uint16
}{
rows,
columns,
0,
0,
}
syscall.Syscall(
syscall.SYS_IOCTL, // syscall number
context.pty.Fd(),
syscall.TIOCSWINSZ, // call option
uintptr(unsafe.Pointer(&window)),
)
是否要使用系统调用
shellshell
shell
这里需要注意,在我们的源码中调用第三方工具,我们要为这些第三方工具的正确性负责。一是要保证以正确的方式使用,二是在第三方工具的内部实现有 bug 时,我们要有相应的能力来分析与诊断相应的问题。