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

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
syscalleunsafedo 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
syscalldo Go é poderoso e moderno para lidar com chamadas de baixo nível