---
id: 43974d116e135fd9d3478f658dcf71c6
author: Lemon Mint
title: 'HQQ: 캐시 라인 공유 메모리를 통한 Go와 C 라이브러리의 격리'
description: 제로-cgo IPC 런타임, IDL 컴파일러(hqq-bindgen), 그리고 검증된 프로세스 감독기로서, 공유 메모리 링을 통해 격리된 워커 프로세스에서 네이티브 C11 라이브러리를 실행합니다.
language: ko
date: 2026-10-18T13:56:46.094643498Z
path: /hqq
go_package: gosuda.org/hqq
go_repourl: https://github.com/gosuda/HQQ.git
category: lowlevel
---

`gosuda.org/hqq`는 zero-cgo 프로세스 간 통신(IPC) 런타임이자 Go 및 C11용 코드 생성 툴체인입니다. 이 프로젝트는 네이티브 C 라이브러리를 별도의 OS 워커 프로세스 내부에 격리하고, 메모리 매핑되고 캐시 라인에 정렬된 공유 메모리 링(ring)을 통해 이를 Go 호출자와 연결합니다. 이 프로젝트는 두 가지 전송 계층(단일 생산자 단일 소비자 전용 세션인 `StandardLink`와 다중 워커 MPMC 디스패치인 `Advanced`), `hqq-bindgen` IDL 컴파일러, 그리고 변조 검증을 수행하는 프로세스 감독기(`secureexec` 및 `launch`)를 결합합니다.

```bash
go get gosuda.org/hqq
```

## C와 주소 공간을 공유하는 데 따르는 비용

모든 Go 시스템 엔지니어는 결국 동일한 갈림길에 직면하게 됩니다. 여러분은 `CGO_ENABLED=0`으로 몇 초 만에 컴파일되고, 단일 정적 바이너리로 배포되며, sysroot 없이도 노트북에서 Linux 또는 Windows 대상으로 교차 컴파일할 수 있고, 메모리 안전성이 보장된 가비지 컬렉션 힙 안에서 실행되는 Go 서비스를 구축하고 있습니다. 그런데 SQLite(임베디드 관계형 저장소), 사전 기반 압축을 위한 Zstandard(`libzstd`), 또는 하드웨어 가속 미디어 또는 암호화 라이브러리와 같은 성숙한 C 라이브러리가 필요해집니다.

`cgo`를 활성화하여 해당 C 라이브러리를 Go 프로세스에 링크하면 당장의 바인딩 문제는 해결되지만, 세 가지 구조적 부담이 발생합니다.

1. **툴체인 및 교차 컴파일 결합**: `CGO_ENABLED=1`은 모든 대상 OS 및 아키텍처마다 C 컴파일러, 대상 libc 헤더, 플랫폼 링커를 요구합니다. `darwin/arm64` 워크스테이션에서 `linux/arm64` 또는 `windows/arm64` 아티팩트를 빌드하려면 단일 `go build` 명령이 교차 컴파일러와 sysroot의 매트릭스로 바뀝니다.
2. **런타임 스케줄링 오버헤드**: 모든 `cgo` 호출은 Go 런타임 경계를 넘고, 확장 가능한 고루틴 스택에서 시스템 스택으로 전환하며, OS 스레드(`M`)를 고정합니다. 세밀한 호출의 경우 전환 비용(호출당 약 50~150 ns)과 스케줄러 경합이 빠르게 누적됩니다.
3. **장애 도메인 붕괴**: C 코드는 Go 런타임과 동일한 가상 주소 공간에서 실행됩니다. C 함수 내부의 단일 범위 외 쓰기, 해제 후 사용(use-after-free), 스택 오버플로, 또는 `abort()`는 Go 힙 메타데이터를 손상시키거나 전체 서버 프로세스를 즉시 종료시키며, `recover()`를 우회하여 진행 중인 모든 HTTP 또는 RPC 요청을 함께 중단시킵니다.

팀이 `cgo`를 피하려고 할 때 흔히 선택하는 대안은 다른 종류의 절충을 강요합니다. C 코드베이스를 순수 Go로 기계적으로 변환하면 `CGO_ENABLED=0`은 유지되지만, 상류 C 릴리스를 따라가지 못하는 경우가 많고, 에뮬레이션된 가상 메모리 계층을 포함하며, 부하 상황에서 메모리를 대량으로 할당합니다. C 라이브러리를 루프백 TCP 또는 Unix 도메인 소켓을 통해 헬퍼 프로세스로 옮기면 장애 격리는 회복되지만, 모든 호출이 두 번의 커널 컨텍스트 전환, 소켓 버퍼 복사, 와이어 직렬화 비용을 지불해야 합니다.

