GoSuda

Chamadas de Sistema Linux Diretas em Go Puro com syscall.Syscall e unsafe.Pointer

By Yunjin Lee views ...

Resumo

Vamos aprender sobre chamadas de sistema diretas em Go. Como Go oferece erros de compilação rigorosos e um GC rígido, é muito melhor substituir chamadas de baixo nível por Go puro. Felizmente, a maioria das chamadas de função em C foi totalmente reimplementada em Go, de forma boa e contemporânea. Vamos dar uma olhada nisso.

Chamada de Sistema

Uma chamada de sistema é uma requisição direta ao sistema operacional. Como o sistema costuma ser escrito em um estilo rígido e antiquado, já que roda diretamente sobre o hardware, precisamos considerar que sua chamada deve entregar uma forma estrita e correta de requisição. Assim, mesmo que não precisemos de algumas variáveis, ainda precisamos preencher o tamanho, independentemente do uso. Vamos verificar com um exemplo totalmente funcional.

Exemplo Completo

 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}

Este exemplo inclui todas as variáveis e imprime informações extensas sobre as informações atuais do sistema. Podemos comparar este código com uma fechadura e uma chave.syscall.SYS_SYSINFO é uma chave que abre uma fechadura que está dentro do kernel. Portanto, usar a chave correta para a fechadura é importante. O que acontecerá se usarmos syscall.SYS_GETPID nesta chamada? Esta é a chave para uma fechadura que contém o Process ID. Isso tentará obter um PID a partir de um espaço destinado a informações do sistema. Como resultado, nenhuma das informações pode ser lida corretamente; a chamada deve retornar em estado de falha.

Agora, precisamos saber quais itens estão contidos e como os itens estão ordenados. No primeiro slot da fechadura, temos Uptime, com tamanho de 2^64. Se tentarmos lê-lo com 2^32, a sequência de bits não é lida por completo. Não podemos usar esses tipos de binários parciais, a menos que pretendamos escrever truques de baixo nível.

Após ler 64 bits de dados binários, finalmente chegamos ao segundo slot. Ele só pode ser lido com precisão quando tivermos lido o inteiro de 64 bits anterior.

Repetindo esses fluxos rigorosos e lógicos para obter informações adequadas do sistema, podemos tratar corretamente os dados lidos.

Como pular 'nomes de variáveis'

Embora não possamos 'pular' as próprias variáveis, é importante distinguir variáveis usadas das descartadas. Se o uso do programa for claro o suficiente, é melhor usar variáveis sem nome como marcadores de posição do que rotular cada valor, mesmo que não sejam usadas para sempre. Vamos verificar isso com um exemplo, "Verificador de Memória Livre".

Exemplo - Verificador de Memória Livre

Ao verificar a Memória Livre/Swap, não precisamos de outras informações que indicam diferentes recursos. Para obter melhor visibilidade, você pode criar variáveis anônimas para conter espaços específicos.

 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}

Consequentemente, as variáveis são lidas sem rótulos. Embora os valores anônimos sejam de fato armazenados em uma estrutura, não há rótulos ou marcas legíveis no código.

Conclusão

  • Usar syscall e unsafe do Go ainda é mais seguro do que C/CGo
  • Se você estiver escrevendo um projeto grande que possa ser expandido facilmente:
    • Não crie variáveis anônimas; dê nomes a cada membro.
  • Se você estiver escrevendo um projeto de uso limitado:
    • Você pode usar variáveis anônimas para ocupar espaços que realmente não são usados.
  • O syscall do Go é poderoso e moderno para lidar com chamadas de baixo nível

Leia Mais

syscall unsafe x/sys/unix