GoSuda

HQQ: Önbellek Hattı Paylaşımlı Bellek Üzerinden C Kütüphanelerinin Go'dan İzole Edilmesi

By Lemon Mint views ...

gosuda.org/hqq, C11 ve Go için sıfır-cgo bir süreçlerarası iletişim çalışma zamanı ve kod üretme araç zinciridir. Yerel C kitaplıklarını ayrı işletim sistemi çalışan süreçlerinde izole eder ve bunları bellek eşlemeli, önbellek satırına hizalanmış paylaşımlı bellek halkaları üzerinden Go çağıranlarına bağlar. Proje, iki aktarım katmanını (tek üretici-tek tüketicili özel oturumlar için StandardLink ve çok işçili MPMC dağıtımı için Advanced), hqq-bindgen IDL derleyicisini ve doğrulanmış bir süreç denetleyicisini (secureexec ve launch) birleştirir.

1go get gosuda.org/hqq

C ile Adres Uzayını Paylaşmanın Maliyeti

Her Go sistem mühendisi er ya da geç aynı yol ayrımıyla karşılaşır. CGO_ENABLED=0 ile saniyeler içinde derlenen, tek bir statik ikili dosya olarak dağıtılan, bir sysroot gerektirmeden bir dizüstü bilgisayardan Linux veya Windows hedeflerine çapraz derlenebilen ve bellek açısından güvenli, çöp toplayıcılı bir yığında çalışan bir Go servisi geliştiriyorsunuz. Ardından olgun bir C kitaplığına ihtiyaç duyarsınız: gömülü ilişkisel depolama için SQLite, sözlük sıkıştırması için Zstandard (libzstd) ya da donanım hızlandırmalı bir medya veya kriptografi kitaplığı.

Bu C kitaplığını Go süreci içine bağlamak için cgo'yu etkinleştirmek, anlık bağlama sorununu çözer ancak üç yapısal sorumluluk doğurur:

  1. Araç Zinciri ve Çapraz Derleme Bağımlılığı: CGO_ENABLED=1, her hedef işletim sistemi ve mimarisi için bir C derleyicisi, hedef libc başlıkları ve bir platform bağlayıcısı gerektirir. Bir darwin/arm64 iş istasyonundan linux/arm64 veya windows/arm64 yapıtları oluşturmak, tek bir go build komutunu bir çapraz derleyiciler ve sysroot matrisine dönüştürür.
  2. Çalışma Zamanı Zamanlama Yükü: Her cgo çağrısı Go çalışma zamanı sınırını geçer, büyüyebilen bir goroutine yığınından bir sistem yığınına geçer ve bir işletim sistemi iş parçacığını (M) sabitler. İnce taneli çağrılarda geçiş maliyeti (çağrı başına yaklaşık 50 ila 150 ns) ve zamanlayıcı çekişmesi hızla birikir.
  3. Hata Alanının Çökmesi: C kodu, Go çalışma zamanıyla aynı sanal adres uzayında çalışır. Bir C işlevi içindeki tek bir sınır dışı yazma, serbest bırakılmış belleğe erişim, yığın taşması veya abort() çağrısı, Go yığın meta verilerini bozar ya da recover() mekanizmasını atlayarak tüm sunucu sürecini anında sonlandırır ve uçuşta olan her HTTP veya RPC isteğini de beraberinde götürür.

Ekipler cgo'dan kaçınmaya çalıştığında, yaygın alternatifler farklı bir uzlaşma yapmayı zorunlu kılar. C kod tabanlarının saf Go'ya mekanik çevirileri CGO_ENABLED=0 desteğini korur, ancak genellikle üst akış C sürümlerinin gerisinde kalır, öykünülmüş bir sanal bellek katmanı taşır ve yük altında yoğun bellek ayırır. C kitaplığını yerel döngü TCP veya Unix alan soketleri üzerinden bir yardımcı sürece taşımak hata izolasyonunu geri kazandırır, ancak her çağrı iki çekirdek bağlam anahtarlaması, soket tampon kopyaları ve kablo serileştirmesi bedelini öder.

