GoSuda

Přímá volání systému Linux v čistém Go pomocí syscall.Syscall a unsafe.Pointer

By Yunjin Lee views ...

Shrnutí

Naučíme se o přímých systémových voláních v Go. Jelikož Go nabízí striktní kontrolu při kompilaci a přísný garbage collector (GC), je mnohem lepší nahradit nízkoúrovňová volání čistým Go. Naštěstí je většina volání funkcí z C plně reimplementována v Go, a to dobrým a moderním způsobem. Podívejme se na to.

Systémové volání

Systémové volání je přímý požadavek na operační systém. Protože systém je obvykle napsán v přísném, staromódním stylu, neboť běží přímo na hardwaru, musíme počítat s tím, že jeho volání musí být předáno v přesné a správné podobě požadavku. Proto, i když některé proměnné nepotřebujeme, musíme přesto vyplnit jejich velikost bez ohledu na to, zda je použijeme. Podívejme se na plně funkční příklad.

Úplný příklad

 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}

Tento příklad obsahuje všechny proměnné a vypisuje podrobné informace o aktuálním stavu systému. Tento kód můžeme přirovnat k zámku a klíči.syscall.SYS_SYSINFO je klíč, který odemyká zámek uvnitř jádra. Proto je důležité použít správný klíč pro daný zámek. Co se stane, když pro toto volání použijeme syscall.SYS_GETPID? Jde o klíč k zámku, který obsahuje ID procesu. Tím se pokusíme získat PID z prostoru určeného pro systémové informace. Výsledkem je, že žádnou z informací nelze přečíst správně; volání musí skončit ve stavu selhání.

Nyní potřebujeme vědět, jaké položky jsou obsaženy a v jakém pořadí jsou uspořádány. V prvním slotu zámku je Uptime o velikosti 2^64. Pokud se pokusíme přečíst tuto hodnotu pomocí 2^32, bitová sekvence nebude přečtena celá. Nemůžeme používat takovéto částečné binární hodnoty, pokud nechceme psát nízkoúrovňové triky.

Po přečtení 64 bitů binárních dat se konečně dostaneme ke druhému slotu. Lze ho přečíst správně až tehdy, když jsme přečetli předchozí 64bitové celé číslo.

Opakováním těchto přísných a logických kroků získáme správné informace o systému a můžeme s přečtenými daty správně pracovat.

Jak přeskočit „názvy proměnných“

I když proměnné samotné nelze „přeskočit“, je důležité rozlišovat mezi použitými a zahozenými proměnnými. Pokud je účel programu dostatečně jasný, je lepší použít anonymní proměnné jako zástupné symboly, než označovat každou hodnotu, i když nebudou nikdy použity. Podívejme se na to na příkladu „Kontrola volné paměti“.

Příklad: Kontrola volné paměti

Při kontrole volné paměti a swapu nepotřebujeme ostatní informace, které označují jiné zdroje. Pro lepší přehlednost můžete vytvořit anonymní proměnné, které zastupují konkrétní prostory.

 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}

Výsledkem je, že hodnoty se čtou bez popisků. Ačkoli se anonymní hodnoty ve skutečnosti ukládají do struktury, v kódu nejsou žádné popisky ani čitelné označení.

Závěr

  • Použití syscall a unsafe v Go je stále bezpečnější než C/CGo
  • Pokud píšete rozsáhlý projekt, který lze snadno rozšiřovat:
    • Nepoužívejte anonymní proměnné; pojmenujte každý člen.
  • Pokud píšete projekt s omezeným použitím:
    • Můžete použít anonymní proměnné k zastoupení prostorů, které jsou skutečně nepoužité.
  • syscall v Go je výkonný a moderní nástroj pro práci s nízkoúrovňovými voláními

Další čtení

syscall unsafe x/sys/unix