GoSuda

HQQ: Izolácia C knižníc od Go cez Shared Memory s Cache-Line

By Lemon Mint views ...

gosuda.org/hqq je runtime pre medziprocesovú komunikáciu bez zero-cgo a toolchain na generovanie kódu pre Go a C11. Izoluje natívne C knižnice vo vnútri samostatných procesov OS workerov a prepája ich s volajúcimi v Go prostredníctvom zdieľaných kruhových bufferov (rings) v pamäti mapovanej do pamäte, zarovnaných na hranice cache riadkov. Projekt kombinuje dve úrovne transportu (StandardLink pre dedikované jednosmerné SPSC relácie a Advanced pre viacworkerové MPMC rozosielanie), kompilátor IDL hqq-bindgen a supervízor procesov s overovaním integrity (secureexec a launch).

1go get gosuda.org/hqq

Cena zdieľania adresného priestoru s C

Každý inžinier systémov v Go skôr či neskôr narazí na rovnakú rázcestie. Budujete službu v Go, ktorá sa skompiluje počas niekoľkých sekúnd s CGO_ENABLED=0, distribuuje sa ako jeden statický binárny súbor, krížovo sa kompiluje z notebooku pre cieľové systémy Linux alebo Windows bez sysroot a beží v pamäťovo bezpečnej heape spravovanej garbage collectorom. Potom potrebujete zrelú C knižnicu: SQLite pre vnorené relačné úložisko, Zstandard (libzstd) pre kompresiu so slovníkom alebo hardvérovo akcelerovanú knižnicu pre médiá alebo kryptografiu.

Povolenie cgo na prepojenie tejto C knižnice do procesu Go síce okamžite vyrieši problém väzby, no zavádza tri štrukturálne nevýhody:

  1. Previazanie toolchainu a krížovej kompilácie: CGO_ENABLED=1 vyžaduje C kompilátor, hlavičkové súbory cieľovej libc a platformový linker pre každý cieľový operačný systém a architektúru. Zostavenie artefaktov linux/arm64 alebo windows/arm64 z pracovnej stanice darwin/arm64 mení jediný príkaz go build na maticu krížových kompilátorov a sysroot.
  2. Réžia plánovania behu: Každé volanie cgo prechádza hranicou runtime jazyka Go, prepína sa z rastúceho zásobníka goroutine na systémový zásobník a viaže vlákno OS (M). Pri jemnozrnných volaniach sa daň za prechod (približne 50 až 150 ns na volanie) a konkurencia plánovača rýchlo nahromadia.
  3. Kolaps domény porúch: Kód C beží vo rovnakom virtuálnom adresnom priestore ako runtime Go. Jediný zápis mimo hraníc, použitie po uvoľnení, pretečenie zásobníka alebo abort() vnútri funkcie C poškodí metadáta heapy Go alebo okamžite ukončí celý serverový proces, obíde recover() a so sebou strhne každú prebiehajúcu požiadavku HTTP alebo RPC.

Keď sa tímy snažia vyhnúť cgo, bežné alternatívy vynucujú iný kompromis. Mechanické preklady kódovej základne C do čistého Go zachovávajú CGO_ENABLED=0, no často zaostávajú za upstream vydaniami C, nesú emulovanú vrstvu virtuálnej pamäte a pri záťaži intenzívne alokujú. Presun knižnice C do pomocného procesu cez loopback TCP alebo Unix doménové sokety obnoví izoláciu porúch, no každé volanie platí dvoma prepnutiami kontextu jadra, kopírovaním vyrovnávacích pamätí soketov a serializáciou na drôte.

HQQ sme vytvorili po položení priamej otázky: čo ak by knižnica C žila v samostatnom procese OS s vlastným virtuálnym adresným priestorom, zatiaľ čo Go a C by si rámce vymieňali priamo prostredníctvom zdieľaných riadkov cache procesora bez vstupu do jadra na rýchlej ceste?

