HQQ: Isolering af C-biblioteker fra Go via delt hukommelse på cache-linjeniveau

gosuda.org/hqq er en zero-cgo runtime for interproceskommunikation og et kodegenereringsværktøjssæt for Go og C11. Det isolerer native C-biblioteker i separate OS-arbejdsprocesser og forbinder dem med Go-kaldere via hukommelsesafbildede, cache-linje-justerede delte hukommelsesringbuffere. Projektet kombinerer to transportniveauer (StandardLink for dedikerede single-producer single-consumer-sessioner og Advanced for multi-worker MPMC-dispatch), hqq-bindgen IDL-compileren og en manipulationsverificeret procesovervåger (secureexec og launch).
1go get gosuda.org/hqq
Omkostningen ved at dele et adresserum med C
Enhver Go-systemingeniør står før det samme vejvalg. Du bygger en Go-tjeneste, der kompileres på få sekunder med CGO_ENABLED=0, distribueres som en enkelt statisk binærfil, krydskompileres fra en laptop til Linux- eller Windows-mål uden et sysroot og kører i en hukommelsessikker, affaldsindsamlet heap. Derefter har du brug for et modent C-bibliotek: SQLite til indlejret relationel lagring, Zstandard (libzstd) til ordbogsbaseret komprimering eller et hardwareaccelereret medie- eller kryptografibibliotek.
Ved at aktivere cgo for at linke C-biblioteket ind i Go-processen løses det umiddelbare bindingsproblem, men det introducerer tre strukturelle ulemper:
- Kobling mellem værktøjskæde og krydskompilering:
CGO_ENABLED=1kræver en C-compiler, C-bibliotekets headere for målet og en platformslinker for hvert mål-OS og hver arkitektur. Opbygning aflinux/arm64- ellerwindows/arm64-artefakter fra endarwin/arm64-arbejdsstation gør et enkeltgo build-kald til en matrix af krydskompilere og sysroots. - Overhead ved runtime-planlægning: Hvert
cgo-kald krydser grænsen til Go-runtime, skifter fra en voksende goroutine-stak til en systemstak og fastlåser en OS-tråd (M). For finkornede kald akkumuleres overgangsomkostningen (~50 til 150 ns per kald) og planlæggerkonkurrencen hurtigt. - Kollaps af fejldomæne: C-kode kører i samme virtuelle adresserum som Go-runtime. Et enkelt skrivning uden for grænserne, use-after-free, stakoverløb eller
abort()inde i en C-funktion korrumperer Go-heap-metadata eller afslutter hele serverprocessen med det samme, under omgåelse afrecover()og tager alle igangværende HTTP- eller RPC-anmodninger med sig.
Når teams forsøger at undgå cgo, tvinger de almindelige alternativer til et andet kompromis. Rene Go-mekaniske oversættelser af C-kodebaser bevarer CGO_ENABLED=0, men halter ofte efter upstream C-udgivelser, medfører et emuleret virtuelt hukommelseslag og allokerer kraftigt under belastning. Flytning af C-biblioteket til en hjælpeproces over loopback-TCP eller Unix-domænesockets genskaber fejlisolering, men hvert kald betaler for to kernelkontekstskift, kopiering af socketbuffere og wire-serialisering.
Vi byggede HQQ ud fra et direkte spørgsmål: Hvad hvis C-biblioteket levede i en separat OS-proces med sit eget virtuelle adresserum, mens Go og C udvekslede rammer direkte gennem delte CPU-cache-linjer uden at gå ind i kernen på den hurtige sti?
| Integrationsmodel | CGO_ENABLED=0 | Fejlisolering | Syscalls på hurtig sti | Heap-allokeringer pr. kald | Upstream C11-troskab |
|---|---|---|---|---|---|
cgo In-Process-linking | Nej | Ingen (delt heap) | 0 | 0 til 1+ | Præcis upstream C |
| Socket / Pipe-underprocess-RPC | Ja | Procesgrænse | 2 til 4 | Flere (indramning + I/O) | Præcis upstream C |
| Mekanisk C-til-Go-oversættelse | Ja | Go i processen | 0 | Høj ved komplekse kald | Emuleret C-runtime |
| HQQ Shared-Memory IPC | Ja | Procesgrænse | 0 (adaptiv spin) | 0 (StandardLink) | Præcis upstream C |
Arkitektonisk oversigt: Fire lag på tværs af procesgrænser
HQQ adskiller procesisolering i fire sammensættelige lag, så mekanikken for delte hukommelsesringbuffere, typede RPC-kodegenereringer, binær integritetsverifikation og supervision af underprocesser forbliver uafhængige.
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 via delt hukommelse (gosuda.org/hqq og c/)
Basispakken gosuda.org/hqq og dens C11-modstykke i c/ implementerer to transporttilstande via delt hukommelse over POSIX mmap (Linux og macOS) og Win32 CreateFileMappingW (Windows). Hvert delte segment begynder med en verificeret HQQ_STL3-superblok, der fastlægger protokolversion, bufferGeometri og endpoint-PID'er, før noget ringindeks læses.
Alle varme kontrolord og slot-headere er justeret til 64-byte cache-linjegrænser, så producent- og forbrugerkerner aldrig konkurrerer om samme cache-linje via falsk deling.
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 +---------------------------------------------------------------------+
| Felt | Offset | Størrelse | Semantik |
|---|---|---|---|
seq | 0x00 | 8 B | Monotont sekvenstæller. Ulige værdier markerer en igangværende skrivning; lige værdier offentliggør en committet ramme via store-release. |
len | 0x08 | 4 B | Payloadens bytelængde skrevet i slotten (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Rammekontrolbits (fortsættelse, fejlstatus eller lukningsbesked). |
payload | 0x40 | BufferSize | Sammenhængende payloadområde, der læses og skrives in place uden mellemliggende kopier. |
Kaldere vælger mellem to transportniveauer afhængigt af samtidighedsstrukturen:
StandardLink(1:1 SPSC-session): Parrer en klient- og en serversession over dedikerede anmodnings- og svarslots beskyttet af en sekvenslås (SeqLock). På den hurtige sti serialiserer skriveren direkte ind i den afbildede slot og committer med et atomisk store-release; læseren spinner kort i brugerrummet, før den falder tilbage til OS-vækkeprimitiver (futexpå Linux,__ulock_waitpå macOS,WaitOnAddresspå Windows). En enkelt rammes round trip fuldføres på ~200 ns med0 allocs/op.Advanced(MPMC-dispatch-ring): MultiplekserMsamtidige Go-kaldere overNC-arbejdertråde ved hjælp af en låsefri multi-producer multi-consumer-indsendelsesring, en delt slotpulje og en indekseret færdiggørelsestabel. Kaldere reserverer en slot, offentliggør slotindekset til ringen og venter på deres færdiggørelsespost, mens arbejdertrådene udtager og færdiggør anmodninger uden for rækkefølge.
Begge niveauer overvåger løbende peers levetid. Hvert endpoint opdaterer et atomisk monotont tidsstempel via Heartbeat() og undersøger PeerAlive(timeout) under ventetiden. Hvis en arbejdsproces går ned midt i et kald, registrerer peeren det forældede heartbeat eller den døde PID med det samme, afbryder blokerede ventetider med ErrPeerGone og understøtter atomisk session-Takeover uden at efterlade hængende delte hukommelsesfiler.
2. Nul-allokerings-RPC og generationskontrollerede handles (gosuda.org/hqq/bindgen)
At skrive indramning af delt hukommelse i hånden til et C-bibliotek med snesevis af funktioner er kedeligt og fejlbehæftet. hqq-bindgen er en skemakompiler, der læser en Interface Definition Language-fil (.idl) (eller opretter en direkte fra en C-.h-header via hqq-bindgen init) og udsender en zero-cgo Go-klientpakke sammen med et C11-serverskelet.
Den farligste fejl i enhver C-binding er at eksponere rå C-pointere (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) over sproggrænsen. Hvis en kalder sender en pointer efter at have frigivet den, sender en pointer, der tilhører en anden session, eller korrumperer en heltalsadresse, dereferencer C-biblioteket ugyldig hukommelse.
hqq-bindgen forbyder rå pointere på wiren. I stedet modellerer IDL'en stateful C-ressourcer som typede handle-deklarationer:
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}
På wiren er hver handle et 64-bit heltal, der pakker et 32-bit tabelindeks og en 32-bit generationstæller forskellig fra nul: (uint64_t)generation << 32 | index. C-runtime (c/src/hqq_rpc.c) vedligeholder en handle-tabel pr. session med tre invarianter:
- Generationsvalidering: Hvert opslag verificerer, at slotten er optaget, at dens lagrede generation svarer til de øverste 32 bit af handlen, og at dens handle-typetag svarer til parametertypen (
Dbvs.Stmt). Genbrug af en lukket handle fejler straks medHQQRPC_E_BAD_HANDLE, før der kaldes ind i C-biblioteket. - Hierarkisk ejerskab (
@owned_by): FordiStmter annoteret med@owned_by(Db), går lukning af enDb-handle automatisk igennem alle levende børne-Stmt-handles, der ejes af denneDb, og kører deres destruktorer i omvendt allokeringsrækkefølge (sqlite3_finalizeførsqlite3_close_v2). - Oprydningsfejning ved session-nedlukning: Hvis en Go-klient afbryder forbindelsen eller går ned uden at lukke sine handles, fejer sessionsdestruktoren på C-serveren hele session-handle-tabellen i omvendt rækkefølge og garanterer nul lækkede C-allokeringer på tværs af klientgenstarter.
Før en session behandler sit første RPC-kald, udveksler klient og server et 32-byte SHA-256-interfacedigest beregnet over den kanoniserede IDL (præfiks HQQ-BINDGEN-IDL-V1\n). Hvis Go-klienten og C-arbejderen er kompileret fra inkompatible IDL-skemaer, afviser håndtrykket forbindelsen ved opstart.
3. Manipulationsfri binær eksekvering (gosuda.org/hqq/secureexec)
At flytte C-kode ind i en arbejderbinærfil rejser et spørgsmål om forsyningskæden: Hvordan ved Go-processen, at arbejderbinærfilen på disken ikke blev ændret eller erstattet mellem verifikation og eksekvering?
Kald af crypto/sha256 på en filsti efterfulgt af os/exec.Command(path).Start() efterlader et Time-of-Check to Time-of-Use (TOCTOU)-vindue, hvor en anden proces kan overskrive eller udskifte filen, før kernen eksekverer den. gosuda.org/hqq/secureexec lukker dette vindue:
- På Linux åbner
secureexecbinærfilen og streamer dens indhold ind i en anonym hukommelsesfil-deskriptor oprettet viamemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING), mens dens SHA-256-hash beregnes. Den verificerer hashen mod et fastlagt 32-byte digest eller et Ed25519-signeret udgivelsesmanifest (cmd/hqq-manifest), anvenderfcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE), så ingen proces på systemet kan ændre siderne, og starter barnet fra den forseglede deskriptor. - På macOS og Windows materialiserer
secureexecverificerede binærfiler i tilladelseslåste cache-mapper, der indekseres efter SHA-256-digest, og verificerer filstørrelse og digest-integritet før opstart.
4. Forælder-livline-procesovervågning (gosuda.org/hqq/launch)
Når en Go-applikation starter en baggrunds-C-arbejderproces, må arbejderen aldrig overleve sin forælder som en forældreløs proces, selv hvis Go-forælderen afsluttes med SIGKILL (kill -9) og ikke kan køre defer-oprydningshandlere.
gosuda.org/hqq/launch håndhæver livstidskobling via en anonym OS-pipe, der sendes til arbejderens standardinput med flaget --parent-stdin. Go-forælderen holder pipens skrivende ende og skriver aldrig til den; C-arbejderen overvåger den læsende ende på en dedikeret livlinetråd. Hvis Go-processen afslutter normalt, går i panik eller dræbes af OS-kernen (SIGKILL eller OOM-dræberen), lukker kernen straks skrivedeskriptoren. C-arbejderen modtager EOF på pipen inden for mikrosekunder, afbryder eventuelle aktive kald, kører sine session-handle-destruktorer, fjerner de delte hukommelsesfiler (unlink) og afslutter rent.
Hurtigstart: Kald af en genereret tjeneste
Med hqq-bindgen og launch ser kald af en isoleret C-arbejder fra Go ud som en almindelig typet Go-klient, samtidig med at CGO_ENABLED=0 bevares:
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}
Repository-struktur
Hvert delsystem i gosuda.org/hqq er organiseret i fokuserede pakker med eksplicitte grænser:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) og lavniveau-primitiver til delte hukommelsesringbuffere
3├── advanced.go / slotpool.go # Advanced (MPMC)-dispatcher, slotpulje og færdiggørelsestabel
4├── shm_*.go / wait_*.go # OS-specifik mmap/CreateFileMapping og futex/ulock/WaitOnAddress
5├── c/ # C11-transportbibliotek (hqq.c) og RPC-serverruntime (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL-parser, verifikator (V1-V41) og Go/C11-kodegenerator
7│ └── rt/ # Nul-allokerings Go-klientruntime for genererede tjenester
8├── secureexec/ # SHA-256 / Ed25519-manifestverifikation og memfd-forseglet eksekvering
9├── launch/ # Arbejderprocesovervåger med --parent-stdin-livlinehåndtering
10├── cmd/hqq-manifest/ # CLI-værktøj til generering og signering af manifester for binære udgivelser
11└── spec/ # Eksekverbare wire-specifikationer og golden test-vektorer (BINDGEN.md)
For at se hvordan HQQ driver drop-in produktionserstatninger for to af de mest udbredte cgo-biblioteker i Go-økosystemet, se de medfølgende pakker bygget på denne stak:
gosuda.org/sqlite3: Drop-indatabase/sql-driver kompatibel medgithub.com/mattn/go-sqlite3, der kører native C SQLite3.54.1over delt hukommelse med pipelinet batch-prefetching.gosuda.org/zstd: Drop-in erstatning forgithub.com/DataDog/zstd, der kører native Clibzstd 1.5.7i en isoleret arbejder med delt hukommelse, med nul-allokerings-kontekster og bulk-batching.- GitHub-repository: https://github.com/gosuda/HQQ