HQQ'yu doğrudan bir soruyu sorarak geliştirdik: C kitaplığı kendi sanal adres uzayına sahip ayrı bir işletim sistemi sürecinde yaşarsa ve Go ile C, hızlı yolda çekirdeğe girmeden çerçeveleri doğrudan paylaşılan CPU önbellek satırları üzerinden değiş tokuş ederse ne olur?

Entegrasyon ModeliCGO_ENABLED=0Hata İzolasyonuHızlı Yol Sistem ÇağrılarıÇağrı Başına Yığın AyırmalarıÜst Akış C11 Sadakati
cgo Süreç İçi BağlamaHayırYok (paylaşımlı yığın)00 ila 1+Birebir üst akış C
Soket / Boru Alt Süreç RPCEvetSüreç sınırı2 ila 4Birden çok (çerçeveleme + G/Ç)Birebir üst akış C
Mekanik C'den Go'ya ÇeviriEvetSüreç içi Go0Karmaşık çağrılarda yüksekÖykünülmüş C çalışma zamanı
HQQ Paylaşımlı Bellek IPCEvetSüreç sınırı0 (uyarlanabilir döngü)0 (StandardLink)Birebir üst akış C

Mimari Genel Bakış: Süreç Sınırları Boyunca Dört Katman

HQQ, süreç izolasyonunu birbirinden bağımsız kalacak şekilde birleştirilebilir dört katmana ayırır; böylece paylaşımlı bellek halkası mekaniği, türlendirilmiş RPC kod üretimi, ikili bütünlük doğrulaması ve alt süreç denetimi birbirinden bağımsız kalır.

 1+-------------------------------------------------------------------+
 2| Go Uygulama Süreci (CGO_ENABLED=0)                                |
 3|                                                                   |
 4|  +-----------------------+     +-------------------------------+  |
 5|  | Üretilmiş Go İstemcisi|     | Süreç Denetleyicisi           |  |
 6|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
 7|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
 8|  +-----------+-----------+     +---------------+---------------+  |
 9|              |                                 |                  |
10|  +-----------v-----------+                     | Anonim Boru      |
11|  | Aktarım Katmanı       |                     | Yaşam Hattı      |
12|  | (StandardLink /       |                     | (--parent-stdin) |
13|  |  Advanced MPMC)       |                     |                  |
14+--------------+---------------------------------+------------------+
15               |                                 |
16===============|========= Paylaşımlı Bellek ======|===================
17               |     (mmap / CreateFileMapping)  |
18+--------------v---------------------------------v------------------+
19| C11 Çalışan Süreç (İzole Adres Uzayı)                             |
20|                                                                   |
21|  +-----------------------+     +-------------------------------+  |
22|  | Üretilmiş C11 Sunucusu|---->| Üretim Denetimli Tutamaç      |  |
23|  | & Toplu Katmanlar     |     | Tablosu (hqq_rpc.c)           |  |
24|  +-----------+-----------+     +---------------+---------------+  |
25|              |                                 |                  |
26|              +----------------+----------------+                  |
27|                               |                                   |
28|                     +---------v---------+                         |
29|                     | Yerel C Kitaplığı |                         |
30|                     | (SQLite, Zstd...) |                         |
31|                     +-------------------+                         |
32+-------------------------------------------------------------------+

1. Paylaşımlı Bellek Aktarımı (gosuda.org/hqq ve c/)

Temel gosuda.org/hqq paketi ve c/ dizinindeki C11 karşılığı, POSIX mmap (Linux ve macOS) ile Win32 CreateFileMappingW (Windows) üzerinden iki paylaşımlı bellek aktarım modu uygular. Her paylaşımlı segment, herhangi bir halka indeksi okunmadan önce protokol sürümünü, tampon geometrisini ve uç nokta PID'lerini sabitleyen doğrulanmış bir HQQ_STL3 süper bloğuyla başlar.

Tüm sıcak kontrol kelimeleri ve yuva başlıkları, üretici ve tüketici çekirdeklerinin yanlış paylaşım (false sharing) nedeniyle aynı önbellek satırı üzerinde çakışmaması için 64 baytlık önbellek satırı sınırlarına hizalanmıştır.

