GoSuda

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

By Lemon Mint views ...

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:

  1. Kobling mellom verktøykjede og krysskompilering: CGO_ENABLED=1 krever en C-kompilator, headere for målets libc og en plattformlinker for hvert mål-OS og hver arkitektur. Bygging av linux/arm64- eller windows/arm64-artefakter fra en darwin/arm64-arbeidsstasjon gjør en enkelt go build-kommando om til en matrise av krysskompilatorer og sysroots.
  2. 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.
  3. 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, forbikobler recover() 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?

IntegrasjonsmodellCGO_ENABLED=0FeilisolasjonSystemkall på raskt sporHeap-allokeringer per kallTrofasthet mot oppstrøms C11
cgo med lenking i prosessenNeiIngen (delt heap)00 til 1+Eksakt oppstrøms C
Socket / Pipe-underprosess-RPCJaProsessgrense2 til 4Flere (rammer + I/O)Eksakt oppstrøms C
Mekanisk C-til-Go-oversettelseJaGo i prosessen0Høy ved komplekse kallEmulert C-kjøretid
HQQ delt-minne-IPCJaProsessgrense0 (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              +---------------------------------------------------------------------+
FeltOffsetStørrelseSemantikk
seq0x008 BMonoton sekvensteller. Oddetallsverdier markerer en pågående skriving; partallsverdier publiserer en forpliktet ramme via store-release.
len0x084 BLengde i byte på nyttelasten skrevet inn i slotten (0 <= len <= BufferSize).
flags0x0C4 BKontrollbiter for rammen (fortsettelse, feilstatus eller avslutningsvarsel).
payload0x40BufferSizeSammenhengende 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 (futex på Linux, __ulock_wait på macOS, WaitOnAddress på Windows). En enkelt rammes rundtur fullføres på ~200 ns med 0 allocs/op.
  • Advanced (MPMC-dispatch-ring): Multiplekser M samtidige Go-kallere over N C-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:

  1. 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 (Db mot Stmt). Gjenbruk av et lukket handle feiler umiddelbart med HQQRPC_E_BAD_HANDLE før kall inn i C-biblioteket.
  2. Hierarkisk eierskap (@owned_by): Fordi Stmt er annotert med @owned_by(Db), går lukking av et Db-handle automatisk gjennom alle levende barn-Stmt-handles eid av det Db og kjører deres destruktorer i omvendt allokeringsrekkefølge (sqlite3_finalize før sqlite3_close_v2).
  3. 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 secureexec binærfilen og strømmer innholdet inn i en anonym minnefildeskriptor opprettet via memfd_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), anvender fcntl(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 secureexec verifiserte 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 for database/sql-driver kompatibel med github.com/mattn/go-sqlite3, som kjører native C SQLite 3.54.1 over delt minne med pipelinet batch-prefetching.
  • gosuda.org/zstd: Direkte erstatning for github.com/DataDog/zstd, som kjører native C libzstd 1.5.7 i en isolert arbeider med delt minne, med nullallokeringskontekster og bulk-batching.
  • GitHub-repositorium: https://github.com/gosuda/HQQ