GoSuda

HQQ: Önbellek Satırı Paylaşımlı Bellek Üzerinden C Kütüphanelerinin Go'dan İzolasyonu

By Lemon Mint
views ...

gosuda.org/hqq, Go ve C11 için sıfır cgo (zero-cgo) tabanlı bir süreçler arası iletişim (inter-process communication) çalışma zamanı ve kod oluşturma araç zinciridir. Yerel C kütüphanelerini ayrı işletim sistemi (OS) işçi süreçleri (worker processes) içinde izole eder ve onları bellek eşlemeli (memory-mapped), önbellek satırı hizalamalı (cache-line-aligned) paylaşımlı bellek halkaları üzerinden Go çağırıcılarına bağlar. Proje, iki aktarım katmanını (StandardLink - adanmış tek üretici tek tüketici oturumları için ve Advanced - çoklu işçi MPMC dağıtımı için), hqq-bindgen IDL derleyicisini ve kurcalaması doğrulanmış bir süreç denetçisini (secureexec ve launch) birleştirir.

1go get gosuda.org/hqq

C ile Bir Adres Alanını Paylaşmanın Maliyeti

Her Go sistem mühendisi sonunda aynı yol ayrımına gelir. CGO_ENABLED=0 ile saniyeler içinde derlenen, tek bir statik ikili dosya (binary) olarak dağıtılan, bir dizüstü bilgisayardan Linux veya Windows hedeflerine sysroot gerektirmeden çapraz derlenebilen (cross-compile) ve bellek açısından güvenli çöp toplayıcılı (garbage-collected) bir yığın (heap) içinde çalışan bir Go servisi inşa ediyorsunuzdur. Sonra olgun bir C kütüphanesine ihtiyaç duyarsınız: Gömülü ilişkisel depolama için SQLite, sözlük sıkıştırma için Zstandard (libzstd) veya donanım hızlandırmalı bir medya veya kriptografi kütüphanesi.

Bu C kütüphanesini Go sürecine bağlamak için cgo'yu etkinleştirmek, anlık bağlama problemini çözerken üç yapısal sorumluluk getirir:

  1. Araç Zinciri ve Çapraz Derleme Bağlantısı: CGO_ENABLED=1, her hedef OS ve mimari için bir C derleyicisi, hedef libc başlıkları (headers) ve bir platform bağlayıcısı (linker) gerektirir. Bir darwin/arm64 iş istasyonundan linux/arm64 veya windows/arm64 yapıları (artifacts) inşa etmek, tek bir go build komutunu çapraz derleyiciler ve sysroot'lardan oluşan bir matrise dönüştürür.
  2. Çalışma Zamanı Zamanlama Ek Yükü (Runtime Scheduling Overhead): Her cgo çağrısı Go çalışma zamanı sınırını geçer, büyütülebilir bir goroutine yığınından sistem yığınına geçer ve bir OS iş parçacığını (M) sabitler. İnce taneli çağrılar için geçiş vergisi (çağrı başına yaklaşık 50 ila 150 ns) ve zamanlayıcı çekişmesi hızla birikir.
  3. Hata Alanı Çöküşü (Fault Domain Collapse): C kodu, Go çalışma zamanı ile aynı sanal adres alanında çalışır. Bir C fonksiyonunun içindeki tek bir sınır dışı yazma (out-of-bounds write), kullanım sonrası serbest bırakma (use-after-free), yığın taşması (stack overflow) veya abort() çağrısı, Go yığın meta verilerini bozar veya tüm sunucu sürecini derhal sonlandırarak recover() mekanizmasını atlar ve devam etmekte olan tüm HTTP veya RPC isteklerini beraberinde çökertir.