우리는 하나의 직접적인 질문을 던진 뒤 HQQ를 만들었습니다. C 라이브러리를 자체 가상 주소 공간을 가진 별도의 OS 프로세스에 두고, Go와 C가 고속 경로에서 커널에 진입하지 않고도 CPU 캐시 라인을 통해 프레임을 직접 교환할 수 있다면 어떨까?

| 통합 모델                          | `CGO_ENABLED=0` | 장애 격리          | 고속 경로 시스템 콜 | 호출당 힙 할당            | 상류 C11 충실도       |
| :--------------------------------- | :-------------- | :----------------- | ------------------: | ------------------------: | :-------------------- |
| **`cgo` 인프로세스 링크**          | 아니오          | 없음 (공유 힙)     |                   0 |                   0 ~ 1+ | 정확한 상류 C         |
| **소켓 / 파이프 서브프로세스 RPC** | 예              | 프로세스 경계      |                 2 ~ 4 |  다수 (프레이밍 + I/O)    | 정확한 상류 C         |
| **기계적 C-to-Go 변환**            | 예              | 프로세스 내 Go     |                   0 | 복잡한 호출에서 높음      | 에뮬레이션된 C 런타임 |
| **HQQ 공유 메모리 IPC**            | 예              | 프로세스 경계      |  0 (적응형 스핀)    |  0 (`StandardLink`)       | 정확한 상류 C         |

## 아키텍처 개요: 프로세스 경계를 넘나드는 네 개의 계층

HQQ는 공유 메모리 링 메커니즘, 타입화된 RPC 코드 생성, 바이너리 무결성 검증, 자식 프로세스 감독이 서로 독립적으로 유지되도록 프로세스 격리를 조합 가능한 네 개의 계층으로 분리합니다.

```text
+-------------------------------------------------------------------+
| Go Application Process (CGO_ENABLED=0)                            |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Generated Go Client   |     | Process Supervisor            |  |
|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|  +-----------v-----------+                     | Anonymous Pipe   |
|  | Transport Tier        |                     | Lifeline         |
|  | (StandardLink /       |                     | (--parent-stdin) |
|  |  Advanced MPMC)       |                     |                  |
+--------------+---------------------------------+------------------+
               |                                 |
===============|========= Shared Memory =========|===================
               |     (mmap / CreateFileMapping)  |
+--------------v---------------------------------v------------------+
| C11 Worker Process (Isolated Address Space)                       |
|                                                                   |
|  +-----------------------+     +-------------------------------+  |
|  | Generated C11 Server  |---->| Generation-Checked Handle     |  |
|  | & Batch Shims         |     | Table (hqq_rpc.c)             |  |
|  +-----------+-----------+     +---------------+---------------+  |
|              |                                 |                  |
|              +----------------+----------------+                  |
|                               |                                   |
|                     +---------v---------+                         |
|                     | Native C Library  |                         |
|                     | (SQLite, Zstd...) |                         |
|                     +-------------------+                         |
+-------------------------------------------------------------------+
```

### 1. 공유 메모리 전송 (`gosuda.org/hqq` 및 `c/`)

기본 패키지 `gosuda.org/hqq`와 그 C11 대응 구현인 `c/`는 POSIX `mmap`(Linux 및 macOS)과 Win32 `CreateFileMappingW`(Windows)를 기반으로 두 가지 공유 메모리 전송 모드를 구현합니다. 모든 공유 세그먼트는 링 인덱스를 읽기 전에 프로토콜 버전, 버퍼 형상, 엔드포인트 PID를 고정하는 검증된 `HQQ_STL3` 슈퍼블록으로 시작합니다.

모든 핫 제어 워드와 슬롯 헤더는 64바이트 캐시 라인 경계에 정렬되어, 생산자와 소비자 코어가 거짓 공유(false sharing)를 통해 동일한 캐시 라인에서 경합하지 않습니다.