1StandardLink Yuva Düzeni (64 Baytlık Önbellek Satırına Hizalı)
2Bayt Ofseti:  0x00                0x08                0x10                          0x40
3              +-------------------+-------------------+-----------------------------+
4Başlık Satırı:| seq (u64 atomik)  | len (u32) | flags | reserved (sıfır dolgulu)    |
5              +-------------------+-------------------+-----------------------------+
6Yük Alanı:    | yük baytları [0 .. BufferSize - 1] (doğrudan kodlanmış / çözülmüş)  |
7              +---------------------------------------------------------------------+
AlanOfsetBoyutAnlamı
seq0x008 BMonoton dizi sayacı. Tek değerler devam eden bir yazmayı işaretler; çift değerler store-release ile onaylanmış bir çerçeveyi yayımlar.
len0x084 BYuvaya yazılan yük bayt uzunluğu (0 <= len <= BufferSize).
flags0x0C4 BÇerçeve kontrol bitleri (devamlılık, hata durumu veya kapanış bildirimi).
payload0x40BufferSizeAra kopyalar olmadan yerinde okunan ve yazılan bitişik yük bölgesi.

Çağıranlar, eşzamanlılık yapısına bağlı olarak iki aktarım katmanı arasında seçim yapar:

  • StandardLink (1:1 SPSC Oturumu): Bir istemci ve bir sunucu oturumunu, bir sıra kilidi (SeqLock) ile korunan özel istek ve yanıt yuvaları üzerinden eşleştirir. Hızlı yolda yazıcı, doğrudan eşlenmiş yuvaya serileştirir ve atomik store-release ile onaylar; okuyucu, OS uyandırma ilkellerine (Linux'ta futex, macOS'ta __ulock_wait, Windows'ta WaitOnAddress) geri dönmeden önce kullanıcı alanında kısa süre döner. Tek çerçeveli bir gidiş-dönüş yaklaşık 200 ns'de ve 0 allocs/op ile tamamlanır.
  • Advanced (MPMC Dağıtım Halkası): Kilitsiz çoklu üretici çoklu tüketici gönderim halkası, paylaşımlı bir yuva havuzu ve indeksli bir tamamlanma tablosu kullanarak M eşzamanlı Go çağıranını N C işçi iş parçacığı üzerinden çoklar. Çağıranlar bir yuva ayırır, yuva indeksini halkaya yayımlar ve işçi iş parçacıkları istekleri sıra dışı kuyruktan çıkarıp tamamlarken kendi tamamlanma girdilerini bekler.

Her iki katman da eş canlılığını sürekli izler. Her uç nokta, Heartbeat() aracılığıyla atomik monoton bir zaman damgasını günceller ve beklerken PeerAlive(timeout) ile denetler. Bir işçi süreci bir çağrının ortasında çökerse, eş bayat kalp atışını ya da ölü PID'yi hemen fark eder, bloke olmuş beklemeleri ErrPeerGone ile iptal eder ve geride kilitli paylaşımlı bellek dosyaları bırakmadan atomik oturum Takeover işlemini destekler.

2. Sıfır Ayırmalı RPC ve Üretim Denetimli Tutamaçlar (gosuda.org/hqq/bindgen)

Düzinelerce işlevi olan bir C kitaplığı için paylaşımlı bellek çerçevelemesini elle yazmak zahmetli ve hataya açıktır. hqq-bindgen, bir Arayüz Tanımlama Dili (.idl) dosyasını okuyan (veya hqq-bindgen init ile doğrudan bir C .h başlığından bir tane türeten) ve bir C11 sunucu iskeletiyle birlikte sıfır-cgo bir Go istemci paketi üreten bir şema derleyicisidir.

Herhangi bir C bağlamasındaki en tehlikeli hata, ham C işaretçilerini (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) dil sınırının ötesine açmaktır. Bir çağıran, serbest bıraktıktan sonra bir işaretçi geçerse, başka bir oturuma ait bir işaretçi geçerse ya da bir tamsayı adresini bozarsa, C kitaplığı geçersiz belleğe erişir.

hqq-bindgen, tel üzerinde ham işaretçilere izin vermez. Bunun yerine IDL, durumlu C kaynaklarını türlendirilmiş handle bildirimleri olarak modeller:

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}

