HQQ: C könyvtárak izolálása a Go nyelvtől cache-vonal szintű megosztott memória révén
A gosuda.org/hqq egy zero-cgo inter-process communication futtatókörnyezet és kódegenerációs eszközlánc Go és C11 nyelvekhez.
Elszigeteli a natív C könyvtárakat különálló OS worker folyamatokban, és a Go hívókhoz memóriára leképezett, cache-line-ig igazított shared-memory gyűrűken keresztül kapcsolja őket.
A projekt két transzport réteget (dedikált single-producer single-consumer munkamenetekhez a StandardLink-et és multi-worker MPMC diszpécserhez az Advanced-et), a hqq-bindgen IDL fordítót, valamint egy hamisítás ellen védett folyamatfelügyelőt (secureexec és launch) ötvöz.
1go get gosuda.org/hqq
The Cost of Sharing an Address Space with C
Minden Go rendszermérnök elkerülhetetlenül eléri ugyanazt a válaszúthelyzetet.
Egy olyan Go szolgáltatást épít, amely másodpercek alatt lefordul CGO_ENABLED=0 beállítással, egyetlen statikus binárisként szállítható, laptopról Linux vagy Windows célpontokra cross-compileolható sysroot nélkül, valamint egy memóriabiztonságos, szemétgyűjtött (garbage-collected) heapen fut.
Ekkor azonban szükségessé válik egy érett C könyvtár: a SQLite beágyazott relációs tároláshoz, a Zstandard (libzstd) szótár-tömörítéshez, vagy egy hardverrel gyorsított média- illetve kriptográfiai könyvtár.
A cgo engedélyezése az említett C könyvtár Go folyamatba történő linkeléséhez megoldja az azonnali kötési problémát, miközben három strukturális hátrányt vezet be:
- Toolchain and Cross-Compilation Coupling: A
CGO_ENABLED=1beállításhoz C fordítóra, cél libc fejlécekre és platform linkerre van szükség minden egyes cél OS és architektúra esetén. Alinux/arm64vagywindows/arm64artefaktumok építése egydarwin/arm64munkaállomásról a keresztfordítók és sysrootok mátrixává alakít egyetlengo buildparancsot. - Runtime Scheduling Overhead: Minden egyes
cgohívás átlépi a Go futtatókörnyezet határát, növelhető goroutine vermről (stack) rendszer vermre vált, és leköt egy OS szálat (M). Finomszemcsés hívások esetén az átmeneti adó (~hívásonként 50 és 150 ns között) és a scheduler versengés gyorsan felhalmozódik. - Fault Domain Collapse: A C kód ugyanabban a virtuális címtérben fut, mint a Go futtatókörnyezet. Egyetlen határokon túli írás (out-of-bounds write), use-after-free, verm túlcsordulás vagy
abort()egy C függvényen belül azonnal károsítja a Go heap metaadatait, vagy leállítja a teljes szerverfolyamatot, megkerülve arecover()hívást, és magával rántva minden folyamatban lévő HTTP vagy RPC kérést.
Amikor a csapatok megpróbálják elkerülni a cgo-t, a gyakori alternatívák másfajta kompromisszumra kényszerítenek.
A C kódbázisok tiszta Go-beli mechanikus fordításai megőrzik a CGO_ENABLED=0 beállítást, de gyakran lemaradnak az upstream C kiadások mögött, emulált virtuális memória réteget hordoznak, és terhelés alatt jelentős memóriafoglalást végeznek.
A C könyvtár áthelyezése egy segédfolyamatba (helper process) loopback TCP-n vagy Unix domain socketeken keresztül visszaállítja a hibaelosztást (fault isolation), de minden hívás megfizeti két kernel kontextusváltás, a socket puffer másolatok és a vezetékes szerializáció árát.
Az HQQ-t egy direkt kérdés feltetése után építettük fel: mi lenne, ha a C könyvtár egy különálló OS folyamatban élne a saját virtuális címtérével, miközben a Go és a C közvetlenül, közös CPU cache line-okon keresztül cserélne kereteket anélkül, hogy belépne a kernelbe a gyors utakon (fast path)?
| Integration Model | CGO_ENABLED=0 | Fault Isolation | Fast-Path Syscalls | Per-Call Heap Allocations | Upstream C11 Fidelity |
|---|---|---|---|---|---|
cgo In-Process Linking | Nem | Nincs (osztott heap) | 0 | 0 és 1+ között | Pontos upstream C |
| Socket / Pipe Subprocess RPC | Igen | Folyamathatár | 2 és 4 között | Többszörös (keretezés + I/O) | Pontos upstream C |
| Mechanical C-to-Go Translation | Igen | Folyamaton belüli Go | 0 | Magas komplex hívásoknál | Emulált C futtatókörnyezet |
| HQQ Shared-Memory IPC | Igen | Folyamathatár | 0 (adaptív spin) | 0 (StandardLink) | Pontos upstream C |
Architectural Overview: Four Layers Across Process Boundaries
Az HQQ négy komponálható rétegre választja szét a folyamatelszigetelést, így a shared-memory gyűrűmechanika, a típusos RPC kódegeneráció, a bináris integritás-ellenőrzés és a gyermekfolyamat-felügyelet függetlenek maradnak.
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. Shared-Memory Transport (gosuda.org/hqq and c/)
A gosuda.org/hqq alapcsomag és a c/ könyvtárban található C11 párja két shared-memory transzport módot implementál a POSIX mmap (Linux és macOS) és a Win32 CreateFileMappingW (Windows) rendszereken keresztül.
Minden megosztott szegmens egy ellenőrzött HQQ_STL3 szuperblokkal kezdődik, amely rögzíti a protokollverziót, a puffer geometriáját és a végpontok PID-jeit, mielőtt bármilyen gyűrű indexet olvasnának.
Minden forró vezérlőszó és slot fejléc 64 bájtos cache-line határokhoz van igazítva, így a producer és consumer magok soha nem versengenek ugyanazon a cache line-on hamis megosztás (false sharing) miatt.
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 +---------------------------------------------------------------------+
| Field | Offset | Size | Semantics |
|---|---|---|---|
seq | 0x00 | 8 B | Monoton szekvenciaszámláló. A páratlan értékek egy folyamatban lévő írást jeleznek; a páros értékek egy kötelezett keretet (committed frame) publikálnak store-release műveleten keresztül. |
len | 0x08 | 4 B | A slotba írt adattartalom bájthossza (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Keretvezérlő bitek (folytatás, hibaállapot vagy leállítási értesítés). |
payload | 0x40 | BufferSize | Folytonos adattartalom régió, amely helyben (in place) olvasható és írható köztes másolatok nélkül. |
A hívók a koncurrency struktúrától függően két transzport réteg közül választanak:
StandardLink(1:1 SPSC Session): Egy klienst és egy szerver munkamenetet párosít dedikált kérés és válasz slotokon keresztül, amelyeket egy szekvenciazár (SeqLock) véd. A gyors útvonalon a író közvetlenül a leképezett slotba szerializál, és egy atomi store-release művelettel commitol; az olvasó röviden pörög a user space-ben, mielőtt visszatérne az OS ébresztési primitívekhez (futexLinuxon,__ulock_waitmacOS-en,WaitOnAddressWindows-on). Egyetlen keretes oda-vissza út ~200 ns alatt fejeződik be0 allocs/opértékkel.Advanced(MPMC Dispatch Ring):Mpárhuzamos Go hívót multiplexelNC worker szálon keresztül egy lock-mentes multi-producer multi-consumer beküldési gyűrű, egy megosztott slot pool és egy indexelt befejezési tábla segítségével. A hívók lefoglalnak egy slotot, közzéteszik a slot indexét a gyűrűn, és a saját befejezési bejegyzésükre várakoznak, miközben a worker szálak kiveszik a sorból és teljesítik a kéréseket sorrenden kívül.
Mindkét réteg folyamatosan követi a partner élhetőségét (liveness).
Minden végpont frissít egy atomi monoton időbélyeget a Heartbeat() függvényen keresztül, és vizsgálja a PeerAlive(timeout) állapotot várakozás közben.
Ha a worker folyamat összeomlik a hívás közben, a partner azonnal érzékeli az elavult szívverést vagy a halott PID-t, megszakítja a blokkolt várakozásokat az ErrPeerGone hibával, és támogatja az atomi munkamenet-átvételt (Takeover) anélkül, hogy beragadt shared-memory fájlokat hagyna hátra.
2. Zero-Allocation RPC and Generation-Checked Handles (gosuda.org/hqq/bindgen)
A shared-memory keretezés kézi megírása egy tucatnyi függvénnyel rendelkező C könyvtárhoz fárasztó és hibalehetőségeket rejt.
A hqq-bindgen egy sémafordító, amely beolvas egy Interfész Definíciós Nyelv (.idl) fájlt (vagy közvetlenül indít egyet egy C .h fejlécből a hqq-bindgen init paranccsal), és egy zero-cgo Go kliens csomagot, valamint egy C11 szervervázat generál.
A legveszélyesebb hiba bármilyen C kötésekben a nyers C mutatók (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) expozíciója a nyelvi határon túlra.
Ha egy hívó átad egy mutatót annak felszabadítása után, átad egy másik munkamenethez tartozó mutatót, vagy meghamisít egy egész címet, a C könyvtár érvénytelen memóriát dereferál.
A hqq-bindgen tiltja a nyers mutatókat a vezetéken (on the wire).
Ehelyett az IDL az állapottal rendelkező C erőforrásokat típusos handle deklarációkként modellezi:
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}
A vezetéken minden handle egy 64 bites egész szám, amely egy 32 bites táblázatindexet és egy 32 bites, nem nulla generációs számlálót csomagol össze: (uint64_t)generation << 32 | index.
A C futtatókörnyezet (c/src/hqq_rpc.c) munkamenetenkénti handle táblázatot tart fenn három invariánssal:
- Generation Validation: Minden keresés ellenőrzi, hogy a slot foglalt-e, a tárolt generáció megegyezik-e a handle felső 32 bitjével, és a handle típuscímkéje megegyezik-e a paraméter típusával (
Dbvs.Stmt). Egy lezárt handle újrafelhasználása azonnal meghiúsul aHQQRPC_E_BAD_HANDLEhibával, mielőtt még meghívná a C könyvtárat. - Hierarchical Ownership (
@owned_by): Mivel aStmtmeg van jelölve a@owned_by(Db)annotációval, egyDbhandle lezárása automatikusan bejárja az összes élő, az adottDbáltal birtokolt gyermekStmthandle-t, és lefutattja azok destruktorait a lefoglalási sorrend fordítottjában (sqlite3_finalizeazsqlite3_close_v2előtt). - Session Teardown Sweep: Ha egy Go kliens lekapcsolódik vagy összeomlik anélkül, hogy lezárná a handle-jeit, a C szerver munkamenet-destruktora a munkamenet teljes handle táblázatát végigsöpri fordított sorrendben, biztosítva a szivárgásmentes C memóriafoglalásokat a kliens újraindulásai során.
Mielőtt egy munkamenet feldolgozná az első RPC hívását, a kliens és a szerver kicserél egy 32 bájtos SHA-256 interfész kivonatot (digest), amelyet a kanonizált IDL alapján számítanak ki (HQQ-BINDGEN-IDL-V1\n előtaggal).
Ha a Go kliens és a C worker nem kompatibilis IDL sémákból lett lefordítva, a kézfogás (handshake) az indításkor elutasítja a kapcsolatot.
3. Tamper-Free Binary Execution (gosuda.org/hqq/secureexec)
A C kód áthelyezése egy worker binárisba felvet egy ellátási lánc (supply-chain) kérdést: honnan tudja a Go folyamat, hogy a lemezen lévő worker bináris nem lett módosítva vagy kicserélve az ellenőrzés és a végrehajtás között?
A crypto/sha256 meghívása egy fájlútvonalon, amit az os/exec.Command(path).Start() követ, létrehoz egy Time-of-Check to Time-of-Use (TOCTOU) ablakot, ahol egy másik folyamat felülírhatja vagy kicserélheti a fájlt, mielőtt a kernel végrehajtaná azt.
A gosuda.org/hqq/secureexec bezárja ezt az ablakot:
- Linuxon a
secureexecmegnyitja a bináris fájlt, streameli annak tartalmát egy névtelen memória fájlleíróba, amelyet amemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)hívás hoz létre, miközben kiszámítja annak SHA-256 hash értékét, ellenőrzi a hash-t egy rögzített 32 bájtos kivonathoz vagy egy Ed25519-aláírt kiadási manifesthez (cmd/hqq-manifest), alkalmazza afcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)hívást, így a rendszeren semmilyen folyamat nem tudja módosítani az oldalakat, majd a lepecsételt leíróból indítja el a gyermeket. - macOS-en és Windows-on a
secureexecaz ellenőrzött binárisokat engedéllyel zárolt cache könyvtárakba materializálja, amelyeket az SHA-256 kivonat kulcsol, és az indítás előtt ellenőrzi a fájlméretet és a kivonat integritását.
4. Parent-Lifeline Process Supervision (gosuda.org/hqq/launch)
Amikor egy Go alkalmazás egy háttérbeli C worker folyamatot indít, a worker soha nem élheti túl a szülőjét árva folyamatként, még akkor sem, ha a Go szülőt SIGKILL (kill -9) paranccsal szüntetik meg, és az nem tudja futtatni a defer takarítási kezelőket.
A gosuda.org/hqq/launch élettartam-kapcsolást kényszerít ki egy névtelen OS csövön (pipe) keresztül, amelyet a worker szabványos bemenetére adnak át a --parent-stdin jelzővel.
A Go szülő tartja a cső írási végét, és soha nem ír bele; a C worker egy dedikált mentőöv (lifeline) szállal figyeli az olvasási véget.
Ha a Go folyamat normál módon kilép, pánikba esik, vagy az OS kernel öli meg (SIGKILL vagy OOM killer), a kernel azonnal lezárja az írási leírót.
A C worker mikromásodperceken belül EOF jelzést kap a csövön, megszakítja az aktív hívásokat, lefuttatja a munkamenet handle destruktorait, megszünteti a shared-memory fájlok hivatkozásait, és tisztán kilép.
Quickstart: Calling a Generated Service
A hqq-bindgen és a launch használatával egy elszigetelt C worker hívása Go-ból úgy néz ki, mint egy hagyományos típusos Go kliens, miközben fenntartja a CGO_ENABLED=0 beállítást:
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 Structure
A gosuda.org/hqq minden alrendszere fókuszált csomagokba van szervezve explicit határokkal:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) and low-level shared-memory ring primitives
3├── advanced.go / slotpool.go # Advanced (MPMC) dispatcher, slot pool, and completion table
4├── shm_*.go / wait_*.go # OS-specific mmap/CreateFileMapping and futex/ulock/WaitOnAddress
5├── c/ # C11 transport library (hqq.c) and RPC server runtime (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL parser, verifier (V1-V41), and Go/C11 code generator
7│ └── rt/ # Zero-allocation Go client runtime for generated services
8├── secureexec/ # SHA-256 / Ed25519 manifest verification and memfd-sealed execution
9├── launch/ # Worker process supervisor with --parent-stdin lifeline management
10├── cmd/hqq-manifest/ # CLI tool for generating and signing release binary manifests
11└── spec/ # Executable wire specifications and golden test vectors (BINDGEN.md)
Annak megtekintéséhez, hogy az HQQ hogyan biztosít csereszabatos, éles környezetbe szánt helyettesítőket a Go ökoszisztéma két legszélesebb körben használt cgo könyvtárához, tekintse meg az ezen a stacken épült kiegészítő csomagokat:
gosuda.org/sqlite3: Csereszabatosdatabase/sqlillesztőprogram (driver), amely kompatibilis agithub.com/mattn/go-sqlite3csomaggal, és a natív C SQLite3.54.1-et futtatja shared memory-n keresztül pipeline-olt batch előzetes lekéréssel (prefetching).gosuda.org/zstd: Csereszabatos helyettesítő agithub.com/DataDog/zstdcsomaghoz, amely a natív Clibzstd 1.5.7-et futtatja egy elszigetelt shared-memory workerben zero-allocation kontextusokkal és tömeges kötegeléssel.- GitHub Repository: https://github.com/gosuda/HQQ