```text
StandardLink Slot Layout (64-Byte Cache-Line Aligned)
Byte Offset:  0x00                0x08                0x10                          0x40
              +-------------------+-------------------+-----------------------------+
Header Line:  | seq (u64 atomic)  | len (u32) | flags | reserved (zero-padded)      |
              +-------------------+-------------------+-----------------------------+
Payload Area: | payload bytes [0 .. BufferSize - 1] (directly encoded / decoded)    |
              +---------------------------------------------------------------------+
```

| 필드      | 오프셋 |         크기 | 의미                                                                                                                       |
| :-------- | -----: | -----------: | :------------------------------------------------------------------------------------------------------------------------- |
| `seq`     | `0x00` |          8 B | 단조 증가 시퀀스 카운터. 홀수 값은 진행 중인 쓰기를 나타내고, 짝수 값은 store-release를 통해 커밋된 프레임을 게시합니다. |
| `len`     | `0x08` |          4 B | 슬롯에 기록된 페이로드 바이트 길이(`0 <= len <= BufferSize`).                                                              |
| `flags`   | `0x0C` |          4 B | 프레임 제어 비트(연속, 오류 상태, 또는 종료 알림).                                                                         |
| `payload` | `0x40` | `BufferSize` | 중간 복사 없이 제자리에서 읽고 쓰는 연속 페이로드 영역.                                                                    |

호출자는 동시성 구조에 따라 두 가지 전송 계층 중 하나를 선택합니다.

- **`StandardLink` (1:1 SPSC 세션)**: 시퀀스 락(`SeqLock`)으로 보호되는 전용 요청 및 응답 슬롯을 통해 클라이언트와 서버 세션을 짝지습니다. 고속 경로에서 쓰기 측은 매핑된 슬롯에 직접 직렬화하고 원자적 store-release로 커밋합니다. 읽기 측은 OS 대기 프리미티브(Linux의 `futex`, macOS의 `__ulock_wait`, Windows의 `WaitOnAddress`)로 되돌아가기 전에 사용자 공간에서 짧게 스핀합니다. 단일 프레임 왕복은 `0 allocs/op`로 약 200 ns에 완료됩니다.
- **`Advanced` (MPMC 디스패치 링)**: 락-프리 다중 생산자 다중 소비자 제출 링, 공유 슬롯 풀, 인덱스 방식 완료 테이블을 사용하여 `M`개의 동시 Go 호출자를 `N`개의 C 워커 스레드에 다중화합니다. 호출자는 슬롯을 예약하고, 슬롯 인덱스를 링에 게시한 뒤, 완료 항목에서 대기하며, 워커 스레드는 요청을 비순차적으로 가져와 완료합니다.

두 계층 모두 피어 생존 상태를 지속적으로 추적합니다. 각 엔드포인트는 `Heartbeat()`를 통해 원자적 단조 타임스탬프를 갱신하고, 대기 중에는 `PeerAlive(timeout)`을 확인합니다. 워커 프로세스가 호출 도중 충돌하면 피어는 오래된 하트비트나 종료된 PID를 즉시 감지하고, 차단된 대기를 `ErrPeerGone`으로 중단시키며, 공유 메모리 파일을 남기지 않고 원자적 세션 `Takeover`를 지원합니다.

### 2. 제로 할당 RPC 및 세대 검사 핸들 (`gosuda.org/hqq/bindgen`)

수십 개의 함수를 가진 C 라이브러리를 위해 공유 메모리 프레이밍을 손으로 작성하는 것은 지루하고 오류가 발생하기 쉽습니다. `hqq-bindgen`은 Interface Definition Language(`.idl`) 파일을 읽거나(또는 `hqq-bindgen init`을 통해 C `.h` 헤더로부터 직접 부트스트랩하여) zero-cgo Go 클라이언트 패키지와 C11 서버 스켈레톤을 함께 생성하는 스키마 컴파일러입니다.

모든 C 바인딩에서 가장 위험한 실수는 원시 C 포인터(`sqlite3*`, `sqlite3_stmt*`, `ZSTD_CCtx*`)를 언어 경계 너머로 노출하는 것입니다. 호출자가 해제한 뒤 포인터를 전달하거나, 다른 세션에 속한 포인터를 전달하거나, 정수 주소를 손상시키면 C 라이브러리는 잘못된 메모리를 역참조하게 됩니다.

`hqq-bindgen`은 와이어 상에서 원시 포인터를 금지합니다. 대신 IDL은 상태를 가진 C 리소스를 타입이 지정된 `handle` 선언으로 모델링합니다.