Tel üzerinde her tutamaç, 32 bitlik bir tablo indeksini ve 32 bitlik sıfır olmayan bir üretim sayacını paketleyen 64 bitlik bir tamsayıdır: (uint64_t)generation << 32 | index. C çalışma zamanı (c/src/hqq_rpc.c), üç değişmezi koruyan oturum başına bir tutamaç tablosu tutar:

  1. Üretim Doğrulaması: Her arama, yuvanın dolu olduğunu, saklanan üretimin tutamacın üst 32 bitiyle eşleştiğini ve tutamaç türü etiketinin parametre türüyle (Db ya da Stmt) uyuştuğunu doğrular. Kapatılmış bir tutamacın yeniden kullanılması, C kitaplığına çağrı yapılmadan önce HQQRPC_E_BAD_HANDLE ile hemen başarısız olur.
  2. Hiyerarşik Sahiplik (@owned_by): Stmt, @owned_by(Db) ile işaretlendiği için, bir Db tutamacını kapatmak, o Db tarafından sahip olunan tüm canlı alt Stmt tutamaçlarını otomatik olarak dolaşır ve yıkıcılarını ayırma sırasının tersine çalıştırır (sqlite3_close_v2'den önce sqlite3_finalize).
  3. Oturum Sonlandırma Taraması: Bir Go istemcisi tutamaçlarını kapatmadan bağlantısı kesilir ya da çökerse, C sunucusunun oturum yıkıcısı tüm oturum tutamaç tablosunu ters sırada tarar ve istemci yeniden başlatmaları boyunca sıfır sızıntılı C ayırmasını garanti eder.

Bir oturum ilk RPC çağrısını işlemeden önce, istemci ve sunucu, kanonikleştirilmiş IDL üzerinden hesaplanan 32 baytlık bir SHA-256 arayüz özetini (HQQ-BINDGEN-IDL-V1\n öneki ile) değiş tokuş eder. Go istemcisi ve C işçisi uyumsuz IDL şemalarından derlenmişse, el sıkışma bağlantıyı başlangıçta reddeder.

3. Kurcalamaya Karşı Korumalı İkili Yürütme (gosuda.org/hqq/secureexec)

C kodunu bir işçi ikili dosyasına taşımak bir tedarik zinciri sorusunu gündeme getirir: Go süreci, doğrulama ile yürütme arasında diskteki işçi ikili dosyasının değiştirilmediğini ya da yerine başka bir dosya konulmadığını nasıl bilir?

Bir dosya yolu üzerinde crypto/sha256 çağırmak, ardından os/exec.Command(path).Start() çağırmak, çekirdek dosyayı çalıştırmadan önce başka bir sürecin dosyanın üzerine yazabileceği veya dosyayı değiştirebileceği bir Kontrol Zamanı ile Kullanım Zamanı (TOCTOU) penceresi bırakır. gosuda.org/hqq/secureexec bu pencereyi kapatır:

  • Linux'ta secureexec, ikiliyi açar, içeriğini memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING) ile oluşturulan anonim bellek dosyası tanımlayıcısına akıtırken SHA-256 özetini hesaplar, özeti sabitlenmiş 32 baytlık bir özete ya da Ed25519 ile imzalanmış bir sürüm manifestosuna (cmd/hqq-manifest) karşı doğrular, sistemdeki hiçbir sürecin sayfaları değiştirememesi için fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) uygular ve alt süreci mühürlenmiş tanımlayıcıdan başlatır.
  • macOS ve Windows'ta secureexec, doğrulanmış ikilileri SHA-256 özetiyle anahtarlanmış, izinleri kilitli önbellek dizinlerine yerleştirir ve başlatmadan önce dosya boyutunu ve özet bütünlüğünü doğrular.

4. Ebeveyn Yaşam Hattı ile Süreç Denetimi (gosuda.org/hqq/launch)

Bir Go uygulaması arka plan C işçi süreci başlattığında, Go ebeveyni SIGKILL (kill -9) ile sonlandırılsa ve defer temizleme işleyicilerini çalıştıramasa bile, işçi hiçbir zaman ebeveyninden yetim bir süreç olarak yaşamamalıdır.

gosuda.org/hqq/launch, --parent-stdin bayrağıyla işçinin standart girdisine aktarılan anonim bir OS borusu üzerinden yaşam döngüsü bağını zorunlu kılar. Go ebeveyni borunun yazma ucunu tutar ve oraya hiçbir zaman yazmaz; C işçisi okuma ucunu özel bir yaşam hattı iş parçacığında izler. Go süreci normal şekilde çıkarsa, panik yaparsa veya işletim sistemi çekirdeği tarafından sonlandırılırsa (SIGKILL ya da OOM öldürücüsü), çekirdek yazma tanımlayıcısını anında kapatır. C işçisi boru üzerinde mikrosaniyeler içinde EOF alır, etkin çağrıları keser, oturum tutamaç yıkıcılarını çalıştırır, paylaşımlı bellek dosyalarının bağlantısını kaldırır ve temiz bir şekilde çıkar.

Hızlı Başlangıç: Üretilmiş Bir Servisi Çağırmak

hqq-bindgen ve launch ile izole bir C işçisini Go'dan çağırmak, CGO_ENABLED=0 ortamını koruyarak sıradan türlendirilmiş bir Go istemcisi gibi görünür:

 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}

