GoSuda

HQQ: Izolácia C knižníc od jazyka Go prostredníctvom zdieľanej pamäte na úrovni Cache-Line

By Lemon Mint
views ...

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:

  1. Prepojenie toolchainu a cross-kompilácie: CGO_ENABLED=1 vyž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 pre linux/arm64 alebo windows/arm64 z pracovnej stanice darwin/arm64 mení jediný príkaz go build na maticu cross-kompilátorov a sysrootov.
  2. Režijná záťaž plánovania behu (Runtime Scheduling Overhead): Každé cgo volanie 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ú.
  3. 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íde recover() 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ý modelCGO_ENABLED=0Izolácia zlyhaníSystémové volania rýchlej cestyAlokácie haldy na volanieVěrnosť upstreamu C11
cgo In-Process prepojenieNieŽiadna (zdieľaná halda)00 až 1+Presný upstream C
Socket / Pipe Subprocess RPCÁnoHranica procesu2 až 4Viacnásobné (framing + I/O)Presný upstream C
Mechanický C-to-Go prekladÁnoIn-process Go0Vysoká pri zložitých volaniachEmulovaný C runtime
HQQ Shared-Memory IPCÁnoHranica procesu0 (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              +---------------------------------------------------------------------+
PoleOffsetVeľkosťSémantika
seq0x008 BMonotó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.
len0x084 BDĺžka bajtov užitočného zaťaženia (payload) zapísaná do slotu (0 <= len <= BufferSize).
flags0x0C4 BRámcové riadiace bity (pokračovanie, stav chyby alebo oznámenie o vypnutí).
payload0x40BufferSizeSú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 (futex v systéme Linux, __ulock_wait v systéme macOS, WaitOnAddress v systéme Windows). Jednorámcová spiatočná cesta sa dokončí za ~200 ns s 0 alokáciami/operáciu.
  • Advanced (MPMC dispečerský kruh): Multiplexuje M súbežných volajúcich v Go naprieč N C 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:

  1. 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 (Db vs. Stmt). Opätovné použitie zatvoreného handlu okamžite zlyhá s chybou HQQRPC_E_BAD_HANDLE pred volaním do C knižnice.
  2. Hierarchické vlastníctvo (@owned_by): Keďže Stmt je označený anotáciou @owned_by(Db), zatvorenie handlu Db automaticky prejde všetky živé podriadené handly Stmt vlastnené daným Db a spustí ich deštruktory v opačnom poradí alokácie (sqlite3_finalize pred sqlite3_close_v2).
  3. 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 secureexec otvára binárny súbor, prúdi jeho obsah do anonymného pamäťového deskriptora súboru vytvoreného cez memfd_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), aplikuje fcntl(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 secureexec materializuje 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 pre database/sql ovládač kompatibilná s github.com/mattn/go-sqlite3, ktorá spúšťa natívne C SQLite 3.54.1 nad zdieľanou pamäťou s pipelined dávkovým predbežným načítaním (prefetching).
  • gosuda.org/zstd: Náhrada za github.com/DataDog/zstd, ktorá spúšťa natívnu C knižnicu libzstd 1.5.7 v 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