Ekipler cgo'dan kaçınmaya çalıştığında, yaygın alternatifler farklı bir uzlaşmaya zorlar. C kod tabanlarının saf Go (pure-Go) mekanik çevirileri CGO_ENABLED=0 durumunu korur, ancak genellikle yukarı akış (upstream) C sürümlerinin gerisinde kalır, taklit edilmiş bir sanal bellek katmanı taşır ve yük altında yoğun şekilde bellek tahsis eder. C kütüphanesini geri döngü (loopback) TCP veya Unix etki alanı soketleri (domain sockets) üzerinden bir yardımcı sürece taşımak hata izolasyonunu yeniden sağlar, ancak her çağrı iki çekirdek bağlam geçişi (kernel context switch), soket tampon kopyalamaları ve kablo serileştirmesi (wire serialization) için bedel öder.

HQQ'yu doğrudan bir soru sorduktan sonra inşa ettik: ya C kütüphanesi, kendi sanal adres alanına sahip ayrı bir OS sürecinde yaşasaydı ve Go ile C, hızlı yolda çekirdeğe girmeden doğrudan CPU önbellek satırları üzerinden çerçeveleri (frames) takas etseydi ne olurdu?

Entegrasyon ModeliCGO_ENABLED=0Hata İzolasyonuHızlı Yol Sistem ÇağrılarıÇağrı Başına Yığın TahsisleriYukarı Akış C11 Sadakati
cgo Süreç İçi BağlamaHayırYok (paylaşımlı yığın)00 ila 1+Tam yukarı akış C
Soket / Boru Alt Süreç RPCEvetSüreç sınırı2 ila 4Çoklu (çerçeveleme + I/O)Tam yukarı akış C
Mekanik C-Go ÇevirisiEvetSüreç içi Go0Karmaşık çağrılarda yüksekTaklit edilmiş C çalışma zamanı
HQQ Paylaşımlı Bellek IPCEvetSüreç sınırı0 (uyarlanabilir spin)0 (StandardLink)Tam yukarı akış C

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

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

 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. Paylaşımlı Bellek Aktarımı (gosuda.org/hqq ve c/)

Temel paket gosuda.org/hqq ve c/ içindeki 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 hoca indeksinin okunmasından önce protokol sürümünü, tampon geometrisini ve uç nokta (endpoint) PID'lerini sabitleyen doğrulanmış bir HQQ_STL3 süper bloğu ile başlar.

Tüm sıcak kontrol kelimeleri (hot control words) ve yuva başlıkları (slot headers) 64 baytlık önbellek satırı sınırlarına hizalanmıştır; böylece üretici ve tüketici çekirdekleri, yanlış paylaşım (false sharing) yoluyla asla aynı önbellek satırı için çekişmez.

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              +---------------------------------------------------------------------+
AlanOfsetBoyutAnlambilim
seq0x008 BMonotonik sıra sayaç. Tek değerler devam eden bir yazmayı işaret eder; çift değerler store-release yoluyla işlenmiş bir çerçeveyi yayınlar.
len0x084 BYuvaya yazılan yük bayt uzunluğu (0 <= len <= BufferSize).
flags0x0C4 BÇerçeve kontrol bitleri (devam, hata durumu veya kapatma bildirimi).
payload0x40BufferSizeAra kopyalar olmaksızın yerinde okunan ve yazılan bitişik yük bölgesi.

Çağırıcılar, 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 sıra kilidi (SeqLock) ile korunan adanmış istek ve yanıt yuvaları üzerinden bir istemci ve sunucu oturumunu eşleştirir. Hızlı yolda yazar, doğrudan eşlenmiş yuvaya serileştirme yapar ve atomik bir store-release ile commit eder; okuyucu, işletim sistemi uyandırma temellerine (Linux üzerinde futex, macOS üzerinde __ulock_wait, Windows üzerinde WaitOnAddress) başvurmadan önce kullanıcı alanında kısa süre döner (spin). Tek çerçeveli bir gidiş-dönüş 0 allocs/op ile yaklaşık 200 ns içinde tamamlanır.
  • Advanced (MPMC Dağıtım Halkası): Kilitsiz (lock-free) çoklu üretici çoklu tüketici (multi-producer multi-consumer) gönderim halkası, paylaşımlı yuva havuzu (slot pool) ve indekslenmiş tamamlama tablosu kullanarak M eşzamanlı Go çağırıcısını N C işçi iş parçacığı üzerinde çoklar (multiplex). Çağırıcılar bir yuva ayırır, yuva indeksini halkaya yayınlar ve işçi iş parçacıkları istekleri sırasız olarak sıradan çıkarıp tamamlarken kendi tamamlama girdilerini beklerler.

