Skip to content

Crux 架构文档

本文档面向希望深入理解 Crux 内部设计的贡献者。阅读前建议先熟悉 核心概念,这里不再复述概念定义,而是聚焦设计决策、接口边界、数据流和扩展路径

如需查询具体 API 签名和参数细节,请参阅 参考手册。 如需实战操作指引,请参阅 教程


目录

  1. 微内核设计
  2. 进程模型
  3. 驱动层
  4. 上下文管理

1. 微内核设计

1.1 设计哲学

Crux 内核采用接口组合模式——将系统调用按功能分类为独立的子接口,由一个统一的 KernelImpl 结构体组合实现。这个设计选择源于 OS 隐喻与 Go 语言特性的交汇:

  • Unix 微内核隐喻:传统微内核将进程管理、文件系统、IPC 分离为独立的服务器。Crux 在单进程内以接口边界模拟这种分离,每个子接口各自承担一个功能域的职责。
  • Go 接口组合的天然适配:Go 的小接口 + 组合优于大接口的哲学恰好匹配——每个子接口只定义 2~5 个方法,职责清晰、可独立测试、可独立演进。

决策记录:接口组合模式 vs 单一大接口 vs 函数集合——选择接口组合的核心理由是扩展性:新增一类 syscall 只需定义新接口并在 KernelImpl 上实现,不影响已有子接口的编译和测试。

1.2 KernelImpl 与子接口

KernelImpl 是内核的核心实现结构体,定义于 kernel/kernel.go

go
type KernelImpl struct {
    procTable   *xsync.SyncMap[types.PID, *Process]
    vfs         *vfs.VFS
    ctxMgr      *cruxctx.Manager
    callbacks   KernelCallbacks
    reapCh      chan types.PID
    stopCh      chan struct{}
    reaperWg    sync.WaitGroup
    shutdownOnce sync.Once
    msgQueues   *xsync.SyncMap[types.PID, *MessageQueue]
    msgSeq      atomic.Uint64
    procGroups  *xsync.SyncMap[types.PGID, *ProcGroup]
    mountMgr    MountManager
}

它通过方法集合隐式实现以下 6 个分类子接口:

子接口方法职责
ProcessManagerSpawn(intent, agent, opts) (PID, error)进程创建(分配 PID、上下文、FD,启动推理 goroutine)
Kill(pid, signal) error向进程发送信号(SIGTERM/SIGKILL/SIGPAUSE/SIGRESUME)
Wait(pid) (ExitStatus, error)阻塞等待进程结束并触发 reapProcess 资源释放
MountManagerMount(path, config) error挂载 MCP 服务器到 /mnt/mcp/
Unmount(path) error卸载 MCP 服务器
UnmountAll() error卸载所有 MCP(Shutdown 时调用)
IPCManagerSend(senderPID, targetPID, data) error向目标进程发送消息
Recv(pid) (*Message, error)阻塞接收消息
Pipe(writerPID, readerPID) (writeFD, readFD, error)创建进程间管道
SignalManagerSignal(pid, sig) error投递信号(含自定义 handler 分发)
SigBlock(pid, sig) error阻塞信号
SigUnblock(pid, sig) error解除信号阻塞并投递 pending
ProcGroupManagerJoinGroup(pid, groupID) error加入进程组
LeaveGroup(pid, groupID) error离开进程组
GetProcGroup(groupID) ([]PID, error)查询进程组成员列表
SignalGroup(groupID, signal) error向进程组广播信号
SupervisorManagerSpawnSupervisor(spec) (PID, error)创建 Supervisor 树节点

编译时接口合规检查确保 KernelImpl 满足 ProcessManager 约束:

go
var _ ProcessManager = (*KernelImpl)(nil)

1.3 KernelCallbacks 回调机制

KernelCallbacks 是内核到 CLI/UI 层的通知通道,解耦内核与展示逻辑:

go
type KernelCallbacks interface {
    OnSpawn(pid types.PID, intent string)
    OnStep(pid types.PID, step int, total int)
    OnComplete(pid types.PID, result string, exit ExitStatus)
    OnError(pid types.PID, err error)
}
回调触发时机用途
OnSpawn进程注册到进程表后CLI 显示 [kernel] spawning PID N...
OnStep每次 reasonStep 循环开头CLI 显示 [agent] step X/N
OnCompletefinishProcess 写入 ExitStatus 后CLI 显示最终结果和退出码
OnErrorfinishProcess 中 exit.Err 非 nilCLI 显示错误信息

传入 nil 可关闭回调(静默模式),适用于测试和嵌入式集成。

1.4 数据流:从 Spawn 到完成

一次完整的智能体执行遵循以下数据流:

CLI 层                    内核层                   VFS/驱动层
  │                        │                         │
  │  Spawn(intent, agent)  │                         │
  ├───────────────────────>│                         │
  │                        │  CtxAlloc(64)           │
  │                        ├────────────────────────>│ context.Manager
  │                        │                         │
  │                        │  SetSystemPrompt(cid)   │
  │                        ├────────────────────────>│ context.Manager
  │                        │                         │
  │                        │  Open(/dev/llm/claude)  │
  │                        ├────────────────────────>│ vfs.DeviceRegistry
  │                        │                         │
  │                        │  [启动推理 goroutine]    │
  │                        │                         │
  │     OnSpawn(pid)       │                         │
  │<───────────────────────│                         │
  │                        │                         │
  │                     ┌──┤ reasonStep 循环         │
  │     OnStep(1/10)    │  │                         │
  │<────────────────────┤  │  BuildPrompt(cid)       │
  │                     │  ├────────────────────────>│ context.Manager
  │                     │  │                         │
  │                     │  │  Write(llmFD, req)      │
  │                     │  ├────────────────────────>│ /dev/llm/claude
  │                     │  │                         │
  │                     │  │  Read(llmFD)            │
  │                     │  ├────────────────────────>│ /dev/llm/claude
  │                     │  │                         │
  │                     │  │  [解析 action]           │
  │                     │  │                         │
  │                     │  │  如果 tool_call:         │
  │                     │  │  Open/Write/Read/Close   │
  │                     │  ├────────────────────────>│ /dev/fs, /dev/shell, ...
  │                     │  │                         │
  │                     │  │  AppendToolResult(cid)   │
  │                     │  ├────────────────────────>│ context.Manager
  │                     │  │                         │
  │                     └──┤ 如果 text → 完成        │
  │                        │                         │
  │                        │  finishProcess(exit)     │
  │   OnComplete(result)   │                         │
  │<───────────────────────│                         │
  │                        │                         │
  │  Wait(pid) / Reap      │                         │
  ├───────────────────────>│  reapProcess 序列       │
  │                        ├────────────────────────>│ CtxFree, CloseAll, ...

1.5 扩展路径

