HQQ: Izolácia C knižníc od jazyka Go prostredníctvom zdieľanej pamäte na úrovni Cache-Line
gosuda.org/hqq je zero-cgo inter-process communication runtime a kód generujúci toolchain pre jazyky Go a C11. Izoluje natívne C knižnice v rámci separátnych OS worker procesov a prepája ich s Go volajúcimi prostredníctvom pamäťovo mapovaných, na cache line zarovnaných shared-memory kruhových vyrovnávacích pamätí. Projekt kombinuje dve transportné vrstvy (StandardLink pre vyhradené single-producer single-consumer relácie a Advanced pre multi-worker MPMC dispečing), hqq-bindgen IDL kompilátor a procesový supervisor overujúci neporušenosť (secureexec a launch).
1go get gosuda.org/hqq
Náklady na zdieľanie adresného priestoru s C
Každý systémový inžinier v Go nakoniec narazí na rovnakú rázcestie. Budujete Go službu, ktorá sa skompiluje za pár sekúnd s CGO_ENABLED=0, distribuuje sa ako jeden statický binárny súbor, cross-kompiluje sa z notebooku na ciele Linux alebo Windows bez sysrootu a beží v rámci pamäťovo bezpečnej haldy spravovanej garbage collectorom. Potom však potrebujete zrelú C knižnicu: SQLite pre zabudované relačné úložisko, Zstandard (libzstd) pre kompresiu pomocou slovníka, alebo hardvérovo akcelerovanú knižnicu pre médiá či kryptografiu.
Povolenie cgo na prepojenie tejto C knižnice do Go procesu rieši okamžitý problém väzby, no zároveň zavádza tri štrukturálne záväzky:
- Prepojenie toolchainu a cross-kompilácie:
CGO_ENABLED=1vyžaduje C kompilátor, cieľové libc hlavičkové súbory a platformový linker pre každý cieľový OS a architektúru. Budovanie artefaktov prelinux/arm64alebowindows/arm64z pracovnej stanicedarwin/arm64mení jediný príkazgo buildna maticu cross-kompilátorov a sysrootov. - Režijná záťaž plánovania behu (Runtime Scheduling Overhead): Každé
cgovolanie prekračuje hranicu Go runtime, prepína sa zo zväčšiteľného stacku gorutiny na systémový stack a fixuje jedno OS vlákno (M). Pri jemnozrnných volaniach sa prechodová daň (~50 až 150 ns na jedno volanie) a konkurencia v plánovači rýchlo kumulujú. - Kolaps domény zlyhaní (Fault Domain Collapse): C kód beží v rovnakom virtuálnom adresnom priestore ako Go runtime. Jediný zápis mimo vyhradeného rozsahu, použitie uvoľnenej pamäte (use-after-free), pretečenie stacku alebo
abort()vo vnútri C funkcie korumpuje metadáta Go haldy alebo okamžite ukončí celý proces servera, pričom obíderecover()a stiahne so sebou každú prebiehajúcu HTTP alebo RPC požiadavku.
Keď sa tímy snažia vyhnúť cgo, bežné alternatívy vynucujú iný kompromis. Čisto Go mechanické preklady C kódových základní zachovávajú CGO_ENABLED=0, ale často zaostávajú za upstream C vydaniami, nesú so sebou emulovanú vrstvu virtuálnej pamäte a pri zaťažení masívne alokujú. Presun C knižnice do pomocného procesu cez loopback TCP alebo Unixové doménové sockety obnovuje izoláciu chýb, no každé volanie platí za dva kontextové prepínače jadra, kópie socketových bufferov a sieťovú serializáciu.
HQQ sme vytvorili po položenie priamej otázky: čo keby C knižnica žila v samostatnom OS procese s vlastným virtuálnym adresným priestorom, zatiaľ čo Go a C vymieňali rámce priamo cez zdieľané CPU cache line bez vstupu do jadra na rýchlej ceste?
| Integračný model | CGO_ENABLED=0 | Izolácia zlyhaní | Systémové volania rýchlej cesty | Alokácie haldy na volanie | Věrnosť upstreamu C11 |
|---|---|---|---|---|---|
cgo In-Process prepojenie | Nie | Žiadna (zdieľaná halda) | 0 | 0 až 1+ | Presný upstream C |
| Socket / Pipe Subprocess RPC | Áno | Hranica procesu | 2 až 4 | Viacnásobné (framing + I/O) | Presný upstream C |
| Mechanický C-to-Go preklad | Áno | In-process Go | 0 | Vysoká pri zložitých volaniach | Emulovaný C runtime |
| HQQ Shared-Memory IPC | Áno | Hranica procesu | 0 (adaptívne točenie) | 0 (StandardLink) | Presný upstream C |
Architektonický prehľad: Štyri vrstvy naprieč hranicami procesov
HQQ oddeľuje izoláciu procesov do štyroch kompozovateľných vrstiev, takže mechanika zdieľaných pamäťových kruhov, generovanie typovaného RPC kódu, overovanie binárnej integrity a dohľad nad podriadeným procesom zostávajú nezávislé.
1+-------------------------------------------------------------------+
2| Go Application Process (CGO_ENABLED=0) |
3| |
4| +-----------------------+ +-------------------------------+ |
5| | Generated Go Client | | Process Supervisor | |
6| | (gosuda.org/hqq/ | | (gosuda.org/hqq/launch + | |
7| | bindgen/rt) | | gosuda.org/hqq/secureexec) | |
8| +-----------+-----------+ +---------------+---------------+ |
9| | | |
10| +-----------v-----------+ | Anonymous Pipe |
11| | Transport Tier | | Lifeline |
12| | (StandardLink / | | (--parent-stdin) |
13| | Advanced MPMC) | | |
14+--------------+---------------------------------+------------------+
15 | |
16===============|========= Shared Memory =========|===================
17 | (mmap / CreateFileMapping) |
18+--------------v---------------------------------v------------------+
19| C11 Worker Process (Isolated Address Space) |
20| |
21| +-----------------------+ +-------------------------------+ |
22| | Generated C11 Server |---->| Generation-Checked Handle | |
23| | & Batch Shims | | Table (hqq_rpc.c) | |
24| +-----------+-----------+ +---------------+---------------+ |
25| | | |
26| +----------------+----------------+ |
27| | |
28| +---------v---------+ |
29| | Native C Library | |
30| | (SQLite, Zstd...) | |
31| +-------------------+ |
32+-------------------------------------------------------------------+
1. Transport cez zdieľanú pamäť (gosuda.org/hqq a c/)
Základný balík gosuda.org/hqq a jeho C11 náprotivok v c/ implementujú dva transportné režimy zdieľanej pamäte prostredníctvom POSIX mmap (Linux a macOS) a Win32 CreateFileMappingW (Windows). Každý zdieľaný segment začína overeným superblokom HQQ_STL3, ktorý fixuje verziu protokolu, geometriu bufferu a PID koncových bodov predtým, než sa prečíta akýkoľvek index kruhovej pamäte.
Všetky kritické riadiace slová a hlavičky slotov sú zarovnané na hranice 64-bytových cache line, takže producenti a konzumenti nikdy nesúperia o tú istú cache line prostredníctvom falošného zdieľania (false sharing).
1StandardLink Slot Layout (64-Byte Cache-Line Aligned)
2Byte Offset: 0x00 0x08 0x10 0x40
3 +-------------------+-------------------+-----------------------------+
4Header Line: | seq (u64 atomic) | len (u32) | flags | reserved (zero-padded) |
5 +-------------------+-------------------+-----------------------------+
6Payload Area: | payload bytes [0 .. BufferSize - 1] (directly encoded / decoded) |
7 +---------------------------------------------------------------------+
| Pole | Offset | Veľkosť | Sémantika |
|---|---|---|---|
seq | 0x00 | 8 B | Monotónne počítadlo sekvencií. Nepárne hodnoty označujú prebiehajúci zápis; párne hodnoty publikujú zverejnený rámec pomocou operácie store-release. |
len | 0x08 | 4 B | Dĺžka bajtov užitočného zaťaženia (payload) zapísaná do slotu (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Rámcové riadiace bity (pokračovanie, stav chyby alebo oznámenie o vypnutí). |
payload | 0x40 | BufferSize | Súvislá oblasť užitočného zaťaženia čítaná a zapisovaná na mieste bez medzikópií. |
Volajúci si vyberajú medzi dvoma transportnými vrstvami v závislosti od štruktúry súbežnosti:
StandardLink(1:1 SPSC relácia): Spája klientsku a serverovú reláciu cez vyhradené sloty požiadaviek a odpovedí chránené sekvenčným zámkom (SeqLock). Na rýchlej ceste zapisovateľ serializuje priamo do mapovaného slotu a potvrdzuje zápis pomocou atomického store-release; čitateľ chvíľu rotuje v používateľskom priestore pred prechodom na primitiva prebúdzania OS (futexv systéme Linux,__ulock_waitv systéme macOS,WaitOnAddressv systéme Windows). Jednorámcová spiatočná cesta sa dokončí za ~200 ns s0 alokáciami/operáciu.Advanced(MPMC dispečerský kruh): MultiplexujeMsúbežných volajúcich v Go napriečNC pracovnými vláknami pomocou bezzámkového multi-producer multi-consumer odovzdávacieho kruhu, zdieľaného poolu slotov a indexovanej tabuľky dokončenia. Volajúci si rezervujú slot, publikujú index slotu do kruhu a čakajú na svoju položku dokončenia, zatiaľ čo pracovné vlákna vyberajú a dokončujú požiadavky mimo poradia.
Obidve vrstvy nepretržite sledujú živosť partnera. Každý koncový bod aktualizuje atomickú monotónnu časovú pečiatku cez Heartbeat() a kontroluje PeerAlive(timeout) počas čakania. Ak proces pracovníka havaruje uprostred volania, druhý partner okamžite detekuje zastaranú srdcovú tepovú frekvenciu alebo mŕtvy PID, preruší zablokované čakanie s ErrPeerGone a podporuje atomické prevzatie relácie (Takeover) bez zanechania osirotených súborov zdieľanej pamäte.
2. RPC bez alokácií a handly overené generáciou (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 schémový kompilátor, ktorý číta súbor definície rozhrania (Interface Definition Language – .idl) (alebo ho priamo bootstrapuje z hlavičkového C .h súboru cez hqq-bindgen init) a generuje zero-cgo Go klientsky balík spolu s kostrou servera C11.
Najnebezpečnejšou chybou v akejkoľvek C väzbe je vystavenie surových C ukazovateľov (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) cez hranicu jazyka. Ak volajúci prenesie ukazovateľ po jeho uvoľnení, prenesie ukazovateľ patriaci inej relácii alebo skompromituje celočíselnú adresu, C knižnica dereferencuje neplatnú pamäť.
hqq-bindgen zakazuje surové ukazovatele na drôte. Namiesto toho IDL modeluje stavové C zdroje 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 obaľujúce 32-bitový index tabuľky a 32-bitové nenulové počítadlo generácií: (uint64_t)generation << 32 | index. C runtime (c/src/hqq_rpc.c) udržiava tabuľku handlov pre každú reláciu s tromi invariantmi:
- Overenie generácie: Každé vyhľadávanie overuje, či je slot obsadený, či jeho uložená generácia zodpovedá horným 32 bitom handlu a či značka typu handlu zodpovedá typu parametra (
Dbvs.Stmt). Opätovné použitie zatvoreného handlu okamžite zlyhá s chybouHQQRPC_E_BAD_HANDLEpred volaním do C knižnice. - Hierarchické vlastníctvo (
@owned_by): KeďžeStmtje označený anotáciou@owned_by(Db), zatvorenie handluDbautomaticky prejde všetky živé podriadené handlyStmtvlastnené danýmDba spustí ich deštruktory v opačnom poradí alokácie (sqlite3_finalizepredsqlite3_close_v2). - Upratovanie pri ukončení relácie: Ak sa Go klient odpojí alebo zlyhá bez zatvorenia svojich handlov, deštruktor relácie C servera prejde celú tabuľku handlov relácie v opačnom poradí, čo zaručuje nulové unikanie C alokácií naprieč reštartmi klienta.
Predtým, než relácia spracuje svoje prvé RPC volanie, klient a server si vymenia 32-bytový SHA-256 digest rozhrania vypočítaný nad kanonizovaným IDL (s predponou HQQ-BINDGEN-IDL-V1\n). Ak boli Go klient a C pracovník skompilovaní z nekompatibilných IDL schém, handshake odmietne pripojenie pri zavádzkovaní.
3. Bezpečná binárna exekúcia bez falšovania (gosuda.org/hqq/secureexec)
Presun C kódu do binárneho súboru pracovníka vyvoláva otázku dodávateľského reťazca: ako Go proces vie, že binárny súbor pracovníka na disku nebol modifikovaný alebo nahradený medzi overením a spustením?
Volanie crypto/sha256 na cestu k súboru, po ktorom nasleduje os/exec.Command(path).Start(), vytvára okno medzi časom kontroly a časom použitia (Time-of-Check to Time-of-Use – TOCTOU), v ktorom môže iný proces prepísať alebo vymeniť súbor predtým, než ho jadro spustí. gosuda.org/hqq/secureexec toto okno uzatvára:
- V systéme Linux
secureexecotvára binárny súbor, prúdi jeho obsah do anonymného pamäťového deskriptora súboru vytvoreného cezmemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)počas výpočtu jeho SHA-256 hash, overuje hash voči fixovanému 32-bytovému digestu alebo manifestu vydania podpísanému kľúčom Ed25519 (cmd/hqq-manifest), aplikujefcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE), takže žiadny proces v systéme nemôže mutovať stránky, a spúšťa potomka z uzatvoreného deskriptora. - V systémoch macOS a Windows
secureexecmaterializuje overené binárne súbory do adresárov vyrovnávacej pamäte chránených oprávneniami, ktoré sú indexované pomocou SHA-256 digestu, a pred spustením overuje veľkosť súboru a integritu digestu.
4. Dohľad nad procesom prostredníctvom záchranného lana rodiča (gosuda.org/hqq/launch)
Keď Go aplikácia spúšťa proces pozadia C pracovníka, pracovník nesmie nikdy prežiť svojho rodiča ako osirotený proces, a to ani vtedy, keď je Go rodič ukončený príkazom SIGKILL (kill -9) a nemôže spustiť obslužné programy pre čistenie pomocou príkazu defer.
gosuda.org/hqq/launch vynucuje prepojenie životnosti prostredníctvom anonymnej OS rúry (pipe) prejdenej do štandardného vstupu pracovníka s príznakom --parent-stdin. Go rodič drží zapisovací koniec rúry a nikdy doň nezapisuje; C pracovník monitoruje čítací koniec na vyhradenom záchrannom vlákne (lifeline thread). Ak Go proces normálne skončí, vyvolá paniku alebo je usmrtený jadrom OS (pomocou SIGKILL alebo OOM killera), jadro okamžite zatvorí zapisovací deskriptor. C pracovník prijme EOF na rúre v priebehu mikrosekúnd, preruší akékoľvek aktívne volania, spustí deštruktory handlov relácie, odpojí súbory zdieľanej pamäte a čistým spôsobom skončí.
Rýchly štart: Volanie generovanej služby
S modulmi hqq-bindgen a launch vyzerá volanie izolovaného C pracovníka z Go ako bežný typovaný Go klient pri zachovaní nastavenia 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 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
16 defer cancel()
17
18 hash, err := secureexec.ParseHash("7649b1b45dacd934cde8149f35f4fcc3937f6486fbac23ce976466719b3752bb")
19 if err != nil {
20 log.Fatal(err)
21 }
22
23 listener, err := launch.NewListenerName("demo")
24 if err != nil {
25 log.Fatal(err)
26 }
27
28 srv, err := launch.Start(ctx, launch.Config{
29 Binary: "/path/to/verified-worker",
30 SHA256: hash,
31 Listener: listener,
32 Args: []string{listener},
33 })
34 if err != nil {
35 log.Fatal(err)
36 }
37 defer srv.Close()
38
39 fmt.Printf("worker running on shared-memory listener %s (opts=%+v)\n", listener, rt.SessionOptions{})
40}
Štruktúra repozitára
Každý subsystém v gosuda.org/hqq je organizovaný do zacielených balíkov s explicitnými hranicami:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) a nízkoúrovňové primitíva kruhovej pamäte zdieľanej pamäte
3├── advanced.go / slotpool.go # Pokročilý (MPMC) dispečer, pool slotov a tabuľka dokončenia
4├── shm_*.go / wait_*.go # OS-špecifické mmap/CreateFileMapping a futex/ulock/WaitOnAddress
5├── c/ # C11 transportná knižnica (hqq.c) a runtime RPC servera (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL parser, overovač (V1-V41) a generátor kódu Go/C11
7│ └── rt/ # Zero-allocation Go klientsky runtime pre generované služby
8├── secureexec/ # Overenie manifestu SHA-256 / Ed25519 a exekúcia utesnená cez memfd
9├── launch/ # Supervisor procesu pracovníka so správou záchranného lana --parent-stdin
10├── cmd/hqq-manifest/ # CLI nástroj na generovanie a podpisovanie manifestov binárnych súborov vydania
11└── spec/ # Vykonateľné sieťové špecifikácie a zlaté testovacie vektory (BINDGEN.md)
Ak chcete zistiť, ako HQQ poháňa produkčné náhrady pre dve z najširšie používaných cgo knižníc v ekosystéme Go priamo na mieste, pozrite si sprievodné balíky postavené na tejto štruktúre:
gosuda.org/sqlite3: Náhrada predatabase/sqlovládač kompatibilná sgithub.com/mattn/go-sqlite3, ktorá spúšťa natívne C SQLite3.54.1nad zdieľanou pamäťou s pipelined dávkovým predbežným načítaním (prefetching).gosuda.org/zstd: Náhrada zagithub.com/DataDog/zstd, ktorá spúšťa natívnu C knižniculibzstd 1.5.7v izolovanom worker procese so zdieľanou pamäťou, s kontextmi bez alokácií a hromadným dávkovaním.- Repozitár na GitHub: https://github.com/gosuda/HQQ