```text
service sqlite3 {
    handle Db;
    handle Stmt @owned_by(Db);

    fn open_v2(path: string, flags: i32) -> result<Db, i32>;
    fn prepare_one(db: Db, sql: string) -> result<Stmt, i32>;
    fn finalize(stmt: @consume Stmt) -> result<void, i32>;
    fn close(db: @consume Db) -> result<void, i32>;
}
```

와이어 상에서 모든 핸들은 32비트 테이블 인덱스와 32비트 0이 아닌 세대 카운터를 묶은 64비트 정수입니다: `(uint64_t)generation << 32 | index`. C 런타임(`c/src/hqq_rpc.c`)은 세 가지 불변 조건을 유지하는 세션별 핸들 테이블을 관리합니다.

1. **세대 검증**: 모든 조회는 슬롯이 점유되어 있는지, 저장된 세대가 핸들의 상위 32비트와 일치하는지, 그리고 핸들 타입 태그가 매개변수 타입(`Db` 대 `Stmt`)과 일치하는지 확인합니다. 닫힌 핸들을 재사용하면 C 라이브러리를 호출하기 전에 `HQQRPC_E_BAD_HANDLE`로 즉시 실패합니다.
2. **계층적 소유권(`@owned_by`)**: `Stmt`에 `@owned_by(Db)`가 주석으로 달려 있으므로, `Db` 핸들을 닫으면 해당 `Db`가 소유한 살아 있는 모든 자식 `Stmt` 핸들을 자동으로 순회하고, 할당의 역순으로 소멸자를 실행합니다(`sqlite3_close_v2` 이전에 `sqlite3_finalize`).
3. **세션 종료 스윕**: Go 클라이언트가 핸들을 닫지 않은 채 연결을 끊거나 충돌하면, C 서버의 세션 소멸자가 세션 핸들 테이블 전체를 역순으로 스윕하여 클라이언트 재시작 간에 C 할당이 누수되지 않음을 보장합니다.

세션이 첫 번째 RPC 호출을 처리하기 전에, 클라이언트와 서버는 정규화된 IDL(`HQQ-BINDGEN-IDL-V1\n` 접두사 포함)에 대해 계산된 32바이트 SHA-256 인터페이스 다이제스트를 교환합니다. Go 클라이언트와 C 워커가 호환되지 않는 IDL 스키마로부터 컴파일되었다면, 핸드셰이크는 부트스트랩 단계에서 연결을 거부합니다.

### 3. 변조 방지 바이너리 실행 (`gosuda.org/hqq/secureexec`)

C 코드를 워커 바이너리로 옮기면 공급망 문제가 제기됩니다. Go 프로세스는 검증과 실행 사이에 디스크상의 워커 바이너리가 수정되거나 교체되지 않았음을 어떻게 알 수 있을까요?

파일 경로에 대해 `crypto/sha256`을 호출한 뒤 `os/exec.Command(path).Start()`를 호출하면, 커널이 파일을 실행하기 전에 다른 프로세스가 파일을 덮어쓰거나 교체할 수 있는 검사 시점과 사용 시점 간(TOCTOU) 간극이 남습니다. `gosuda.org/hqq/secureexec`는 이 간극을 차단합니다.

- Linux에서 `secureexec`는 바이너리를 열고, SHA-256 해시를 계산하면서 그 내용을 `memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING)`으로 생성된 익명 메모리 파일 디스크립터에 스트리밍합니다. 해시를 고정된 32바이트 다이제스트 또는 Ed25519 서명된 릴리스 매니페스트(`cmd/hqq-manifest`)와 대조하여 검증하고, `fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)`를 적용하여 시스템의 어떤 프로세스도 페이지를 변경할 수 없도록 한 뒤, 봉인된 디스크립터로부터 자식을 생성합니다.
- macOS와 Windows에서 `secureexec`는 검증된 바이너리를 SHA-256 다이제스트를 키로 하는 권한 잠금 캐시 디렉터리에 구체화하고, 실행 전에 파일 크기와 다이제스트 무결성을 검증합니다.

### 4. 부모-생명선 프로세스 감독 (`gosuda.org/hqq/launch`)

Go 애플리케이션이 백그라운드 C 워커 프로세스를 생성할 때, Go 부모가 `SIGKILL`(`kill -9`)로 종료되어 `defer` 정리 핸들러를 실행할 수 없더라도 워커는 고아 프로세스로 부모보다 오래 살아남아서는 안 됩니다.