Her iki katman da eşler arası canlılığı (peer liveness) sürekli olarak takip eder. Her uç nokta Heartbeat() aracılığıyla atomik bir monotonik zaman damgası günceller ve beklerken PeerAlive(timeout) durumunu inceler. Bir işçi süreç çağrı ortasında çökerse, eş (peer) bayat kalp atışını veya ölü PID'yi derhal tespit eder, engellenen beklemeleri ErrPeerGone ile iptal eder ve arkasında sıkışmış paylaşımlı bellek dosyaları bırakmadan atomik oturum Takeover (Devralma) işlemlerini destekler.

2. Sıfır Tahsisli RPC ve Nesil Denetimli Tutamaçlar (gosuda.org/hqq/bindgen)

Onlarca fonksiyonu olan bir C kütüphanesi için paylaşımlı bellek çerçevelemesini elle yazmak yorucu ve hata etmeye açıktır. hqq-bindgen, bir Arayüz Tanım Dili (.idl) dosyasını okuyan (veya hqq-bindgen init aracılığıyla doğrudan bir C .h başlığından önyükleyen) ve bir C11 sunucu iskeletinin yanı sıra sıfır cgo Go istemci paketi yayan 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çığa çıkarmaktır. Bir çağırıcı serbest bıraktıktan sonra bir işaretçi geçirirse, başka bir oturuma ait bir işaretçi geçirirse veya tamsayı bir adresi bozarsa, C kütüphanesi geçersiz belleğe başvurur (dereference).

hqq-bindgen kabloda ham işaretçileri yasaklar. Bunun yerine IDL, durum bilgisi olan C kaynaklarını tipli 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}

Kabloda her tutamaç (handle), 32 bitlik bir tablo indeksini ve 32 bitlik sıfır olmayan bir nesil 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şmeze (invariants) sahip oturum başına bir tutamaç tablosu tutar:

  1. Nesil Doğrulama (Generation Validation): Her arama, yuvanın dolu olduğunu, depolanan neslin tutamağın üst 32 bitiyle eşleştiğini ve tutamaç türü etiketinin parametre türüyle (Db ve Stmt) eşleştiğini doğrular. Kapatılmış bir tutamağı yeniden kullanmak, C kütüphanesine çağrı yapmadan hemen önce HQQRPC_E_BAD_HANDLE ile başarısız olur.
  2. Hiyerarşik Sahiplik (@owned_by): Stmt, @owned_by(Db) ile etiketlendiğinden, bir Db tutamağının kapatılması, o Db'ye ait tüm canlı alt Stmt tutamaıklarını otomatik olarak dolaşır ve yıkıcılarını (destructors) ters tahsis sırasına göre çalıştırır (sqlite3_close_v2 öncesinde sqlite3_finalize).
  3. Oturum Sonlandırma Süpürmesi (Session Teardown Sweep): Bir Go istemcisi tutamaçlarını kapatmadan bağlantısını keserse veya çökerse, C sunucusunun oturum yıkıcısı tüm oturum tutamaç tablosunu ters sırada süpürür ve istemci yeniden başlatmaları arasında sıfır sızdırılmış C tahsisatını garanti eder.

