HQQ: Isolering av C-biblioteker fra Go over Cache-Line Shared Memory
gosuda.org/hqq er en null-cgo prosessinterkommunikasjonskjøretid og verktøykjede for kode generering for Go og C11. Den isolerer native C-biblioteker inne i separate OS-arbeiderprosesser og kobler dem til Go-anropere over minnekartlagde, cache-linjejusterte delt-minne-ringer. Prosjektet kombinerer to transportlag (StandardLink for dedikerte økter med en enkelt produsent og en enkelt forbruker, og Advanced for MPMC-utsendelse med flere arbeidere), hqq-bindgen IDL-kompilatoren og en manipulasjonsverifisert prosessovervåker (secureexec og launch).
1go get gosuda.org/hqq
Kostnaden ved å dele et adresserom med C
Hver eneste Go-systemingeniør treffer før eller siden det samme veiskillet. Du bygger en Go-tjeneste som kompilerer på sekunder med CGO_ENABLED=0, distribueres som en enkelt statisk binærfil, krysskompilerer fra en bærbar datamaskin til Linux- eller Windows-mål uten et sysroot, og kjører inne i en minnesikker, søppelsamlet heap. Deretter trenger du et modent C-bibliotek: SQLite for innebygd relasjonell lagring, Zstandard (libzstd) for ordbokkomprimering, eller et maskinvareakselerert medie- eller kryptografibibliotek.
Å aktivere cgo for å lenke det C-biblioteket inn i Go-prosessen løser det umiddelbare bindingsproblemet samtidig som det introduserer tre strukturelle forpliktelser:
- Verktøykjede- og krysskompileringskobling:
CGO_ENABLED=1krever en C-kompilator, målets libc-hoder og en plattformlinker for hvert målos og arkitektur. Å byggelinux/arm64- ellerwindows/arm64-artefakter fra endarwin/arm64-arbeidsstasjon gjør en enkelgo build-kommando om til en matrise av krysskompilatorer og sysroot. - Kjøretidsplanleggingsoverhode: Hvert
cgo-anrop krysser Go-kjøretidsgrensen, bytter fra en voksbar gorutinenstabel til en systemstabel, og fester en OS-tråd (M). For fingranerte anrop akkumuleres overgangsskatten (~50 til 150 ns per anrop) og planleggerkonkurransen raskt. - Feildomenekollaps: C-kode kjører i det samme virtuelle adresserommet som Go-kjøretiden. En enkelt minneoverskridelse, bruk-etter-frigjøring, stabeloverflyt eller
abort()inne i en C-funksjon korrumperer Go-heap-metadata eller terminerer hele serverprosessen umiddelbart, og omgårrecover()samt drar med seg hver pågående HTTP- eller RPC-forespørsel i fallet.
Når team forsøker å unngå cgo, tvinger de vanlige alternativene frem et annet kompromiss. Ren-Go mekaniske oversettelser av C-kodebaser bevarer CGO_ENABLED=0, men henger ofte etter oppstrøms C-utgivelser, bærer et emulert virtuelt minnelag og allokerer tungt under last. Å flytte C-biblioteket til en hjelperprosess over loopback-TCP eller Unix-domenesokler gjenoppretter feilisolusjon, men hvert anrop betaler for to kjernekontekstskifter, sokkelbufferkopier og trådserialisering.
Vi bygget HQQ etter å ha stilt et direkte spørsmål: hva om C-biblioteket levde i en separat OS-prosess med sitt eget virtuelle adresserom, mens Go og C utvekslet rammer direkte gjennom delte CPU-cache-linjer uten å gå inn i kjernen på hurtigbanen?
| Integrasjonsmodell | CGO_ENABLED=0 | Feilisolusjon | Hurtigbane-systemkall | Per-anrop Heap-allokeringer | Oppstrøms C11-trofasthet |
|---|---|---|---|---|---|
cgo In-Process-lenking | Nei | Ingen (delt heap) | 0 | 0 til 1+ | Eksakt oppstrøms C |
| Sokkel- / Rør-underprosess-RPC | Ja | Prosessgrense | 2 til 4 | Flere (ramming + I/O) | Eksakt oppstrøms C |
| Mekanisk C-til-Go-oversettelse | Ja | In-process Go | 0 | Høy ved komplekse anrop | Emulert C-kjøretid |
| HQQ Delt-minne-IPC | Ja | Prosessgrense | 0 (adaptiv spinn) | 0 (StandardLink) | Eksakt oppstrøms C |
Arkitektonisk oversikt: Fire lag på tvers av prosessgrenser
HQQ skiljer prosessisolusjon i fire komponerbare lag slik at mekanikk for delt-minne-ringer, typet RPC-kode generering, binær integritetsverifisering og underordnet prosessovervåking 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. Delt-minne-transport (gosuda.org/hqq og c/)
Grunnpakken gosuda.org/hqq og dens C11-motpart i c/ implementerer to delt-minne-transportmoduser over POSIX mmap (Linux og macOS) og Win32 CreateFileMappingW (Windows). Hvert delte segment begynner med en verifisert HQQ_STL3-superblokk som fester protokollversjon, buffergeometri og endepunkt-PID-er før noen ringindeks leses.
Alle varme kontrollord og sporetider er justert til 64-bytes cache-linjegrenser slik at produsent- og forbrukerkjerner aldri konkurrerer om den samme cache-linjen 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. Odde verdier markerer en pågående skriving; like verdier publiserer en forpliktet ramme via store-release. |
len | 0x08 | 4 B | Nyttelastens bytelengde skrevet inn i sporet (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Ramme kontrollbit (kontinuasjon, feilstatus eller avslutningsvarsel). |
payload | 0x40 | BufferSize | Sammenhengende nyttelastområde lest og skrevet på plass uten mellomalgerte kopier. |
Anropere velger mellom to transportlag avhengig av samtidighetsstruktur:
StandardLink(1:1 SPSC-økt): Parer en klient- og en serverøkt over dedikerte forespørsels- og responsspor beskyttet av en sekvenslås (SeqLock). På hurtigbanen serieliserer skribenten direkte inn i det kartlagte sporet og forplikter med en atomisk store-release; leseren spinner kort i brukerrom før den faller tilbake til OS-vekkelsesprimitiver (futexpå Linux,__ulock_waitpå macOS,WaitOnAddresspå Windows). En enkeltrammes tur-retur fullføres på ~200 ns med0 allocs/op.Advanced(MPMC-utsendelsesring): MultiplipsererMsamtidige Go-anropere på tvers avNC-arbeidertråder ved hjelp av en låsfri multi-produsent multi-forbruker innsendingsring, en delt sporpool og en indeksert fullføringstabell. Anropere reserverer et spor, publiserer sporindeksen til ringen og venter på sin fullføringsoppføring mens arbeidertråder tar ut av kø og fullfører forespørsler ute av rekkefølge.
Begge lag sporer parteners livaktighet kontinuerlig. Hvert endepunkt oppdaterer et atomisk monotont tidsstempel via Heartbeat() og inspiserer PeerAlive(timeout) under venting. Hvis en arbeiderprosess krasjer midt i et anrop, oppdager parten det foreldede hjerteslaget eller den døde PID-en umiddelbart, avbryter blokkerte venter med ErrPeerGone, og støtter atomisk øktmektig Takeover uten å etterlate fastklemte delt-minne-filer.
2. Null-allokering RPC og generasjonskontrollerte håndtak (gosuda.org/hqq/bindgen)
Å skrive delt-minne-ramming for hånd for et C-bibliotek med dusinvis av funksjoner er kjedelig og feilutsatt. hqq-bindgen er en skjemakompilator som leser en grensesnittdefinisjonsspråkfil (.idl) (eller oppstartsserver en direkte fra et C .h-hode via hqq-bindgen init) og sender ut en null-cgo Go-klientpakke ved siden av et C11-serverskjelett.
Den farligste feilen i enhver C-binding er å eksponere rå C-pekere (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) på tvers av språkegrensen. Hvis en anroper sender en peker etter å ha frigjort den, sender en peker som tilhører en annen økt, eller korrumperer en heltallsadresse, derefererer C-biblioteket ugyldig minne.
hqq-bindgen forbyr rå pekere på tråden. I stedet modellerer IDL-en tilstandsdrevne C-ressurser som typede handle-erklæringer:
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å tråden er hvert håndtak et 64-bits heltall som pakker en 32-bits tabellindeks og en 32-bits ikke-null generasjonsteller: (uint64_t)generation << 32 | index. C-kjøretiden (c/src/hqq_rpc.c) opprettholder en øktspesifikk håndtakstabell med tre invariante egenskaper:
- Generasjonsvalidering: Hvert oppslag verifiserer at sporet er okkupert, at dets lagrede generasjon samsvarer med de øvre 32 bitene av håndtaket, og at dets håndtakstypetag samsvarer med parametertypen (
Dbvs.Stmt). Gjenbruk av et lukket håndtak feiler umiddelbart medHQQRPC_E_BAD_HANDLEfør det anropes inn i C-biblioteket. - Hierarkisk eierskap (
@owned_by): FordiStmter annotert med@owned_by(Db), vil det å lukke etDb-håndtak automatisk gå gjennom alle levende underordnedeStmt-håndtak som eies av denDb-en og kjøre deres destruktører i omvendt allokeringsrekkefølge (sqlite3_finalizeførsqlite3_close_v2). - Øktavslutningssveip: Hvis en Go-klient kobler fra eller krasjer uten å lukke håndtakene sine, sveiper C-serverens øktdestruktør hele øktens håndtakstabell i omvendt rekkefølge, noe som garanterer null lekkede C-allokeringer på tvers av klientomstart.
Før en økt behandler sin første RPC-forespørsel, utveksler klienten og serveren et 32-bytes SHA-256 grensesnittsammendrag beregnet over den kanoniserte IDL-en (HQQ-BINDGEN-IDL-V1\n-prefiks). Hvis Go-klienten og C-arbeideren ble kompilerte fra inkompatible IDL-skjemaer, avviser håndtrykket tilkoblingen ved oppstart.
3. Manipulasjonsfri binær kjøring (gosuda.org/hqq/secureexec)
Å flytte C-kode inn i en arbeiderminærfil reiser et forsyningskjedespørsmål: hvordan vet Go-prosessen at arbeiderminærfilen på disk ikke ble endret eller erstattet mellom verifisering og kjøring?
Å kalle crypto/sha256 på en filbane etterfulgt av os/exec.Command(path).Start() etterlater et tidsvindu mellom sjekk og bruk (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, strømmer innholdet inn i en anonym minnefildeskriptor opprettet viamemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)mens den beregner sin SHA-256-hash, verifiserer hashen mot et festet 32-bytes sammendrag eller et Ed25519-signert utgivelsesmanifest (cmd/hqq-manifest), brukerfcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)slik at ingen prosess på systemet kan mutere sidene, og gyter det underordnede elementet fra den forseglede deskriptoren. - På macOS og Windows materialiserer
secureexecverifiserte binærfiler i tillatelseslåste kadderekataloger nøkkeldet av SHA-256-sammendrag og verifiserer filstørrelse og sammendragsintegritet før oppstart.
4. Overordnet livslinje-prosessovervåking (gosuda.org/hqq/launch)
Når en Go-applikasjon gyter en bakgrunns-C-arbeiderprosess, må arbeideren aldri overleve sin forelder som en foreldreløs prosess, selv om Go-forelderen termineres med SIGKILL (kill -9) og ikke kan kjøre defer-oppryddingsbehandlere.
gosuda.org/hqq/launch håndhever livstidskobling gjennom et anonymt OS-rør som sendes til arbeiderens standardinndata med --parent-stdin-flagget. Go-forelderen holder skriveenden av røret og skriver aldri til det; C-arbeideren overvåker leseenden på en dedikert livlinjetråd. Hvis Go-prosessen avslutter normalt, får panikk eller drepes av OS-kjernen (SIGKILL eller OOM-dreperen), lukker kjernen skrivedeskriptoren umiddelbart. C-arbeideren mottar EOF på røret i løpet av mikrosekunder, avbryter eventuelle aktive anrop, kjører sine øktshåndtak-destruktører, opphever koblingen til delt-minne-filene og avslutter rent.
Hurtigstart: Å kalle en generert tjeneste
Med hqq-bindgen og launch ser det å kalle 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}
Repositoriestruktur
Hvert undersystem i gosuda.org/hqq er organisert i fokuserte pakker med eksplisitte grenser:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) og lavnivå delt-minne-ringprimitiver
3├── advanced.go / slotpool.go # Advanced (MPMC) utsender, sporpool og fullføringstabell
4├── shm_*.go / wait_*.go # OS-spesifikke mmap/CreateFileMapping og futex/ulock/WaitOnAddress
5├── c/ # C11 transportbibliotek (hqq.c) og RPC-serverkjøretid (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL-parser, verifiserer (V1-V41) og Go/C11-kodegenerator
7│ └── rt/ # Null-allokering Go-klientkjøretid for genererte tjenester
8├── secureexec/ # SHA-256 / Ed25519-manifestverifisering og memfd-forseglet kjøring
9├── launch/ # Arbeiderprosessovervåker med --parent-stdin livlinjeadministrasjon
10├── cmd/hqq-manifest/ # CLI-verktøy for generering og signering av utgivelsesbinærmanifester
11└── spec/ # Kjørbare trådspesifikasjoner og gylne testvektorer (BINDGEN.md)
For å se hvordan HQQ driver drop-in produksjonserstatninger for to av de mest utbredte cgo-bibliotekene i Go-økosystemet, sjekk ut tilhørende pakker bygget på denne stacken:
gosuda.org/sqlite3: Drop-indatabase/sql-driver kompatibel medgithub.com/mattn/go-sqlite3, som kjører native C SQLite3.54.1over delt minne med rørledningsbasert satsvis forhåndshenting.gosuda.org/zstd: Drop-in-erstatning forgithub.com/DataDog/zstd, som kjører native Clibzstd 1.5.7i en isolert delt-minne-arbeider med null-allokeringskontekster og massekjøring.- GitHub-repositorie: https://github.com/gosuda/HQQ