标准库syscall给这些系统调用做了不错的封装,不少常用的系统调用已经可以像普通函数一样直接调用,但是大部分使用起来都比较陌生。

Syscall

Golang 的 syscall 库已经对常用系统调用进行了封装,我们只需要调用相应的函数,并传入相应的参数就可以等着执行完成,给我们返回需要的结果了。

man 这个我们平日里经常用到的命令,除了提供各种命令的使用帮助,还提供了不少系统层面的资料,其中就有我们所需要的各个系统调用的具体描述。通过比对 man 里的资料与封装函数的外观,我们可以得到具体系统调用的对应实践方式。

我们来做个尝试,在实现过程中来体会具体的实践方式。这里,我们选择使用 mmap 来实现数据的持久化存储作为示例。

mmap

mmap 就是将文件映射进内存的基本原理,以及相比传统的文件读写方式的优劣势。

然后,查看标准库对 mmap 这个系统调用的封装:

func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error)

接着,我们查看 man 对 mmap 的介绍 :

void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

这下,两边就能够对应上了,让我们来了解一下各个参数的具体定义:

  • fd:映射进内存的文件描述符
  • offset:映射进内存的文件段的起始位置,在文件中的偏移量
  • length:映射进内存的文件段的长度,必须是正整数
  • prot:protection 的缩写,用来做权限控制,golang 标准库已有预定义的值
  • flags:对 mmap 的一些行为进行控制,golang 标准库已有预定义的值

同样,返回值也可以对应得上,不过,在形式上进行了一些转变,需要进行理解和翻译:

  • data:对应 *addr,返回映射进内存的文件段对应的数组,持久化数据就是使用这个数组
  • err:对应这个函数的返回值 void ,返回值的含义,在 golang 已有对应的定义

我们来试一下,根据这个文档写出实现代码:

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.bin 文件,执行 hexdump -C mmap.bin 可以看到,文件里面已有我们写入的内容。

其他的系统调用都可以参照这个方法来理解。

封装方式

标准库syscall提供了 4 个通用的封装方式,供我们执行任意的系统调用:

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)

从外观观察,可以知道它们可以按支持的参数个数分成两类:

  • 供 4 个及 4 个以下参数的系统调用使用的 Syscall、RawSyscall
  • 供 6 个及 6 个以下参数的系统调用使用的 Syscall6、RawSyscall6

而从对我们来说更有意义的实现、功用的角度看,可以分为 Syscall、RawSyscall 两类。