HQQ: Isolering av C-biblioteker fra Go over Cache-Line Shared Memory

gosuda.org/hqq er en zero-cgo kjøretid for interprosesskommunikasjon og verktøykjede for kodegenerering for Go og C11. Den isolerer native C-biblioteker i separate operativsystemprosesser for arbeidere og kobler dem til Go-kallere via minnekartlagte, cache-linjejusterte delte minneringer. Prosjektet kombinerer to transportnivåer (StandardLink for dedikerte single-producer single-consumer-sesjoner og Advanced for multi-worker MPMC-dispatch), IDL-kompilatoren hqq-bindgen, og en prosessovervåker med manipulasjonsverifisering (secureexec og launch).
1go get gosuda.org/hqq
Kostnaden ved å dele adresserom med C
Enhver Go-systemingeniør står før slutt det samme veivalget. Du bygger en Go-tjeneste som kompileres på sekunder med CGO_ENABLED=0, leveres som en enkelt statisk binærfil, krysskompileres fra en bærbar datamaskin til Linux- eller Windows-mål uten sysroot, og kjører i en minnesikker, søppelinnsamlet heap. Deretter trenger du et modent C-bibliotek: SQLite for innebygd relasjonslagring, Zstandard (libzstd) for ordbokbasert komprimering, eller et maskinvareakselerert medie- eller kryptografibibliotek.
Ved å aktivere cgo for å lenke C-biblioteket inn i Go-prosessen løses det umiddelbare bindingsproblemet, men tre strukturelle ulemper introduseres:
- Kobling mellom verktøykjede og krysskompilering:
CGO_ENABLED=1krever en C-kompilator, headere for målets libc og en plattformlinker for hvert mål-OS og hver arkitektur. Bygging avlinux/arm64- ellerwindows/arm64-artefakter fra endarwin/arm64-arbeidsstasjon gjør en enkeltgo build-kommando om til en matrise av krysskompilatorer og sysroots. - Overhead ved kjøretidsskedulering: Hvert
cgo-kall krysser grensen til Go-kjøretiden, bytter fra en voksende goroutine-stakk til en systemstakk og låser en operativsystemtråd (M). For finkornede kall akkumuleres overgangskostnaden (~50 til 150 ns per kall) og konkurranse om skedulereren raskt. - Kollaps av feildomene: C-kode kjører i samme virtuelle adresserom som Go-kjøretiden. Én enkelt skriving utenfor grensene, bruk etter frigjøring, stakkoverflyt eller
abort()inne i en C-funksjon korrumperer metadata i Go-heapen eller avslutter hele tjenerprosessen umiddelbart, forbikoblerrecover()og tar med seg alle pågående HTTP- eller RPC-forespørsler.
Når team forsøker å unngå cgo, tvinger de vanlige alternativene frem et annet kompromiss. Rene Go-baserte mekaniske oversettelser av C-kodebaser bevarer CGO_ENABLED=0, men henger ofte etter oppstrøms C-utgivelser, har et emulert virtuelt minnelag og allokerer mye under last. Å flytte C-biblioteket til en hjelpeprosess over loopback-TCP eller Unix-domenesockets gjenoppretter feilisolasjon, men hvert kall betaler for to kontekstbytter i kjernen, kopiering av socketbuffere og wire-serialisering.
Vi bygde HQQ etter å ha stilt et direkte spørsmål: Hva om C-biblioteket lå i en separat operativsystemprosess med sitt eget virtuelle adresserom, mens Go og C utvekslet rammer direkte gjennom delte CPU-cache-linjer uten å gå inn i kjernen på den raske banen?
| Integrasjonsmodell | CGO_ENABLED=0 | Feilisolasjon | Systemkall på raskt spor | Heap-allokeringer per kall | Trofasthet mot oppstrøms C11 |
|---|---|---|---|---|---|
cgo med lenking i prosessen | Nei | Ingen (delt heap) | 0 | 0 til 1+ | Eksakt oppstrøms C |
| Socket / Pipe-underprosess-RPC | Ja | Prosessgrense | 2 til 4 | Flere (rammer + I/O) | Eksakt oppstrøms C |
| Mekanisk C-til-Go-oversettelse | Ja | Go i prosessen | 0 | Høy ved komplekse kall | Emulert C-kjøretid |
| HQQ delt-minne-IPC | Ja | Prosessgrense | 0 (adaptiv spinn) | 0 (StandardLink) | Eksakt oppstrøms C |
Arkitekturoversikt: Fire lag på tvers av prosessgrenser
HQQ deler prosessisolasjon inn i fire komponerbare lag, slik at mekanikken for delte minneringer, typet RPC-kodegenerering, verifisering av binærintegritet og overvåking av barneprosesser forblir uavhengige.
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 over delt minne (gosuda.org/hqq og c/)
Basispakken gosuda.org/hqq og dens C11-motstykke i c/ implementerer to transportmoduser for delt minne over POSIX mmap (Linux og macOS) og Win32 CreateFileMappingW (Windows). Hvert delte segment begynner med en verifisert HQQ_STL3-superblokk som låser protokollversjon, bufferGeometri og endepunkt-PIDer før noen ringindeks leses.
Alle varme kontrollord og slot-headere er justert til 64-byte cache-linjegrenser, slik at produsent- og konsumentkjerner aldri konkurrerer om samme cache-linje gjennom 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 | Semantikk |
|---|---|---|---|
seq | 0x00 | 8 B | Monoton sekvensteller. Oddetallsverdier markerer en pågående skriving; partallsverdier publiserer en forpliktet ramme via store-release. |
len | 0x08 | 4 B | Lengde i byte på nyttelasten skrevet inn i slotten (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Kontrollbiter for rammen (fortsettelse, feilstatus eller avslutningsvarsel). |
payload | 0x40 | BufferSize | Sammenhengende nyttelastområde som leses og skrives på stedet uten mellomliggende kopier. |
Kallere velger mellom to transportnivåer avhengig av samtidighetsstrukturen:
StandardLink(1:1 SPSC-sesjon): Parer en klient- og en tjenersesjon over dedikerte forespørsels- og responsslots beskyttet av et sekvenslås (SeqLock). På den raske banen serialiserer skriveren direkte inn i den kartlagte slotten og forpliktes med en atomisk store-release; leseren spinner kort i brukerrom før den faller tilbake til OS-vekkingsprimitiver (futexpå Linux,__ulock_waitpå macOS,WaitOnAddresspå Windows). En enkelt rammes rundtur fullføres på ~200 ns med0 allocs/op.Advanced(MPMC-dispatch-ring): MultiplekserMsamtidige Go-kallere overNC-arbeidertråder ved hjelp av en låsefri multi-producer multi-consumer-innsendingsring, en delt slotpool og en indeksert fullføringstabell. Kallere reserverer en slot, publiserer slotindeksen til ringen og venter på sin fullføringsoppføring, mens arbeidertrådene tar ut og fullfører forespørsler i uordnet rekkefølge.
Begge nivåene sporer kontinuerlig om peer er i live. Hvert endepunkt oppdaterer et atomisk monotont tidsstempel via Heartbeat() og inspiserer PeerAlive(timeout) mens det venter. Hvis en arbeiderprosess krasjer midt i et kall, oppdager peeren den utdaterte heartbeaten eller den døde PID-en umiddelbart, avbryter blokkerte ventinger med ErrPeerGone, og støtter atomisk sesjonsovertakelse (Takeover) uten å etterlate låste delte minnefiler.
2. Nullallokerings-RPC og generasjonssjekkede handles (gosuda.org/hqq/bindgen)
Å skrive rammeverk for delt minne for hånd til et C-bibliotek med dusinvis av funksjoner er kjedelig og feilutsatt. hqq-bindgen er en skjemakompilator som leser en Interface Definition Language-fil (.idl) (eller oppretter en direkte fra en C-header .h via hqq-bindgen init) og produserer en zero-cgo Go-klientpakke sammen med et C11-tjenerskjelett.
Den farligste feilen i enhver C-binding er å eksponere rå C-pekere (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) over språkgrensen. Hvis en kaller sender en peker etter at den er frigjort, sender en peker som tilhører en annen sesjon, eller korrumperer en heltallsadresse, derefererer C-biblioteket ugyldig minne.
hqq-bindgen forbyr rå pekere på ledningen. I stedet modellerer IDL-en tilstandsfulle C-ressurser som typede handle-deklarasjoner:
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å ledningen er hvert handle et 64-bits heltall som pakker en 32-bits tabellindeks og en 32-bits generasjonsteller som ikke er null: (uint64_t)generation << 32 | index. C-kjøretiden (c/src/hqq_rpc.c) vedlikeholder en handle-tabell per sesjon med tre invarianter:
- Generasjonsvalidering: Hvert oppslag verifiserer at slotten er opptatt, at dens lagrede generasjon samsvarer med de øvre 32 bitene av handle-en, og at dens handle-typetag samsvarer med parametertypen (
DbmotStmt). Gjenbruk av et lukket handle feiler umiddelbart medHQQRPC_E_BAD_HANDLEfør kall inn i C-biblioteket. - Hierarkisk eierskap (
@owned_by): FordiStmter annotert med@owned_by(Db), går lukking av etDb-handle automatisk gjennom alle levende barn-Stmt-handles eid av detDbog kjører deres destruktorer i omvendt allokeringsrekkefølge (sqlite3_finalizeførsqlite3_close_v2). - Feiing ved sesjonsavslutning: Hvis en Go-klient kobler fra eller krasjer uten å lukke sine handles, feier sesjonsdestruktoren på C-tjeneren hele sesjonens handle-tabell i omvendt rekkefølge, og garanterer null lekkede C-allokeringer på tvers av klientomstarter.
Før en sesjon behandler sitt første RPC-kall, utveksler klienten og tjeneren et 32-byte SHA-256-grensesnittsammendrag beregnet over den kanoniserte IDL-en (prefikset HQQ-BINDGEN-IDL-V1\n). Hvis Go-klienten og C-arbeideren ble kompilert fra inkompatible IDL-skjemaer, avviser håndtrykket tilkoblingen ved oppstart.
3. Manipulasjonssikker binærkjøring (gosuda.org/hqq/secureexec)
Å flytte C-kode inn i en arbeiderbinærfil reiser et spørsmål om forsyningskjeden: Hvordan vet Go-prosessen at arbeiderbinærfilen på disken ikke ble endret eller erstattet mellom verifisering og kjøring?
Å kalle crypto/sha256 på en filsti etterfulgt av os/exec.Command(path).Start() etterlater et vindu for tidssjekk til tidsbruk (TOCTOU), der en annen prosess kan overskrive eller bytte ut filen før kjernen kjører den. gosuda.org/hqq/secureexec lukker dette vinduet:
- På Linux åpner
secureexecbinærfilen og strømmer innholdet inn i en anonym minnefildeskriptor opprettet viamemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)mens den beregner SHA-256-hashen, verifiserer hashen mot et festet 32-byte sammendrag eller et Ed25519-signert utgivelsesmanifest (cmd/hqq-manifest), anvenderfcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)slik at ingen prosess på systemet kan endre sidene, og starter barneprosessen fra den forseglede deskriptoren. - På macOS og Windows materialiserer
secureexecverifiserte binærfiler i tillatelseslåste cachekataloger nøklet etter SHA-256-sammendrag, og verifiserer filstørrelse og integritet ved hjelp av sammendrag før oppstart.
4. Overvåking av prosesser med forelderlivline (gosuda.org/hqq/launch)
Når en Go-applikasjon starter en bakgrunnsprosess for en C-arbeider, må arbeiderprosessen aldri overleve sin forelder som en foreldreløs prosess, selv om Go-forelderen avsluttes med SIGKILL (kill -9) og ikke kan kjøre defer-opprydningsbehandlere.
gosuda.org/hqq/launch håndhever livssyklusbinding gjennom en anonym OS-pipe som sendes til arbeiderens standard inngang med flagget --parent-stdin. Go-forelderen holder skrive-enden av pipen og skriver aldri til den; C-arbeideren overvåker lese-enden på en dedikert livline-tråd. Hvis Go-prosessen avslutter normalt, får panikk, eller blir drept av operativsystemkjernen (SIGKILL eller OOM-drepen), lukker kjernen umiddelbart skrivedeskriptoren. C-arbeideren mottar EOF på pipen innen mikrosekunder, avbryter eventuelle aktive kall, kjører sine sesjonshandle-destruktorer, fjerner koblingen til filene for delt minne og avslutter rent.
Hurtigstart: Kalle en generert tjeneste
Med hqq-bindgen og launch ser kall til en isolert C-arbeider fra Go ut som en vanlig typet Go-klient, samtidig som CGO_ENABLED=0 opprettholdes:
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 organisert i fokuserte pakker med eksplisitte grenser:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) og lavnivå-primitiver for delt minnering
3├── advanced.go / slotpool.go # Advanced (MPMC)-dispatcher, slotpool og fullføringstabell
4├── shm_*.go / wait_*.go # OS-spesifikk mmap/CreateFileMapping og futex/ulock/WaitOnAddress
5├── c/ # C11-transportbibliotek (hqq.c) og RPC-tjenerkjøretid (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL-parser, verifiserer (V1-V41) og Go/C11-kodegenerator
7│ └── rt/ # Nullallokerings Go-klientkjøretid for genererte tjenester
8├── secureexec/ # SHA-256/Ed25519-manifestverifisering og memfd-forseglet kjøring
9├── launch/ # Arbeiderprosessovervåker med --parent-stdin-livline-håndtering
10├── cmd/hqq-manifest/ # CLI-verktøy for generering og signering av binære utgivelsesmanifester
11└── spec/ # Kjørbare wire-spesifikasjoner og gullstandard-testvektorer (BINDGEN.md)
For å se hvordan HQQ driver direkte erstatninger i produksjon for to av de mest brukte cgo-bibliotekene i Go-økosystemet, se følgende søsterpakker bygget på denne stakken:
gosuda.org/sqlite3: Direkte erstatning fordatabase/sql-driver kompatibel medgithub.com/mattn/go-sqlite3, som kjører native C SQLite3.54.1over delt minne med pipelinet batch-prefetching.gosuda.org/zstd: Direkte erstatning forgithub.com/DataDog/zstd, som kjører native Clibzstd 1.5.7i en isolert arbeider med delt minne, med nullallokeringskontekster og bulk-batching.- GitHub-repositorium: https://github.com/gosuda/HQQ