GoSuda

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

By Lemon Mint
views ...

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:

  1. Verktøykjede- og krysskompileringskobling: CGO_ENABLED=1 krever en C-kompilator, målets libc-hoder og en plattformlinker for hvert målos og arkitektur. Å bygge linux/arm64- eller windows/arm64-artefakter fra en darwin/arm64-arbeidsstasjon gjør en enkel go build-kommando om til en matrise av krysskompilatorer og sysroot.
  2. 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.
  3. 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år recover() 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?

IntegrasjonsmodellCGO_ENABLED=0FeilisolusjonHurtigbane-systemkallPer-anrop Heap-allokeringerOppstrøms C11-trofasthet
cgo In-Process-lenkingNeiIngen (delt heap)00 til 1+Eksakt oppstrøms C
Sokkel- / Rør-underprosess-RPCJaProsessgrense2 til 4Flere (ramming + I/O)Eksakt oppstrøms C
Mekanisk C-til-Go-oversettelseJaIn-process Go0Høy ved komplekse anropEmulert C-kjøretid
HQQ Delt-minne-IPCJaProsessgrense0 (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              +---------------------------------------------------------------------+
FeltOffsetStørrelseSemantikk
seq0x008 BMonoton sekvensteller. Odde verdier markerer en pågående skriving; like verdier publiserer en forpliktet ramme via store-release.
len0x084 BNyttelastens bytelengde skrevet inn i sporet (0 <= len <= BufferSize).
flags0x0C4 BRamme kontrollbit (kontinuasjon, feilstatus eller avslutningsvarsel).
payload0x40BufferSizeSammenhengende 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 (futex på Linux, __ulock_wait på macOS, WaitOnAddress på Windows). En enkeltrammes tur-retur fullføres på ~200 ns med 0 allocs/op.
  • Advanced (MPMC-utsendelsesring): Multiplipserer M samtidige Go-anropere på tvers av N C-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:

  1. 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 (Db vs. Stmt). Gjenbruk av et lukket håndtak feiler umiddelbart med HQQRPC_E_BAD_HANDLE før det anropes inn i C-biblioteket.
  2. Hierarkisk eierskap (@owned_by): Fordi Stmt er annotert med @owned_by(Db), vil det å lukke et Db-håndtak automatisk gå gjennom alle levende underordnede Stmt-håndtak som eies av den Db-en og kjøre deres destruktører i omvendt allokeringsrekkefølge (sqlite3_finalize før sqlite3_close_v2).
  3. Ø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 secureexec binærfilen, strømmer innholdet inn i en anonym minnefildeskriptor opprettet via memfd_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), bruker fcntl(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 secureexec verifiserte 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-in database/sql-driver kompatibel med github.com/mattn/go-sqlite3, som kjører native C SQLite 3.54.1 over delt minne med rørledningsbasert satsvis forhåndshenting.
  • gosuda.org/zstd: Drop-in-erstatning for github.com/DataDog/zstd, som kjører native C libzstd 1.5.7 i en isolert delt-minne-arbeider med null-allokeringskontekster og massekjøring.
  • GitHub-repositorie: https://github.com/gosuda/HQQ