HQQ: C-kirjastojen eristäminen Go-kielestä välimuistirivillä jaetun muistin avulla

gosuda.org/hqq on cgo-vapaa prosessien välinen kommunikointiajoaika (inter-process communication runtime) ja koodigenerointityökalu Go:lle ja C11:lle. Se eristää natiivit C-kirjastot erillisiin käyttöjärjestelmän worker-prosesseihin ja yhdistää ne Go-kutsujiin muistikartoitettujen, välimuistirivin mukaan kohdistettujen jaettujen muistirenkaiden (ring) kautta. Projekti yhdistää kaksi siirtotasoa (StandardLink yksittäisille SPSC-istunnoille ja Advanced moniworkerisille MPMC-jakelulle), hqq-bindgen IDL-kääntäjän sekä todennetun prosessivalvojan (secureexec ja launch).
1go get gosuda.org/hqq
Jaetun osoiteavaruuden hinta C-koodin kanssa
Jokainen Go-järjestelmäinsinööri kohtaa lopulta saman haarautumiskohdan. Rakennat Go-palvelua, joka kääntyy sekunneissa komennolla CGO_ENABLED=0, toimitetaan yksittäisenä staattisena binäärinä, ristikääntyy kannettavalta tietokoneelta Linux- tai Windows-kohteisiin ilman sysroot-hakemistoa ja ajetaan muistiturvallisen, roskienkeruun hallitseman keon sisällä. Sitten tarvitset kypsän C-kirjaston: SQLiten upotettuun relaatiotietokantatallennukseen, Zstandardin (libzstd) sanakirjapakkaukseen tai laitekiihdytetyn media- tai salauskirjaston.
C-kirjaston linkittäminen Go-prosessiin cgo:n avulla ratkaisee välittömän sidontaongelman, mutta tuo mukanaan kolme rakenteellista riskiä:
- Työkaluketjun ja ristikääntämisen kytkentä:
CGO_ENABLED=1edellyttää C-kääntäjää, kohde-libc-otsikkotiedostoja ja alustakohtaisen linkittäjän jokaiselle kohde-käyttöjärjestelmälle ja -arkkitehtuurille.linux/arm64- taiwindows/arm64-artefaktien rakentaminendarwin/arm64-työasemalta muuttaa yksittäisengo build-komennon ristikääntäjien ja sysroot-hakemistojen matriisiksi. - Ajonaikaisen ajastuksen ylikuormitus: Jokainen
cgo-kutsu ylittää Go-ajonaikaisen rajan, vaihtaa kasvavasta goroutine-pinosta järjestelmäpinoon ja sitoo käyttöjärjestelmän säikeen (M). Hienojakoisissa kutsuissa siirtymäkustannus (noin 50 ja 150 ns kutsua kohden) sekä ajastimen kilpailu kertyvät nopeasti. - Vikaalueen romahtaminen: C-koodi toimii samassa virtuaalisessa osoiteavaruudessa kuin Go-ajonaikainen. Yksi ulkopuolinen kirjoitus, käyttö vapautetun muistin jälkeen, pinon ylivuoto tai
abort()C-funktion sisällä korruptoi Go-keon metatiedot tai lopettaa koko palvelinprosessin välittömästi ohittaenrecover()-kutsun ja vieden mukanaan jokaisen käsiteltävänä olevan HTTP- tai RPC-pyynnön.
Kun tiimit yrittävät välttää cgo:n, yleiset vaihtoehdot pakottavat toisenlaiseen kompromissiin. Puhtaasti Go-kielisiin mekaanisiin käännöksiin perustuvat C-koodikantojen muunnokset säilyttävät CGO_ENABLED=0:n, mutta jäävät usein jälkeen C-julkaisuista, sisältävät emuloidun virtuaalimuistikerroksen ja varaavat kuormituksen alla runsaasti muistia. C-kirjaston siirtäminen aputtajaan loopback-TCP:n tai Unix-domain-socketien kautta palauttaa vikaeristyksen, mutta jokainen kutsu maksaa kahdesta ytimen kontekstinvaihdosta, socket-puskurikopioinneista ja siirtoformaatin serialisoinnista.
Rakensimme HQQ:n esitettyämme yhden suoran kysymyksen: entä jos C-kirjasto asuisi erillisessä käyttöjärjestelmäprosessissa omalla virtuaalisella osoiteavaruudellaan, ja Go sekä C vaihtaisivat kehyksiä suoraan jaettujen CPU-välimuistirivien kautta ilman ytimeen siirtymistä nopealla polulla?
| Integraatiomalli | CGO_ENABLED=0 | Vikaeristys | Nopean polun järjestelmäkutsut | Kutsukohtaiset keon allokaatiot | Upstream C11 -uskollisuus |
|---|---|---|---|---|---|
cgo-sisäinen linkitys | Ei | Ei (jaettu keko) | 0 | 0 tai enemmän | Tarkka upstream C |
| Socket / putki -aliprosessi RPC | Kyllä | Prosessiraja | 2–4 | Useita (kehystys + I/O) | Tarkka upstream C |
| Mekaaninen C-to-Go-käännös | Kyllä | Prosessinsisäinen Go | 0 | Korkea monimutkaisissa kutsuissa | Emuloitu C-ajonaikainen |
| HQQ jaetun muistin IPC | Kyllä | Prosessiraja | 0 (adaptiivinen odotus) | 0 (StandardLink) | Tarkka upstream C |
Arkkitehtuurin yleiskuvaus: neljä kerrosta prosessirajojen yli
HQQ erottaa prosessieristyksen neljään yhdisteltävään kerrokseen, jotta jaetun muistin renkaiden mekaniikka, tyypitetty RPC-koodigenerointi, binäärien eheyden varmistaminen ja lapsiprosessien valvonta pysyvät toisistaan riippumattomina.
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. Jaetun muistin siirto (gosuda.org/hqq ja c/)
Peruspaketti gosuda.org/hqq ja sen C11-vastine hakemistossa c/ toteuttavat kaksi jaetun muistin siirtotilaa POSIX-mmap:n (Linux ja macOS) sekä Win32-CreateFileMappingW:n (Windows) päällä. Jokainen jaettu segmentti alkaa todennetulla HQQ_STL3-superlohkolla, joka kiinnittää protokollaversion, puskurigeometrian ja päätepisteiden PID-tunnisteet ennen kuin mitään renkaan indeksiä luetaan.
Kaikki aktiiviset ohjaussanat ja slotien otsikot on kohdistettu 64 tavun välimuistirivin rajoille, jotta tuottaja- ja kuluttajaytimet eivät kilpaile samasta välimuistirivistä väärän jakamisen (false sharing) kautta.
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 +---------------------------------------------------------------------+
| Kenttä | Siirtymä | Koko | Merkitys |
|---|---|---|---|
seq | 0x00 | 8 B | Monotoninen sekvenssilaskuri. Parittomat arvot merkitsevät kesken olevaa kirjoitusta; parilliset arvot julkaisevat vahvistetun kehyksen store-release-operaatiolla. |
len | 0x08 | 4 B | Slottiin kirjoitetun hyötykuorman tavupituus (0 <= len <= BufferSize). |
flags | 0x0C | 4 B | Kehyksen ohjausbitit (jatkuvuus, virhetila tai sammutusilmoitus). |
payload | 0x40 | BufferSize | Yhtenäinen hyötykuormaalue, johon luetaan ja josta kirjoitetaan paikallaan ilman välikopiointeja. |
Kutsuja valitsee kahden siirtotason välillä samanaikaisuusrakenteen mukaan:
StandardLink(1:1 SPSC-istunto): Muodostaa client- ja server-istuntoparin omistettujen pyyntö- ja vastausslottien kautta, joita suojaa sekvenssilukko (SeqLock). Nopealla polulla kirjoittaja serialisoi tiedot suoraan kartoitettuun slottiin ja vahvistaa ne atomisella store-release-operaatiolla. Lukija odottaa lyhyesti käyttäjätilassa ennen kuin siirtyy käyttöjärjestelmän herätysprimitiiveihin (futexLinuxilla,__ulock_waitmacOS:llä,WaitOnAddressWindowsilla). Yhden kehyksen kierros valmistuu noin 200 ns:ssa ja0 allocs/op.Advanced(MPMC-jakelurengas): MultipleksoiMsamanaikaista Go-kutsujaaNC-worker-säikeelle lukkovapaan moniesittäjä-moniottajan lähetysrenkaan, jaetun slottialtaan ja indeksoidun valmistumistaulun avulla. Kutsujat varaavat slotin, julkaisevat slotin indeksin renkaaseen ja odottavat valmistumismerkintäänsä, kun worker-säikeet poistavat pyyntöjä jonosta ja suorittavat ne epäjärjestyksessä.
Molemmat tasot seuraavat vastapuolen elossaoloa jatkuvasti. Jokainen päätepiste päivittää atomisen monotonisen aikaleiman Heartbeat()-kutsulla ja tarkastaa PeerAlive(timeout)-funktiolla odottaessaan. Jos worker-prosessi kaatuu kesken kutsun, vastapuoli havaitsee vanhentuneen sykkeen tai kuolleen PID:n välittömästi, keskeyttää estävät odotukset virheellä ErrPeerGone ja tukee atomista istunnon Takeover-siirtoa jättämättä jäljelle jumiutuneita jaetun muistin tiedostoja.
2. Nollakeon RPC ja sukupolvitarkistetut kahvat (gosuda.org/hqq/bindgen)
Jaetun muistin kehystyksen kirjoittaminen käsin C-kirjastolle, jossa on kymmeniä funktioita, on työlästä ja virhealtista. hqq-bindgen on skeemakääntäjä, joka lukee Interface Definition Language (.idl) -tiedoston (tai alustaa sellaisen suoraan C-.h-otsikkotiedostosta komennolla hqq-bindgen init) ja tuottaa cgo-vapaan Go-client-paketin sekä C11-palvelinrungon.
Yleisin vakava virhe missä tahansa C-sidonnassa on paljastaa raakoja C-osoittimia (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) kielirajan yli. Jos kutsuja välittää osoittimen sen jälkeen, kun se on vapautettu, välittää osoittimen toiselle istunnolle tai korruptoi kokonaislukuosoitteen, C-kirjasto dereferoi virheellisen muistin.
hqq-bindgen kieltää raa'at osoittimet langalla. Sen sijaan IDL mallintaa tilalliset C-resurssit tyypitettyinä handle-määrittelyinä:
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}
Langalla jokainen kahva on 64-bittinen kokonaisluku, joka pakkaa 32-bittisen taulukkoindeksin ja 32-bittisen nollasta poikkeavan sukupolvilaskurin: (uint64_t)generation << 32 | index. C-ajonaikainen (c/src/hqq_rpc.c) ylläpitää istuntokohtaista kahvataulukkoa, jossa on kolme invarianttia:
- Sukupolven validointi: Jokainen haku varmistaa, että slotti on varattu, sen tallennettu sukupolvi vastaa kahvan ylempiä 32 bittiä ja sen kahvatyypin tunniste vastaa parametrin tyyppiä (
Dbvs.Stmt). Suljetun kahvan uudelleenkäyttö epäonnistuu välittömästi virheelläHQQRPC_E_BAD_HANDLEennen kuin C-kirjastoa kutsutaan. - Hierarkkinen omistajuus (
@owned_by): KoskaStmton merkitty tunnisteella@owned_by(Db),Db-kahvan sulkeminen käy automaattisesti läpi kaikki sen omistamat elävät lapsi-Stmt-kahvat ja suorittaa niiden tuhoajat käänteisessä allokointijärjestyksessä (sqlite3_finalizeennensqlite3_close_v2:ta). - Istunnon purkupyyhkäisy: Jos Go-client katkeaa tai kaatuu sulkematta kahvojaan, C-palvelimen istunnon tuhoaja pyyhkii koko istuntokahvataulukon käänteisessä järjestyksessä. Tämä takaa, ettei C-allokaatioita vuoda clientin uudelleenkäynnistysten yli.
Ennen kuin istunto käsittelee ensimmäisen RPC-kutsun, client ja palvelin vaihtavat 32 tavun SHA-256-rajapintatiivisteen, joka on laskettu kanonisoidusta IDL:stä (etuliite HQQ-BINDGEN-IDL-V1\n). Jos Go-client ja C-worker on käännetty yhteensopimattomista IDL-skeemoista, kättely hylkää yhteyden jo käynnistyksessä.
3. Väärennyksetön binäärien suoritus (gosuda.org/hqq/secureexec)
C-koodin siirtäminen worker-binääriin nostaa toimitusketjukysymyksen: miten Go-prosessi tietää, ettei levyllä oleva worker-binääri ole muuttunut tai korvattu tarkastuksen ja suorituksen välillä?
Jos crypto/sha256 kutsutaan tiedostopolulle ja sen jälkeen suoritetaan os/exec.Command(path).Start(), jää Time-of-Check to Time-of-Use (TOCTOU) -ikkuna, jonka aikana toinen prosessi voi ylikirjoittaa tai vaihtaa tiedoston ennen kuin ydin suorittaa sen. gosuda.org/hqq/secureexec sulkee tämän ikkunan seuraavasti:
- Linuxilla
secureexecavaa binäärin, virtaa sen sisällön anonyymiin muistitiedostokuvaajaan, joka luodaan kutsullamemfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING), ja laskee samalla sen SHA-256-tiivisteen. Se vertaa tiivistettä kiinnitettyyn 32 tavun tiivisteeseen tai Ed25519-allekirjoitettuun julkaisumanifestiin (cmd/hqq-manifest), soveltaa kutsuafcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE), jotta mikään järjestelmän prosessi ei voi muuttaa sivuja, ja käynnistää lapsiprosessin sinetöidystä kuvaajasta. - macOS:llä ja Windowsilla
secureexecmaterialisoi todennetut binäärit lukittuihin käyttöoikeuksiin sidottuihin välimuistihakemistoihin, jotka on nimetty SHA-256-tiivisteen mukaan, ja tarkistaa tiedoston koon sekä tiivisteen eheyden ennen käynnistystä.
4. Vanhempi-yhteyden prosessivalvonta (gosuda.org/hqq/launch)
Kun Go-sovellus käynnistää taustalla C-worker-prosessin, worker ei saa koskaan jäädä orvoksi vanhempansa kuoleman jälkeen, vaikka Go-vanhempi lopetettaisiin komennolla SIGKILL (kill -9) eikä se ehtisi suorittaa defer-siivoustoimintoja.
gosuda.org/hqq/launch pakottaa elinkaariyhteyden anonyymin käyttöjärjestelmän putken kautta, joka välitetään workerin vakiosyötteeseen lipulla --parent-stdin. Go-vanhempi pitää putken kirjoituspäätä eikä koskaan kirjoita siihen. C-worker valvoo lukupäätä omalla elinkaarisäikeellaan. Jos Go-prosessi päättyy normaalisti, kaatuu paniikkiin tai käyttöjärjestelmän ydin tappaa sen (SIGKILL tai OOM-killer), ydin sulkee kirjoituskuvaajan välittömästi. C-worker saa EOF-merkin putkesta muutamassa mikrosekunnissa, keskeyttää aktiiviset kutsut, suorittaa istuntokahvojen tuhoajat, poistaa jaetun muistin tiedostot linkityksistä ja poistuu siististi.
Pikaopas: generoidun palvelun kutsuminen
hqq-bindgen:n ja launch:n avulla eristetyn C-workerin kutsuminen Go:sta näyttää tavalliselta tyypitetyltä Go-clientilta säilyttäen samalla CGO_ENABLED=0:
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}
Repositorion rakenne
Jokainen gosuda.org/hqq:n osajärjestelmä on jäsennelty kohdennettuihin paketteihin, joilla on selkeät rajat:
1gosuda.org/hqq
2├── standard.go / ring.go # StandardLink (SPSC) ja matalan tason jaetun muistin rengasprimitiivit
3├── advanced.go / slotpool.go # Advanced (MPMC) -jakelija, slottialtaa ja valmistumistaulu
4├── shm_*.go / wait_*.go # Käyttöjärjestelmäkohtainen mmap/CreateFileMapping sekä futex/ulock/WaitOnAddress
5├── c/ # C11-siirtokirjasto (hqq.c) ja RPC-palvelimen ajonaikainen (hqq_rpc.c)
6├── bindgen/ # hqq-bindgen IDL-jäsennin, verifioija (V1–V41) ja Go/C11-koodigeneraattori
7│ └── rt/ # Nollakeon Go-clientin ajonaikainen generoiduille palveluille
8├── secureexec/ # SHA-256/Ed25519-manifestin verifiointi ja memfd-sinetöity suoritus
9├── launch/ # Worker-prosessivalvoja, jossa --parent-stdin-elinkaarihallinta
10├── cmd/hqq-manifest/ # CLI-työkalu julkaisubinäärimanifestien generointiin ja allekirjoittamiseen
11└── spec/ # Suoritettavat siirtomäärittelyt ja kultaiset testivektorit (BINDGEN.md)
Nähdäksesi, miten HQQ tarjoaa pudotettavat tuotantokorvikkeet kahdelle laajimmin käytetylle cgo-kirjastolle Go-ekosysteemissä, tutustu tähän pinoon rakennettuihin kumppanipaketteihin:
gosuda.org/sqlite3: Pudotettavadatabase/sql-ajuri, joka on yhteensopivagithub.com/mattn/go-sqlite3:n kanssa ja ajaa natiivia C SQLite3.54.1:tä jaetun muistin yli putkitetulla eräesihakulla.gosuda.org/zstd: Pudotettava korvike kirjastollegithub.com/DataDog/zstd, joka ajaa natiivia Clibzstd 1.5.7:ää eristetyssä jaetun muistin worker-prosessissa nollakeon konteksteilla ja eräkäsittelyllä.- GitHub-repositorio: https://github.com/gosuda/HQQ