Model integrácieCGO_ENABLED=0Izolácia porúchSystémové volania na rýchlej cesteAlokácie na heape na volanieVerná kópia upstream C11
cgo lineárne prepojenie v proceseNieŽiadna (zdieľaná heap)00 až 1+Presné upstream C
RPC podprocesu cez sokety / rúryÁnoHranica procesu2 až 4Viacnásobné (rámcovanie + I/O)Presné upstream C
Mechanický preklad C do GoÁnoGo v rámci procesu0Vysoké pri zložitých volaniachEmulovaný runtime C
HQQ IPC so zdieľanou pamäťouÁnoHranica procesu0 (adaptívne spinovanie)0 (StandardLink)Presné upstream C

Architektonický prehľad: štyri vrstvy naprieč hranicami procesov

HQQ rozdeľuje izoláciu procesov do štyroch skladateľných vrstiev, aby mechanika kruhových bufferov v zdieľanej pamäti, typovo bezpečné generovanie kódu RPC, overovanie binárnej integrity a správa podriadených procesov zostali nezávislé.

 1+-------------------------------------------------------------------+
 2| Aplikačný proces Go (CGO_ENABLED=0)                               |
 3|                                                                   |
 4|  +-----------------------+     +-------------------------------+  |
 5|  | Generovaný klient Go  |     | Supervízor procesov           |  |
 6|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
 7|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
 8|  +-----------+-----------+     +---------------+---------------+  |
 9|              |                                 |                  |
10|  +-----------v-----------+                     | Anonymná rúra    |
11|  | Transportná úroveň    |                     | Lifeline         |
12|  | (StandardLink /       |                     | (--parent-stdin) |
13|  |  Advanced MPMC)       |                     |                  |
14+--------------+---------------------------------+------------------+
15               |                                 |
16===============|========= Zdieľaná pamäť =========|==================
17               |     (mmap / CreateFileMapping)  |
18+--------------v---------------------------------v------------------+
19| Worker proces C11 (izolovaný adresný priestor)                    |
20|                                                                   |
21|  +-----------------------+     +-------------------------------+  |
22|  | Generovaný server C11 |---->| Tabuľka handle s kontrolou    |  |
23|  | a dávkové shimy       |     | generácie (hqq_rpc.c)         |  |
24|  +-----------+-----------+     +---------------+---------------+  |
25|              |                                 |                  |
26|              +----------------+----------------+                  |
27|                               |                                   |
28|                     +---------v---------+                         |
29|                     | Natívna knižnica C|                         |
30|                     | (SQLite, Zstd...) |                         |
31|                     +-------------------+                         |
32+-------------------------------------------------------------------+

1. Transport so zdieľanou pamäťou (gosuda.org/hqq a c/)

Základný balík gosuda.org/hqq a jeho náprotivok v C11 v adresári c/ implementujú dva režimy transportu so zdieľanou pamäťou nad POSIX mmap (Linux a macOS) a Win32 CreateFileMappingW (Windows). Každý zdieľaný segment začína overeným superblokom HQQ_STL3, ktorý pevne určuje verziu protokolu, geometriu bufferov a PID koncových bodov skôr, než sa prečíta akýkoľvek index kruhu.

Všetky horúce riadiace slová a hlavičky slotov sú zarovnané na hranice cache riadkov po 64 bajtoch, aby jadrá producenta a konzumenta nikdy nesúperili o rovnaký cache riadok vplyvom falošného zdieľania.

