---
id: c90263519492728e7cc2d0ce840057b6
author: Yunjin Lee
title: 使用纯 Go 通过 syscall.Syscall 和 unsafe.Pointer 直接调用 Linux 系统调用
description: 在纯 Go 代码中不使用 cgo，通过 ABI 对齐的结构体、unsafe.Pointer 以及标准 syscall 包直接调用 SYS_SYSINFO 等 Linux 内核系统调用。
language: zh
date: 2025-10-26T12:31:21.655228078Z
path: /blog/posts/go-syscall-is-a-brilliant-replacement-of-low-level-i-o-za50951e1
category: lowlevel
---

## 摘要

我们将学习在 Go 中直接进行系统调用（direct system call）。
由于 Go 提供严格的编译器错误检查和严格的垃圾回收机制（GC），用纯 Go 替换底层调用会更好。
幸运的是，大多数 C 函数调用已在 Go 中完整地重新实现，且实现方式良好且符合现代风格。
让我们来看一看。

## 系统调用

系统调用是向操作系统发出的直接请求。
由于系统通常以严格且陈旧的风格编写，因为它直接运行在硬件之上，我们需要考虑到其调用必须提交严格且正确的请求格式。
因此，即使我们不需要某些变量，也仍然需要无论是否使用都填满其大小。
让我们通过一个完整可运行的示例来验证。

## 完整示例

```go
package main
import (
	"fmt"
	"syscall"
	"unsafe"
)

type sysinfo_t struct {
	Uptime    int64
	Loads     [3]uint64
	Totalram  uint64
	Freeram   uint64
	Sharedram uint64
	Bufferram uint64
	Totalswap uint64
	Freeswap  uint64
	Procs     uint16
	Pad       uint16
	_         [4]byte
	Totalhigh uint64
	Freehigh  uint64
	MemUnit   uint32
	_         [4]byte
}

func main() {
	var info sysinfo_t
	_, _, errno := syscall.Syscall(syscall.SYS_SYSINFO, uintptr(unsafe.Pointer(&info)), 0, 0)
	if errno != 0 {
		fmt.Println("sysinfo syscall failed:", errno)
		return
	}

	scale := float64(1 << 16)
	fmt.Printf("Uptime: %d seconds\n", info.Uptime)
	fmt.Printf("Load Average: %.2f %.2f %.2f\n",
		float64(info.Loads[0])/scale,
		float64(info.Loads[1])/scale,
		float64(info.Loads[2])/scale)
	fmt.Printf("Memory: total=%d MB free=%d MB buffer=%d MB\n",
		info.Totalram*uint64(info.MemUnit)/1024/1024,
		info.Freeram*uint64(info.MemUnit)/1024/1024,
		info.Bufferram*uint64(info.MemUnit)/1024/1024)
	fmt.Printf("Swap: total=%d MB free=%d MB\n",
		info.Totalswap*uint64(info.MemUnit)/1024/1024,
		info.Freeswap*uint64(info.MemUnit)/1024/1024)
	fmt.Printf("Processes: %d\n", info.Procs)
}
```

此示例包含了所有变量，并打印当前系统的详细信息。
我们可以将这段代码比作锁和钥匙。
`syscall.SYS_SYSINFO` 是一把钥匙，用于打开位于内核中的一把锁。
因此，为锁使用正确的钥匙至关重要。
如果我们在此调用中使用 `syscall.SYS_GETPID` 会发生什么？
这是用于包含进程 ID 的锁的钥匙。
这将尝试从系统信息的空间中获取 PID。
结果是，任何信息都无法被正确读取；该调用必须以失败状态返回。

现在，我们需要了解包含哪些项目，以及这些项目如何排列。
在锁的第一个槽位中，有 Uptime，其大小为 2^64。
如果我们尝试以 2^32 读取它，则比特序列不会被完整读取。
除非我们要编写底层技巧，否则不能使用这类部分二进制数据。

在读取 64 位二进制数据之后，我们终于来到第二个槽位。
只有在读取了前面那个 64 位整数之后，才能准确读取它。

通过重复这些严格且符合逻辑的流程，我们便能从系统中获取正确的信息，并正确处理读取到的数据。

## 如何跳过"变量名"

尽管我们无法真正"跳过"变量本身，但区分已使用的变量与被丢弃的变量仍然很重要。
如果程序的用途足够清楚，那么与其为每个值都加上标签（即使它们之后永远不会被使用），不如使用无名变量作为占位符更好。
让我们通过一个示例"空闲内存检查器"来验证这一点。

## 示例 - 空闲内存检查器

在检查空闲内存/交换空间时，我们不需要表示其他资源的信息。
为了获得更好的可读性，你可以使用匿名变量来保存特定的空间。

```go
package main

import (
	"fmt"
	"syscall"
	"unsafe"
)

type sysinfo_t struct {
	_          int64
	_         [3]uint64
	Totalram  uint64
	Freeram   uint64
	Sharedram uint64
	Bufferram uint64
	Totalswap uint64
	Freeswap  uint64
	_         uint16  // anonymous, and unused ones are marked as _
	_         uint16
	_         [4]byte
	_         uint64
	_         uint64
	MemUnit   uint32
	_         [4]byte
}

func main() {
	var info sysinfo_t
	_, _, errno := syscall.Syscall(syscall.SYS_SYSINFO, uintptr(unsafe.Pointer(&info)), 0, 0)
	if errno != 0 {
		fmt.Println("sysinfo syscall failed:", errno)
		return
	}

	fmt.Printf("Memory: total=%d MB free=%d MB buffer=%d MB\n",
		info.Totalram*uint64(info.MemUnit)/1024/1024,
		info.Freeram*uint64(info.MemUnit)/1024/1024,
		info.Bufferram*uint64(info.MemUnit)/1024/1024)
	fmt.Printf("Swap: total=%d MB free=%d MB\n",
		info.Totalswap*uint64(info.MemUnit)/1024/1024,
		info.Freeswap*uint64(info.MemUnit)/1024/1024)
}
```

因此，变量在读取时没有标签。
尽管匿名值实际上仍存储在结构体中，但代码上并没有标签或可读的标记。

## 结论

- 使用 Go 的 `syscall` 和 `unsafe` 仍然比 C/CGo 更安全
- 如果你正在编写一个可以轻松扩展的大型项目：
  - 不要使用匿名变量；为每个成员都指定名称。
- 如果你正在编写一个用途有限的项目：
  - 可以使用匿名变量来保存实际上未使用的空间。
- Go 的 `syscall` 功能强大且现代，能够处理底层调用

## 延伸阅读

[syscall](https://pkg.go.dev/syscall)
[unsafe](https://pkg.go.dev/unsafe)
[x/sys/unix](https://pkg.go.dev/golang.org/x/sys/unix)