Bir oturum ilk RPC çağrısını işlemeden önce istemci ve sunucu, standartlaştırılmış IDL üzerinden hesaplanan 32 baytlık bir SHA-256 arayüz özetini (HQQ-BINDGEN-IDL-V1\n öneki) takas eder. Go istemcisi ve C işçisi uyumsuz IDL şemalarından derlendiyse, el sıkışma önyüklemede bağlantıyı reddeder.

3. Kurcalamasız İkili Çalıştırma (gosuda.org/hqq/secureexec)

C kodunu bir işçi ikili dosyasına taşımak bir tedarik zinciri sorusu ortaya çıkarır: Go süreci, diskteki işçi ikili dosyasının doğrulama ile yürütme arasında değiştirilmediğini veya değiştirilerek yer değiştirtilmediğini nasıl bilir?

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

  • Linux üzerinde secureexec ikili dosyayı açar, SHA-256 karmasını hesaplarken memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING) aracılığıyla oluşturulan anonim bir bellek dosya tanımlayıcısına (file descriptor) içeriğini aktarır, karmayı sabitlenmiş 32 baytlık bir özete veya Ed25519 ile imzalanmış bir sürüm bildirimine (cmd/hqq-manifest) karşı doğrular, sistemdeki hiçbir sürecin sayfaları mutasyona uğratamaması 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ürlü tanımlayıcıdan başlatır.
  • macOS ve Windows üzerinde secureexec, doğrulanmış ikili dosyaları SHA-256 özetine göre anahtarlanmış izin kilitli önbellek dizinlerinde somutlaştırır ve başlatmadan önce dosya boyutu ile özet bütünlüğünü doğrular.

4. Ebeveyn Can Damarı Süreç Denetimi (gosuda.org/hqq/launch)

Bir Go uygulaması arka planda bir 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 asla ebeveyninden daha uzun süre yetim bir süreç olarak yaşamamalıdır.

gosuda.org/hqq/launch, --parent-stdin bayrağı ile işçinin standart girdisine iletilen anonim bir işletim sistemi borusu (pipe) aracılığıyla ömür bağlamasını zorunlu kılar. Go ebeveyni borunun yazma ucunu elinde tutar ve asla üzerine yazmaz; C işçisi okuma ucunu adanmış bir can damarı (lifeline) iş parçacığında izler. Go süreci normal şekilde çıkarsa, paniklerse veya işletim sistemi çekirdeği (SIGKILL veya OOM katili) tarafından öldürülürse, çekirdek yazma tanımlayıcısını derhal kapatır. C işçisi boru üzerinde mikro saniyeler içinde EOF alır, aktif çağrıları kesintiye uğratır, oturum tutamaç yıkıcılarını çalıştırır, paylaşımlı bellek dosyalarının bağlantısını keser (unlink) ve temiz bir şekilde çıkar.

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

hqq-bindgen ve launch ile, CGO_ENABLED=0 durumunu korurken izole edilmiş bir C işçisini Go'dan çağırmak, normal tipli 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ış paketler halinde düzenlenmiştir:

 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)

HQQ'nun, Go ekosistemindeki en yaygın kullanılan cgo kütüphanelerinden ikisi için üretime yönelik açılır (drop-in) alternatiflere nasıl güç verdiğini görmek için bu yığın üzerine inşa edilmiş eşlik eden paketlere göz atın:

  • gosuda.org/sqlite3: Boru hattıyla birleştirilmiş toplu ön getirme (pipelined batch prefetching) ile paylaşımlı bellek üzerinden yerel C SQLite 3.54.1 çalıştıran, github.com/mattn/go-sqlite3 ile uyumlu açılır database/sql sürücüsü.
  • gosuda.org/zstd: Sıfır tahsisli bağlamlar ve toplu paketleme (bulk batching) ile izole edilmiş paylaşımlı bellek işçisinde yerel C libzstd 1.5.7 çalıştıran, github.com/DataDog/zstd için açılır alternatif.
  • GitHub Deposu: https://github.com/gosuda/HQQ