1Rozloženie slotu StandardLink (zarovnané na cache riadok 64 bajtov)
2Posun v bajtoch:  0x00              0x08              0x10                          0x40
3                  +-------------------+-------------------+-----------------------------+
4Riadok hlavičky:  | seq (u64 atomic)  | len (u32) | flags | rezerva (doplnená nulami)   |
5                  +-------------------+-------------------+-----------------------------+
6Oblasť payloadu:  | bajty payloadu [0 .. BufferSize - 1] (kódované / dekódované priamo)  |
7                  +---------------------------------------------------------------------+
PolePosunVeľkosťVýznam
seq0x008 BMonotónny sekvenčný čítač. Nepárne hodnoty označujú zápis v procese; párne hodnoty publikujú potvrdený rámec cez store-release.
len0x084 BDĺžka payloadu v bajtoch zapísaného do slotu (0 <= len <= BufferSize).
flags0x0C4 BRiadiace bity rámca (pokračovanie, stav chyby alebo oznámenie o ukončení).
payload0x40BufferSizeSúvislá oblasť payloadu, čítaná a zapisovaná na mieste bez medziľahlých kópií.

Volajúci si vyberajú medzi dvoma úrovňami transportu podľa štruktúry súbežnosti:

  • StandardLink (SPSC relácia 1:1): Spája klientsku a serverovú reláciu cez dedikované požiadavkové a odpoveďové sloty chránené sekvenčným zámkom (SeqLock). Na rýchlej ceste zapisovateľ serializuje údaje priamo do mapovaného slotu a potvrdí ich atomickým store-release; čitateľ krátko spinuje v používateľskom priestore a až potom sa uchýli k primitívam prebudenia OS (futex na Linuxe, __ulock_wait na macOS, WaitOnAddress na Windows). Jeden rámec v rámci kompletného cyklu sa vykoná približne za 200 ns s 0 allocs/op.
  • Advanced (MPMC rozosielací kruh): Multiplexuje M súbežných volajúcich v Go cez N vlákien workerov C pomocou bezzámkového viacproducentského viackonzumentského odoslacieho kruhu, zdieľaného fondu slotov a indexovanej tabuľky dokončení. Volajúci si rezervujú slot, publikujú jeho index do kruhu a čakajú na svoj záznam o dokončení, zatiaľ čo vlákna workerov vyberajú požiadavky a dokončujú ich v ľubovoľnom poradí.

Obe úrovne priebežne sledujú dostupnosť partnera. Každý koncový bod aktualizuje atomickú monotónnu časovú pečiatku cez Heartbeat() a počas čakania kontroluje PeerAlive(timeout). Ak proces workera uprostred volania zlyhá, partner okamžite zistí neaktuálny heartbeat alebo mŕtvy PID, preruší blokované čakania s ErrPeerGone a podporuje atomické prevzatie relácie (Takeover) bez toho, aby zostali zaseknuté súbory zdieľanej pamäte.

2. RPC bez alokácií a handle s kontrolou generácie (gosuda.org/hqq/bindgen)

Ručné písanie rámcovania zdieľanej pamäte pre C knižnicu s desiatkami funkcií je únavné a náchylné na chyby. hqq-bindgen je kompilátor schém, ktorý načíta súbor Interface Definition Language (.idl) (alebo z hlavičky C .h priamo vytvorí počiatočný súbor príkazom hqq-bindgen init) a vygeneruje balík klienta Go bez zero-cgo spolu so skeletonom servera C11.

Najnebezpečnejšou chybou v akejkoľvek väzbe na C je vystavenie surových ukazovateľov C (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) cez hranicu jazyka. Ak volajúci odovzdá ukazovateľ po jeho uvoľnení, odovzdá ukazovateľ patriaci inej relácii alebo poškodí celočíselnú adresu, knižnica C dereferencuje neplatnú pamäť.

hqq-bindgen zakazuje surové ukazovatele na drôte. Namiesto toho IDL modeluje stavové prostriedky C ako typované deklarácie handle:

1service sqlite3 {
2    handle Db;
3    handle Stmt @owned_by(Db);
4
5    fn open_v2(path: string, flags: i32) -> result<Db, i32>;
6    fn prepare_one(db: Db, sql: string) -> result<Stmt, i32>;
7    fn finalize(stmt: @consume Stmt) -> result<void, i32>;
8    fn close(db: @consume Db) -> result<void, i32>;
9}