添加新 syscall:

  1. kernel/kernel.go 中定义新接口(如 type FooManager interface { ... }
  2. KernelImpl 上实现方法
  3. 添加编译时检查:var _ FooManager = (*KernelImpl)(nil)
  4. debug.NewEvent 中注册新的 syscall 名称
  5. 添加 IPC method(ipc/protocol.go)使 CLI 可调用

添加新设备驱动:

  1. 实现 vfs.VFSFileFactory 函数
  2. cmd/crux/main.go 的初始化代码中调用 devRegistry.Register(path, factory)
  3. VFS 自动处理 Open/Read/Write/Close 路由

2. 进程模型

2.1 Process 结构体设计

Process 定义于 kernel/process.go,是 Crux 进程的完整运行时表示。字段按功能分组:

身份与状态(不可变 / mu 保护):

字段类型说明
PIDtypes.PID全局唯一,创建后不可变
PPIDtypes.PID父进程 PID,孤儿进程被 reparent 时可修改
Statetypes.ProcessState状态机当前状态,mu 保护
Intentstring创建时的意图描述,不可变
Skills[]string加载的 Skill 名称列表
Children[]types.PID子进程 PID 列表
CreatedAttime.Time进程创建时间(用于 elapsed 和 astrace 时间戳)
Exit*ExitStatusZombie/Dead 时非 nil,记录退出状态

资源与通道:

字段类型说明
FDTablemap[types.FD]vfs.VFSFile文件描述符表(VFS 内部管理实际状态)
DebugChanchan types.SyscallEvent缓冲 256,astrace 追踪通道
LogChanchan types.LogEntry缓冲 256,推理日志通道
Donechan ExitStatus缓冲 1,进程退出信号
CtxIDtypes.CtxID关联的上下文空间 ID

推理状态:

字段类型说明
Resultstring最终推理输出
TokensUsedint累计 token 消耗
ContextBudgetinttoken 预算(0 = 无限制)
AllowedDevices[]string设备白名单(nil = 全部允许)
MCPMounts[]string自动挂载的 MCP 路径

并发子系统(均由 mu 保护):

字段类型说明
groups[]types.PGID进程组成员关系
sigHandlersmap[Signal]SignalHandler自定义信号处理器
blockedSignalsmap[Signal]struct{}被阻塞的信号集
pendingSignalsmap[Signal]struct{}待投递的信号集
resumeChchan struct{}SIGPAUSE/SIGRESUME 协调
threadsmap[TID]*Thread线程表
coroutinesmap[CoID]*Coroutine协程表

同步原语:

字段类型说明
musync.Mutex保护所有可变状态
cancelcontext.CancelFunc取消推理 goroutine
ctxcontext.Context推理 goroutine 的上下文
wgsync.WaitGroup等待推理 goroutine 完成
reapOncesync.Once确保 reapProcess 只执行一次

2.2 状态机

进程状态严格遵循单向转移规则,不允许回退:

Created ──Start()──→ Running ──Terminate()──→ Zombie ──Reap()──→ Dead
转移方法触发条件
Created → RunningStart()推理 goroutine 启动
Running → ZombieTerminate(exit)推理完成/出错/超时/Kill/预算超限
Zombie → DeadReap()Wait 调用或自动 reaper 清理

状态转移逻辑使用 validTransitions 表驱动:

go
var validTransitions = map[types.ProcessState][]types.ProcessState{
    types.StateCreated: {types.StateRunning},
    types.StateRunning: {types.StateZombie},
    types.StateZombie:  {types.StateDead},
}

非法转移返回 *SyscallErrorErrInternal)。transitionLocked 在持有 mu 的情况下检查转移合法性,确保并发安全。

2.3 PID 分配策略

PID 使用包级 atomic.Uint64 全局递增分配:

go
var pidCounter atomic.Uint64

func nextPID() types.PID {
    return types.PID(pidCounter.Add(1))
}

设计决策:

  • 不回收:PID 单调递增,永不复用。这简化了进程引用的生命周期管理——持有旧 PID 的引用不会意外指向新进程。
  • 从 1 开始:PID 0 保留给"内核/init"虚拟进程,CLI 直接 Spawn 的顶层进程 PPID 为 0。
  • 原子操作:无需加锁,多个并发 Spawn 安全。

2.4 goroutine 生命周期管理

每个进程拥有一个专属推理 goroutine,在 Spawn 中启动:

go
proc.wg.Add(1)
go func() {
    defer proc.wg.Done()
    defer func() { _ = k.vfs.CloseAll(proc.PID) }()
    _ = proc.Start()    // Created → Running
    k.reasonStep(proc, llmFD, opts)
}()

关键约束:

  1. wg 追踪wg.Add(1) 在 goroutine 启动前调用,wg.Done() 通过 defer 确保执行。reapProcess 中 wg.Wait() 等待 goroutine 退出。
  2. context.Cancel 取消:Kill(SIGKILL) 调用 proc.Cancel(),reasonStep 循环在每个 step 开头检查 proc.ctx.Done()
  3. defer CloseAll:goroutine 退出前关闭所有打开的 VFS 文件描述符。
  4. SIGPAUSE/SIGRESUME:reasonStep 在每个 step 开头调用 proc.WaitIfPaused(),如果 resumeCh 非 nil 则阻塞,直到 Resume 关闭 channel。

2.5 资源释放顺序

reapProcess(定义于 kernel/reap.go)执行严格的资源释放序列。通过 reapOnce 确保幂等——Wait 和自动 reaper 可能并发调用,只有第一个执行:

步骤操作目的
1handleOrphanChildren(proc)Running 子进程 reparent 到 PID 0,Zombie 子进程推入 reapCh
2proc.Cancel()取消 context,通知推理 goroutine 停止
3proc.wg.Wait()等待推理 goroutine 退出(其 defer 会调用 CloseAll)
4close(DebugChan), close(LogChan)先 nil 化再 close,防止与 emitEvent 竞态
5msgQueue.close()关闭消息队列,解除 Recv 阻塞
6removeFromAllGroups清理进程组成员关系
7ClearSignalState()清理信号 handler/blocked/pending/resumeCh
8ClearThreads()取消所有线程并等待完成
9ClearCoroutines()清理协程(关闭 resumeCh,排空 yieldCh)
10CtxFree(CtxID)释放上下文空间
11proc.Reap()Zombie → Dead 状态转移
12RemoveProcess(pid)从进程表移除

步骤顺序至关重要:必须先处理孤儿子进程(步骤 1),再停止 goroutine(步骤 2-3),再关闭通道(步骤 4-5),最后释放资源(步骤 10-12)。

2.6 三级并发模型

Crux 提供三种粒度的并发原语,映射到不同的使用场景:

级别原语调度模型资源隔离适用场景
ProcessSpawn抢占式(独立 goroutine + context)独立 PID、独立 CtxID、独立 FD 表独立任务
ThreadSpawnThread抢占式(独立 goroutine,共享父 ctx)共享父进程上下文并行子任务
CoroutineSpawnCoroutine协作式(yield/resume)共享父进程上下文流式处理、状态机

Thread 结构体包含 TID、ParentPID、Intent、State、Done、Result、Err 以及内部同步字段(mu、cancel、ctx),通过 context.WithCancel(parentCtx) 派生子 context,父进程被 Kill 时子 Thread 的 context 也被取消。

Coroutine 使用 yieldCh / resumeCh 通道对实现协作式让出和恢复,支持值传递。ClearCoroutines 在进程回收时需要处理两种阻塞情况:阻塞在 yieldCh <- value(通过 drain goroutine 排空)和阻塞在 <-resumeCh(通过关闭 channel 解除)。

2.7 进程组与信号系统

进程组允许将多个进程逻辑分组,通过 SignalGroup 向组内所有成员广播信号,适用于 Compose 编排中的批量控制。

信号系统支持 5 种信号:

信号可阻塞可自定义 handler默认行为
SIGTERMCancel context
SIGKILL强制 Cancel
SIGINTCancel context
SIGPAUSE暂停推理循环
SIGRESUME恢复推理循环

信号投递使用 resolveSignalDisposition 在单次锁持有中原子确定分发路径(blocked → pending / handler / default),避免 TOCTOU 竞态。


3. 驱动层

3.1 VFS 设备注册机制

VFS(虚拟文件系统)是 Crux 的资源抽象层。所有外部资源——LLM、文件系统、Shell、MCP 工具——统一表现为可 Open/Read/Write/Close 的"文件"。

核心抽象(vfs/vfs.go):

go
type VFSFile interface {
    Read(length int) ([]byte, error)
    Write(ctx context.Context, data []byte) error
    Close() error
    Stat() (FileStat, error)
}

type VFSFileFactory func(subpath string, flags OpenFlag) (VFSFile, error)

设备注册表(vfs/dev.go):

DeviceRegistry 使用 xsync.Registry(基于 sync.Map 的带注册/反注册语义的注册表)管理路径到工厂的映射:

