使用纯 Go 通过 syscall.Syscall 和 unsafe.Pointer 直接调用 Linux 系统调用

摘要
我们将学习在 Go 中直接进行系统调用(direct system call)。由于 Go 提供严格的编译器错误检查和严格的垃圾回收机制(GC),用纯 Go 替换底层调用会更好。幸运的是,大多数 C 函数调用已在 Go 中完整地重新实现,且实现方式良好且符合现代风格。让我们来看一看。
系统调用
系统调用是向操作系统发出的直接请求。由于系统通常以严格且陈旧的风格编写,因为它直接运行在硬件之上,我们需要考虑到其调用必须提交严格且正确的请求格式。因此,即使我们不需要某些变量,也仍然需要无论是否使用都填满其大小。让我们通过一个完整可运行的示例来验证。
完整示例
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 的锁的钥匙。这将尝试从系统信息的空间中获取 PID。结果是,任何信息都无法被正确读取;该调用必须以失败状态返回。
现在,我们需要了解包含哪些项目,以及这些项目如何排列。在锁的第一个槽位中,有 Uptime,其大小为 2^64。如果我们尝试以 2^32 读取它,则比特序列不会被完整读取。除非我们要编写底层技巧,否则不能使用这类部分二进制数据。
在读取 64 位二进制数据之后,我们终于来到第二个槽位。只有在读取了前面那个 64 位整数之后,才能准确读取它。
通过重复这些严格且符合逻辑的流程,我们便能从系统中获取正确的信息,并正确处理读取到的数据。
如何跳过"变量名"
尽管我们无法真正"跳过"变量本身,但区分已使用的变量与被丢弃的变量仍然很重要。如果程序的用途足够清楚,那么与其为每个值都加上标签(即使它们之后永远不会被使用),不如使用无名变量作为占位符更好。让我们通过一个示例"空闲内存检查器"来验证这一点。
示例 - 空闲内存检查器
在检查空闲内存/交换空间时,我们不需要表示其他资源的信息。为了获得更好的可读性,你可以使用匿名变量来保存特定的空间。
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功能强大且现代,能够处理底层调用