Na drôte je každý handle 64-bitové celé číslo, ktoré obsahuje 32-bitový index tabuľky a 32-bitový nenulový čítač generácie: (uint64_t)generation << 32 | index. Runtime C (c/src/hqq_rpc.c) udržiava tabuľku handle pre každú reláciu s tromi invariantmi:

  1. Overenie generácie: Každé vyhľadanie overí, že slot je obsadený, jeho uložená generácia sa zhoduje s hornými 32 bitmi handle a jeho typová značka handle zodpovedá typu parametra (Db verzus Stmt). Opätovné použitie uzavretého handle zlyhá okamžite s HQQRPC_E_BAD_HANDLE ešte pred volaním knižnice C.
  2. Hierarchické vlastníctvo (@owned_by): Keďže Stmt je anotovaný @owned_by(Db), uzavretie handle Db automaticky prejde všetky živé podriadené handle Stmt patriace tomuto Db a spustí ich deštruktory v opačnom poradí alokácie (sqlite3_finalize pred sqlite3_close_v2).
  3. Čistenie pri ukončení relácie: Ak sa klient Go odpojí alebo zlyhá bez uzavretia svojich handle, deštruktor relácie na serveri C prejde celú tabuľku handle relácie v opačnom poradí a zaručí nulový únik alokácií C pri reštartoch klienta.

Skôr než relácia spracuje prvé volanie RPC, klient a server si vymenia 32-bajtový digest rozhrania SHA-256 vypočítaný nad kanonizovaným IDL (predpona HQQ-BINDGEN-IDL-V1\n). Ak boli klient Go a worker C skompilované z nekompatibilných schém IDL, handshake pripojenie už pri inicializácii odmietne.

3. Binárne spúšťanie odolné voči manipulácii (gosuda.org/hqq/secureexec)

Presun kódu C do binárneho súboru workera vyvoláva otázku dodávateľského reťazca: ako proces Go zistí, že binárny súbor workera na disku nebol medzi overením a spustením zmenený alebo nahradený?

Výpočet crypto/sha256 nad cestou k súboru, za ktorým nasleduje os/exec.Command(path).Start(), ponecháva okno Time-of-Check to Time-of-Use (TOCTOU), v ktorom môže iný proces súbor prepísať alebo vymeniť skôr, než ho jadro spustí. gosuda.org/hqq/secureexec toto okno uzatvára:

  • Na Linuxe secureexec otvorí binárny súbor, preloží jeho obsah do anonymného deskriptora pamäťového súboru vytvoreného cez memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING) a zároveň vypočíta jeho hash SHA-256, overí hash voči pevne určenému 32-bajtovému digestu alebo manifestu vydania podpísanému Ed25519 (cmd/hqq-manifest), aplikuje fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE), aby žiadny proces v systéme nemohol stránky modifikovať, a potomka spustí z zapečateného deskriptora.
  • Na macOS a Windows secureexec materializuje overené binárne súbory do adresárov vyrovnávacej pamäte s uzamknutými oprávneniami, ktorých názvy sú odvodené od digestu SHA-256, a pred spustením overuje integritu veľkosti súboru a digestu.

4. Supervízia procesu s väzbou na rodiča (gosuda.org/hqq/launch)

Keď aplikácia Go spustí proces workera C na pozadí, worker nesmie nikdy prežiť svojho rodiča ako osirelý proces, ani keď je rodičovský proces Go ukončený signálom SIGKILL (kill -9) a nemôže spustiť obslužné rutiny defer na upratovanie.

gosuda.org/hqq/launch vynucuje previazanie životnosti cez anonymnú rúru OS odovzdanú na štandardný vstup workera s príznakom --parent-stdin. Rodičovský proces Go drží zapisovací koniec rúry a nikdy doňho nezapisuje; worker C sleduje čítací koniec na vyhradenom vlákne lifeline. Ak proces Go skončí normálne, spadne panikou alebo je ukončený jadrom OS (SIGKILL alebo OOM killer), jadro okamžite uzavrie zapisovací deskriptor. Worker C dostane EOF na rúre v rámci mikrosekúnd, preruší všetky aktívne volania, spustí deštruktory svojich handle relácie, odstráni prepojenia súborov zdieľanej pamäte a čisto skončí.