go
type DeviceRegistry struct {
    registry *xsync.Registry[VFSFileFactory]
}
  • Register(path, factory):注册设备(路径唯一,重复注册报错)
  • Unregister(path):反注册设备(MCP Unmount 时使用)
  • Open(path, flags):先精确匹配,再最长前缀匹配(/dev/llm/claude 匹配 /dev/llm/claude/subpath

路径解析策略:

  1. 精确匹配:Open("/dev/fs", ...) → factory("", flags)
  2. 最长前缀匹配:Open("/dev/fs/src/main.go", ...) → factory("/src/main.go", flags)

这使设备驱动可以处理子路径——例如 /dev/fs 驱动通过 subpath 访问宿主文件系统中的任意文件。

FD 表:

每个进程在 VFS 层拥有独立的 FD 表(fdTable),FD 从 3 开始分配(0/1/2 保留给 stdin/stdout/stderr 的语义对齐)。FD 表由 VFS 管理,不是 Process 结构体直接持有——Process.FDTable 仅用于跟踪 FD 是否存在。

3.2 已注册设备

系统启动时在 cmd/crux/main.go 中注册以下设备:

设备路径驱动包描述
/dev/llm/claudedrivers/llmLLM 调用(Claude Code CLI)
/dev/fsdrivers/fs宿主文件系统只读访问
/dev/shelldrivers/shellShell 命令执行
/procvfs.ProcFS动态进程信息(/proc/{pid}/status, intent, context
/mnt/mcp/{pid}-{server}动态注册MCP 工具(Spawn 时自动挂载)

3.3 LLMDriver 接口

LLMDriver 定义于 drivers/llm/driver.go,是 LLM 能力的抽象:

go
type LLMDriver interface {
    Call(ctx context.Context, req LLMRequest) (*LLMResponse, error)
    Stream(ctx context.Context, req LLMRequest) (<-chan StreamEvent, error)
    Info() DriverInfo
}

LLMRequest:

字段类型说明
Intentstring用户意图
SystemPromptstring系统提示词
Modelstring模型标识(空 = 驱动默认)
MaxTurnsint最大交互轮次
TimeoutMsint64超时毫秒数

LLMResponse:

字段类型说明
ContentstringLLM 输出内容
TokensUsedint本次消耗 token 数

当前实现为 Claude Code CLI 驱动。添加新 LLM 驱动只需实现 LLMDriver 接口并注册到 VFS。内核 reasonStep 通过 VFS Read/Write 与 LLM 交互,不直接依赖具体驱动实现。

3.4 MCP 挂载机制

MCP(Model Context Protocol)集成通过动态挂载实现,使 MCP 工具以 VFS 路径暴露给智能体。

MCPTransport 接口(vfs/mcp.go):

go
type MCPTransport interface {
    Connect(ctx context.Context) error
    Call(ctx context.Context, method string, params json.RawMessage) (json.RawMessage, error)
    Close() error
    Ping(ctx context.Context) error
}

type TransportFactory func(config MCPConfig) (MCPTransport, error)

接口定义在 vfs 包中(而非 drivers/mcp),这是依赖反转的设计——vfs 定义接口,drivers/mcp 提供实现,避免 vfs → drivers 的反向依赖。

MountManager(接口定义于 kernel/kernel.go,实现于 vfs/mount.govfs.MountManager 结构体):

Mount 流程:

  1. TransportFactory(config) → 创建 transport
  2. transport.Connect(ctx) → 建立连接(500ms 超时,NFR25)
  3. mcpFileFactory(transport) → 创建 VFSFileFactory
  4. devReg.Register(path, factory) → 注册到设备注册表
  5. 存储 mount 记录

Unmount 流程:

  1. 从 mounts 表移除
  2. transport.Close() → 关闭连接
  3. devReg.Unregister(path) → 从设备注册表移除

VFS 子路径映射:

挂载点下的子路径映射到 MCP 协议操作:

VFS 路径MCP 操作Read 行为Write 行为
/mnt/mcp/{mount}/返回 ["tools","resources"]
/mnt/mcp/{mount}/toolstools/list返回工具列表
/mnt/mcp/{mount}/tools/{name}tools/call返回上次调用结果发起工具调用
/mnt/mcp/{mount}/resourcesresources/list返回资源列表
/mnt/mcp/{mount}/resources/{uri}resources/read读取资源内容

3.5 Agent 自动挂载生命周期

Agent 的 agent.yaml 可声明 MCP 依赖。Spawn 时自动处理:

  1. 挂载:遍历 agent.MCPConfigs,为每个 MCP 服务器执行 Mount("/mnt/mcp/{pid}-{serverName}", config)
  2. 白名单注入:挂载路径自动添加到 proc.AllowedDevices
  3. 失败回滚:任一 MCP 挂载失败则回滚已挂载的路径,释放上下文,返回错误
  4. 自动卸载finishProcess 在终止进程前逐个调用 Unmount,卸载失败不阻塞进程退出

4. 上下文管理

4.1 Context 结构体

Context 定义于 context/context.go,表示一个独立的对话空间:

go
type Context struct {
    ID           types.CtxID
    SystemPrompt string
    Messages     []Message
    MaxSize      int
    mu           sync.RWMutex
}

type Message struct {
    Role       Role   `json:"role"`
    Content    string `json:"content"`
    ToolCallID string `json:"tool_call_id,omitempty"`
}

Role 枚举:systemuserassistanttool

MaxSize 限制 Messages 切片长度(消息数量)。当前 MVP 不限制单条消息的字节大小。

4.2 Manager 方法

Manager 管理上下文的完整生命周期,方法分为三类:

分配与释放:

方法签名说明
CtxAlloc(size int) (CtxID, error)分配上下文,size 为消息容量
CtxFree(cid CtxID) error释放上下文(reapProcess 步骤 10)

内容操作:

方法签名说明
SetSystemPrompt(cid, prompt) error设置/更新系统提示词
AppendMessage(cid, role, content) error追加对话消息
AppendToolResult(cid, toolCallID, content) error追加工具执行结果
CtxWrite(cid, offset, data) error低级写入(offset=0 追加,>0 覆盖)
CtxRead(cid, offset, length) ([]byte, error)低级读取(JSON 序列化)

查询:

方法签名说明
BuildPrompt(cid) (*PromptResult, error)组装完整 LLM prompt
GetContextSummary(ctxID) (string, error)/proc/{pid}/context 的摘要

4.3 Prompt 组装流程

BuildPrompt 返回 PromptResult,包含 SystemPromptMessages 两个字段。reasonStep 将其组装为 LLM 请求:

  1. 系统提示词构建(Spawn 阶段):

    • Agent.SystemPrompt() = instructions.md 内容 + 所有激活 Skill 的 body 注入
    • 如果 SpawnOpts 也提供了 SystemPrompt,拼接:opts.SystemPrompt + "\n\n" + agentPrompt
  2. 消息历史累积(reasonStep 循环):

    • 初始:AppendMessage(user, intent)
    • 每轮 LLM 响应:AppendMessage(assistant, resp.Content)
    • 工具调用结果:AppendToolResult(toolPath, result)
  3. 发送给 LLM

    • BuildPrompt(cid) → PromptResult(SystemPrompt + Messages 快照)
    • 序列化为 llmRequest{Intent, SystemPrompt, Model, Messages}
    • 写入 LLM VFS 设备

4.4 Token 预算管理

Token 预算防止单个进程过度消耗 LLM 资源。

预算来源优先级(从高到低):

  1. SpawnOpts.ContextBudget(CLI --budget 或 Compose 配置)
  2. AgentManifest.ContextBudget(agent.yaml 中的配置)
  3. 0(无限制)

负数预算在 Spawn 时规范化为 0。

执行逻辑(在 reasonStep 循环中):

每次 LLM Read 返回后:
    proc.TokensUsed += resp.TokensUsed

    if budget > 0 && TokensUsed >= ContextBudget:
        emitLog("Token budget exceeded: N/M")
        emitEvent(action: "budget_exceeded")
        finishProcess(ExitStatus{Code: 2, Reason: "budget_exceeded"})
        return

退出码约定:

  • 0 — 正常完成
  • 1 — 错误(LLM 失败、工具失败、超时等)
  • 2 — 预算超限(budget_exceeded

4.5 上下文与进程的生命周期绑定

上下文的生命周期严格绑定到拥有它的进程:

进程事件上下文操作
Spawn 开始CtxAlloc(64) 分配上下文
Spawn 失败(MCP 挂载出错等)CtxFree(cid) 立即释放
reasonStep 循环持续 AppendMessage / BuildPrompt
reapProcess 步骤 10CtxFree(cid) 最终释放

Thread 和 Coroutine 共享父进程的上下文(通过 CtxID),不独立分配。这意味着并发线程对同一上下文的 AppendMessage 调用由 Context.mu 序列化,保证消息顺序一致性。


延伸阅读

Released under the MIT License.