Depo Yapısı

gosuda.org/hqq içindeki her alt sistem, açık sınırlara sahip odaklanmış paketlere düzenlenmiştir:

 1gosuda.org/hqq
 2├── standard.go / ring.go      # StandardLink (SPSC) ve düşük seviyeli paylaşımlı bellek halka ilkelleri
 3├── advanced.go / slotpool.go  # Advanced (MPMC) dağıtıcı, yuva havuzu ve tamamlanma tablosu
 4├── shm_*.go / wait_*.go       # OS'e özgü mmap/CreateFileMapping ve futex/ulock/WaitOnAddress
 5├── c/                         # C11 aktarım kitaplığı (hqq.c) ve RPC sunucu çalışma zamanı (hqq_rpc.c)
 6├── bindgen/                   # hqq-bindgen IDL ayrıştırıcısı, doğrulayıcı (V1-V41) ve Go/C11 kod üreteci
 7│   └── rt/                    # Üretilmiş servisler için sıfır ayırmalı Go istemci çalışma zamanı
 8├── secureexec/                # SHA-256 / Ed25519 manifesto doğrulaması ve memfd mühürlü yürütme
 9├── launch/                    # --parent-stdin yaşam hattı yönetimi ile işçi süreç denetleyicisi
10├── cmd/hqq-manifest/          # Sürüm ikili manifestolarını üretmek ve imzalamak için CLI aracı
11└── spec/                      # Çalıştırılabilir kablo spesifikasyonları ve altın test vektörleri (BINDGEN.md)

HQQ'nun Go ekosistemindeki en yaygın kullanılan iki cgo kitaplığı için doğrudan üretim ortamında yerine geçebilen bileşenleri nasıl desteklediğini görmek için, bu yığın üzerine kurulmuş tamamlayıcı paketlere bakın:

  • gosuda.org/sqlite3: github.com/mattn/go-sqlite3 ile uyumlu, doğrudan kullanılabilen bir database/sql sürücüsü; paylaşımlı bellek üzerinden boru hattı toplu ön getirme ile yerel C SQLite 3.54.1 çalıştırır.
  • gosuda.org/zstd: github.com/DataDog/zstd için doğrudan yerine geçen bir bileşen; sıfır ayırmalı bağlamlar ve toplu işleme ile yalıtılmış bir paylaşımlı bellek işçisinde yerel C libzstd 1.5.7 çalıştırır.
  • GitHub Deposu: https://github.com/gosuda/HQQ