GoSuda

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

By Yunjin Lee views ...

摘要

我们将学习在 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 功能强大且现代,能够处理底层调用

延伸阅读

syscall unsafe x/sys/unix