Go syscall 是对低级 I/O 的杰出替代。
摘要
我们将学习如何在 Go 语言中进行直接的系统调用(direct system call)。鉴于 Go 语言提供了严格的编译器错误检查和严谨的 GC(Garbage Collection,垃圾回收),在 Pure Go(纯 Go)中替换低级调用(low-level calls)是更好的选择。幸运的是,大多数 C 语言的函数调用都已在 Go 语言中以良好且现代的方式进行了完整的重新实现。让我们来看一看。
系统调用
系统调用(System call)是对操作系统发出的直接请求。由于系统通常是以严谨、老式(old-fashioned)的风格编写的,因为它直接运行在硬件上,我们需要考虑到其调用必须以严格且正确的形式传递请求。因此,即使我们不需要某些变量,我们仍然需要填写其大小(size),无论是否使用。让我们通过一个完全可运行的示例来验证这一点。
完整示例
1package main
2import (
3 "fmt"
4 "syscall"
5 "unsafe"
6)
7
8type sysinfo_t struct {
9 Uptime int64
10 Loads [3]uint64
11 Totalram uint64
12 Freeram uint64
13 Sharedram uint64
14 Bufferram uint64
15 Totalswap uint64
16 Freeswap uint64
17 Procs uint16
18 Pad uint16
19 _ [4]byte
20 Totalhigh uint64
21 Freehigh uint64
22 MemUnit uint32
23 _ [4]byte
24}
25
26func main() {
27 var info sysinfo_t
28 _, _, errno := syscall.Syscall(syscall.SYS_SYSINFO, uintptr(unsafe.Pointer(&info)), 0, 0)
29 if errno != 0 {
30 fmt.Println("sysinfo syscall failed:", errno)
31 return
32 }
33
34 scale := float64(1 << 16)
35 fmt.Printf("Uptime: %d seconds\n", info.Uptime)
36 fmt.Printf("Load Average: %.2f %.2f %.2f\n",
37 float64(info.Loads[0])/scale,
38 float64(info.Loads[1])/scale,
39 float64(info.Loads[2])/scale)
40 fmt.Printf("Memory: total=%d MB free=%d MB buffer=%d MB\n",
41 info.Totalram*uint64(info.MemUnit)/1024/1024,
42 info.Freeram*uint64(info.MemUnit)/1024/1024,
43 info.Bufferram*uint64(info.MemUnit)/1024/1024)
44 fmt.Printf("Swap: total=%d MB free=%d MB\n",
45 info.Totalswap*uint64(info.MemUnit)/1024/1024,
46 info.Freeswap*uint64(info.MemUnit)/1024/1024)
47 fmt.Printf("Processes: %d\n", info.Procs)
48}
此示例包含了所有变量,并打印了当前系统的详尽信息。我们可以将这段代码比作一个锁和一个钥匙。syscall.SYS_SYSINFO 就是一把钥匙,它能打开内核内部的一个锁。因此,使用正确的钥匙去匹配锁是至关重要的。如果我们在此调用中使用 syscall.SYS_GETPID 会发生什么?
这是用于包含进程 ID(Process ID)的锁的钥匙。这将尝试从系统信息的空间中获取 PID。结果是,没有任何信息能够被正确读取;该调用必须以失败状态返回。
现在,我们需要知道包含哪些项,以及这些项的顺序如何。在锁的第一个槽位中,我们有 Uptime,其大小为 2^64。如果我们尝试用 2^32 读取它,位序列(bit sequence)将不会被完全读取。除非我们打算编写低级技巧(low-level tricks),否则我们不能使用这种部分的二进制数据。
在读取了 64 位的二进制数据之后,我们终于到达了第二个槽位。只有当我们读取了前面 64 位大小的整数之后,它才能被准确读取。
通过重复这些严格且逻辑化的流程,从系统中获取适当的信息,我们可以正确地处理读取到的数据。
如何跳过“变量名”
尽管我们不能“跳过”变量本身,但区分已使用的变量和被丢弃的变量是很重要的。如果程序的使用目的足够清晰,那么使用匿名变量(nameless variables)作为占位符,要优于为每个值都贴上标签,即使它们永远不会被使用。让我们通过一个示例“空闲内存检查器”(Free Memory Checker)来验证这一点。
示例 - 空闲内存检查器
在检查空闲内存/交换区(Free Memory/Swaps)时,我们不需要指示不同资源的其他信息。为了获得更好的可见性,您可以创建匿名变量来占据特定的空间。
1package main
2
3import (
4 "fmt"
5 "syscall"
6 "unsafe"
7)
8
9type sysinfo_t struct {
10 _ int64
11 _ [3]uint64
12 Totalram uint64
13 Freeram uint64
14 Sharedram uint64
15 Bufferram uint64
16 Totalswap uint64
17 Freeswap uint64
18 _ uint16 // anonymous, and unused ones are marked as _
19 _ uint16
20 _ [4]byte
21 _ uint64
22 _ uint64
23 MemUnit uint32
24 _ [4]byte
25}
26
27func main() {
28 var info sysinfo_t
29 _, _, errno := syscall.Syscall(syscall.SYS_SYSINFO, uintptr(unsafe.Pointer(&info)), 0, 0)
30 if errno != 0 {
31 fmt.Println("sysinfo syscall failed:", errno)
32 return
33 }
34
35 fmt.Printf("Memory: total=%d MB free=%d MB buffer=%d MB\n",
36 info.Totalram*uint64(info.MemUnit)/1024/1024,
37 info.Freeram*uint64(info.MemUnit)/1024/1024,
38 info.Bufferram*uint64(info.MemUnit)/1024/1024)
39 fmt.Printf("Swap: total=%d MB free=%d MB\n",
40 info.Totalswap*uint64(info.MemUnit)/1024/1024,
41 info.Freeswap*uint64(info.MemUnit)/1024/1024)
42}
因此,变量在没有标签的情况下被读取。尽管匿名值实际上存储在结构体中,但在代码上没有标签/可读的标记。
结论
- 使用 Go 的
syscall和unsafe仍然比 C/CGo 更安全 - 如果您正在编写一个可以轻松扩展的大型项目:
- 不要创建匿名变量;为每个成员命名。
- 如果您正在编写一个用途有限的项目:
- 您可以使用匿名变量来占据那些实际上未使用的空间。
- Go 的
syscall功能强大且现代化,足以处理低级调用