Rýchly štart: volanie generovanej služby

S hqq-bindgen a launch sa volanie izolovaného workera C z Go správa ako bežný typovaný klient Go, pričom zostáva zachované CGO_ENABLED=0:

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"log"
 7	"time"
 8
 9	"gosuda.org/hqq/bindgen/rt"
10	"gosuda.org/hqq/launch"
11	"gosuda.org/hqq/secureexec"
12)
13
14func main() {
15	// Vytvorenie kontextu s časovým limitom 5 sekúnd
16	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
17	defer cancel()
18
19	// Spracovanie pevne určeného digestu SHA-256 overovaného workera
20	hash, err := secureexec.ParseHash("7649b1b45dacd934cde8149f35f4fcc3937f6486fbac23ce976466719b3752bb")
21	if err != nil {
22		log.Fatal(err)
23	}
24
25	// Vytvorenie názvu listenera pre zdieľanú pamäť
26	listener, err := launch.NewListenerName("demo")
27	if err != nil {
28		log.Fatal(err)
29	}
30
31	// Spustenie overeného workera s pripojením na listener
32	srv, err := launch.Start(ctx, launch.Config{
33		Binary:   "/path/to/verified-worker",
34		SHA256:   hash,
35		Listener: listener,
36		Args:     []string{listener},
37	})
38	if err != nil {
39		log.Fatal(err)
40	}
41	defer srv.Close()
42
43	fmt.Printf("worker beží na zdieľanom listeneri %s (opts=%+v)\n", listener, rt.SessionOptions{})
44}

Štruktúra repozitára

Každý subsystém v gosuda.org/hqq je organizovaný do zameraných balíkov s jasne vymedzenými hranicami:

 1gosuda.org/hqq
 2├── standard.go / ring.go      # StandardLink (SPSC) a nízkoúrovňové primitíva kruhu zdieľanej pamäte
 3├── advanced.go / slotpool.go  # Rozosielač Advanced (MPMC), fond slotov a tabuľka dokončení
 4├── shm_*.go / wait_*.go       # Špecifické pre OS: mmap/CreateFileMapping a futex/ulock/WaitOnAddress
 5├── c/                         # Transportná knižnica C11 (hqq.c) a runtime servera RPC (hqq_rpc.c)
 6├── bindgen/                   # Parser IDL hqq-bindgen, verifikátor (V1-V41) a generátor kódu Go/C11
 7│   └── rt/                    # Runtime klienta Go bez alokácií pre generované služby
 8├── secureexec/                # Overenie manifestu SHA-256 / Ed25519 a spúšťanie zapečateného cez memfd
 9├── launch/                    # Supervízor workerov s riadením lifeline cez --parent-stdin
10├── cmd/hqq-manifest/          # CLI nástroj na generovanie a podpisovanie manifestov binárnych vydaní
11└── spec/                      # Spustiteľné špecifikácie drôtu a referenčné testovacie vektory (BINDGEN.md)

Ak chcete vidieť, ako HQQ poháňa priamu produkčnú náhradu za dve z najpoužívanejších knižníc cgo v ekosystéme Go, pozrite si doplnkové balíky postavené na tomto zásobníku:

  • gosuda.org/sqlite3: Priamy ovládač database/sql kompatibilný s github.com/mattn/go-sqlite3, ktorý beží nad natívnym C SQLite 3.54.1 cez zdieľanú pamäť s pipelinovým predbežným načítavaním dávok.
  • gosuda.org/zstd: Priama náhrada za github.com/DataDog/zstd, ktorá beží nad natívnym C libzstd 1.5.7 v izolovanom workeri so zdieľanou pamäťou, s kontextami bez alokácií a hromadným dávkovaním.
  • Repozitár na GitHube: https://github.com/gosuda/HQQ