`gosuda.org/hqq/launch`는 `--parent-stdin` 플래그와 함께 워커의 표준 입력으로 전달되는 익명 OS 파이프를 통해 수명 결합을 강제합니다. Go 부모는 파이프의 쓰기 끝을 보유하며 절대 그곳에 쓰지 않고, C 워커는 전용 생명선 스레드에서 읽기 끝을 감시합니다. Go 프로세스가 정상적으로 종료하거나, 패닉을 일으키거나, OS 커널에 의해 종료(`SIGKILL` 또는 OOM 킬러)되면, 커널은 즉시 쓰기 디스크립터를 닫습니다. C 워커는 마이크로초 이내에 파이프에서 `EOF`를 수신하고, 활성 호출을 중단하며, 핸들 소멸자를 실행하고, 공유 메모리 파일의 링크를 해제한 뒤 깔끔하게 종료합니다.

## 빠른 시작: 생성된 서비스 호출하기

`hqq-bindgen`과 `launch`를 사용하면, 격리된 C 워커를 Go에서 호출하는 작업은 `CGO_ENABLED=0`을 유지하면서도 일반적인 타입화된 Go 클라이언트처럼 보입니다.

```go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"gosuda.org/hqq/bindgen/rt"
	"gosuda.org/hqq/launch"
	"gosuda.org/hqq/secureexec"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	hash, err := secureexec.ParseHash("7649b1b45dacd934cde8149f35f4fcc3937f6486fbac23ce976466719b3752bb")
	if err != nil {
		log.Fatal(err)
	}

	listener, err := launch.NewListenerName("demo")
	if err != nil {
		log.Fatal(err)
	}

	srv, err := launch.Start(ctx, launch.Config{
		Binary:   "/path/to/verified-worker",
		SHA256:   hash,
		Listener: listener,
		Args:     []string{listener},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer srv.Close()

	fmt.Printf("worker running on shared-memory listener %s (opts=%+v)\n", listener, rt.SessionOptions{})
}
```

## 저장소 구조

`gosuda.org/hqq`의 모든 서브시스템은 명확한 경계를 가진 집중된 패키지로 구성되어 있습니다.

```text
gosuda.org/hqq
├── standard.go / ring.go      # StandardLink (SPSC) 및 저수준 공유 메모리 링 프리미티브
├── advanced.go / slotpool.go  # Advanced (MPMC) 디스패처, 슬롯 풀, 완료 테이블
├── shm_*.go / wait_*.go       # OS별 mmap/CreateFileMapping 및 futex/ulock/WaitOnAddress
├── c/                         # C11 전송 라이브러리(hqq.c) 및 RPC 서버 런타임(hqq_rpc.c)
├── bindgen/                   # hqq-bindgen IDL 파서, 검증기(V1-V41), Go/C11 코드 생성기
│   └── rt/                    # 생성된 서비스를 위한 제로 할당 Go 클라이언트 런타임
├── secureexec/                # SHA-256 / Ed25519 매니페스트 검증 및 memfd 봉인 실행
├── launch/                    # --parent-stdin 생명선 관리를 갖춘 워커 프로세스 감독기
├── cmd/hqq-manifest/          # 릴리스 바이너리 매니페스트 생성 및 서명용 CLI 도구
└── spec/                      # 실행 가능한 와이어 명세 및 골든 테스트 벡터(BINDGEN.md)
```

HQQ가 Go 생태계에서 가장 널리 사용되는 두 `cgo` 라이브러리를 위한 즉시 적용 가능한 프로덕션 대체재를 어떻게 구동하는지 보려면, 이 스택 위에 구축된 동반 패키지를 확인하십시오.

- **[`gosuda.org/sqlite3`](https://gosuda.org/sqlite3)**: `github.com/mattn/go-sqlite3`와 호환되는 드롭인 `database/sql` 드라이버로, 파이프라인 방식 배치 프리페칭과 함께 공유 메모리를 통해 네이티브 C SQLite `3.54.1`을 실행합니다.
- **[`gosuda.org/zstd`](https://gosuda.org/zstd)**: `github.com/DataDog/zstd`의 드롭인 대체재로, 제로 할당 컨텍스트와 대량 배칭을 갖춘 격리된 공유 메모리 워커에서 네이티브 C `libzstd 1.5.7`을 실행합니다.
- **GitHub 저장소**: [https://github.com/gosuda/HQQ](https://github.com/gosuda/HQQ)
