Skip to content

从 guest 到 host:请求怎么出来的

在 CubeSandbox 的架构中,沙箱内部(guest)和宿主机(host)之间需要可靠的通信通道。这篇文章详细分析 guest 内的 agent 如何通过 vsock 与 host 侧的 shim 通信,以及整个请求链路是如何建立的。

通信架构概览

核心问题:shim 运行在 host 的用户态,agent 运行在 guest 的内核态,两者如何通信?

答案是 vsock(Virtual Socket)——一种专为虚拟化场景设计的 socket 类型。

vsock:虚拟化世界的 Socket

vsock 是 Linux 内核提供的 AF_VSOCK 地址族,专门为 host-guest 通信设计:

  • 不依赖网络:不需要 IP 地址、不需要网卡,直接通过 hypervisor 的虚拟设备传输
  • 可靠有序:与 TCP 一样是面向连接的、可靠的、有序的字节流
  • 低延迟:绕过了整个网络协议栈,数据在 hypervisor 内部转发

vsock 地址

vsock 使用 (CID, Port) 二元组寻址:

角色CIDPort
Host(任意进程)VMADDR_CID_HOST自定义
Guest(agent)VMADDR_CID_ANY固定端口

通道建立流程

CubeShim 侧:连接建立

1. 配置 vsock

prepare_resource() 阶段,CubeShim 将 vsock 配置注入 VM 配置:

rust
// sandbox/sb.rs — prepare_resource()
pub async fn prepare_resource(&mut self) -> CResult<VmConfig> {
    let mut vc = VmConfig::default();
    vc.set_kernel(self.conf.kernel.clone())
      .set_vcpus(self.conf.vm_res.cpu)
      .set_memory(self.conf.vm_res.memory, false)
      .add_nets(&self.conf.net)?
      .add_disks(&self.conf.disk)
      .add_virtiofs(&self.conf.virtiofs)
      .add_vsock(self.id.clone());   // ← 关键:配置 vsock
    // ...
    Ok(vc)
}

add_vsock() 会生成 vsock 设备配置,包括 sandbox ID(用于标识连接)。

2. 等待 vsock 就绪

VM 启动后,hypervisor 会通过事件通道通知 shim "vsock 已就绪":

rust
// sandbox/sb.rs — start_vm()
async fn start_vm(&mut self) -> CResult<bool> {
    // ... launch vmm, boot vm ...

    let ch = self.ch.as_mut().unwrap().lock().await;
    let ev = ch.wait_notify(Duration::from_nanos(10_000_000_000)).await?;

    if CH::NotifyEvent::VsockServerReady != ev {
        return Err(format!("Not an expected event: {:?}", ev));
    }
    // vsock 通道已就绪,可以连接 agent 了
    Ok(snapshot)
}

3. 建立 tRPC 连接

收到 VsockServerReady 后,CubeShim 通过 vsock 连接到 agent:

rust
// sandbox/sb.rs — connect_agent()
async fn connect_agent(&mut self) -> CResult<()> {
    let conn = AsyncUtils::connect_agent(&self.id).await?;
    let client = agent_ttrpc::AgentServiceClient::new(conn.clone());
    self.conn = Some(Arc::new(Mutex::new(conn)));
    self.client = Some(Arc::new(Mutex::new(client)));
    Ok(())
}

AsyncUtils::connect_agent() 内部做的事情:

  1. 创建 AF_VSOCK socket
  2. 连接到 guest 的 vsock 端口(sandbox ID 用于路由)
  3. 包装为 tRPC client(基于 gRPC 协议)

4. 发送 RPC 请求

连接建立后,shim 通过 tRPC client 调用 agent 的方法:

rust
// 示例:创建 sandbox
client.create_sandbox(ctx, &req).await?;

// 示例:创建容器
client.create_container(ctx, &req).await?;

// 示例:等待进程退出
client.wait_process(ctx, &req).await?;

Agent 侧:监听和处理

1. 启动 tRPC 服务器

agent 在 start_sandbox() 中启动 tRPC 服务器,监听 vsock:

rust
// main.rs — start_sandbox()
async fn start_sandbox(logger, config, init_mode, tasks, shutdown) {
    let s = Sandbox::new(logger)?;
    let sandbox = Arc::new(Mutex::new(s));

    // 启动 tRPC 服务器
    let mut server = rpc::start(sandbox, config.server_addr.as_str())?;
    server.start().await?;

    // server_addr 格式类似 "vsock:///dev/vsock, port"
    // agent 监听 VMADDR_CID_ANY 上的指定端口
}

2. 注册服务

rpc::start()AgentService 注册为 tRPC 服务:

rust
// rpc.rs
pub fn start(sandbox: Arc<Mutex<Sandbox>>, server_address: &str) -> Result<TtrpcServer> {
    let agent_service = AgentService { sandbox };
    let agent_ttrpc_service = agent_ttrpc::create_agent_service(agent_service);

    let mut server = TtrpcServer::new()
        .bind(server_address)?
        .register_service(agent_ttrpc_service);

    Ok(server)
}

3. 处理请求

当 shim 发来 RPC 请求时,tRPC 框架自动路由到 AgentService 的对应方法:

rust
// rpc.rs — AgentService trait 实现
#[async_trait]
impl agent_ttrpc::AgentService for AgentService {
    async fn create_container(&self, ctx: &TtrpcContext, req: CreateContainerRequest) -> ttrpc::Result<Empty> {
        trace_rpc_call!(ctx, "create_container", req);
        is_allowed!(req);  // 权限检查
        self.do_create_container(req).await
            .map_err(|e| ttrpc_error!(ttrpc::Code::INTERNAL, e))?;
        Ok(Empty::new())
    }

    async fn exec_process(&self, ctx: &TtrpcContext, req: ExecProcessRequest) -> ttrpc::Result<Empty> {
        // ...
    }

    async fn wait_process(&self, ctx: &TtrpcContext, req: WaitProcessRequest) -> ttrpc::Result<WaitProcessResponse> {
        // ...
    }
    // ... 其他方法
}

完整请求链路

kubectl exec 为例,展示一个命令从用户到执行的完整链路:

流式 I/O 的实现

对于 execattach 等场景,需要双向流式 I/O。agent 通过 pipe 或 PTY 实现:

Pipe 模式

rust
// agent/rpc.rs
async fn do_exec_process(&self, req: ExecProcessRequest) -> Result<()> {
    let mut p = Process::new(&logger, &ocip, &exec_id, false, pipe_size)?;
    p.open_io(&logger, None)?;  // 创建 stdin/stdout/stderr pipes
    ctr.run(p).await?;
    Ok(())
}

// 读取 stdout
async fn do_read_stream(&self, req: ReadStreamRequest, stdout: bool) -> Result<ReadStreamResponse> {
    let reader = if p.term_master.is_some() {
        p.get_reader(StreamType::TermMaster)   // PTY 模式
    } else if stdout {
        p.get_reader(StreamType::ParentStdout)  // Pipe 模式
    } else {
        p.get_reader(StreamType::ParentStderr)
    };
    let data = read_stream(reader, req.len).await?;
    Ok(ReadStreamResponse { data })
}

TTY 模式

当请求分配 TTY 时,agent 使用 PTY(pseudo-terminal):

rust
// agent/console.rs
fn run_in_child(slave_fd: c_int, shell: String) -> Result<()> {
    setsid()?;
    dup2(slave_fd, STDIN_FILENO)?;
    dup2(slave_fd, STDOUT_FILENO)?;
    dup2(slave_fd, STDERR_FILENO)?;
    unsafe { libc::ioctl(0, libc::TIOCSCTTY); }  // 设置控制终端
    execvp(cmd, &args);  // 执行 shell
}

master 端通过 pipestream 与 vsock 双向转发数据。

健康检查与 VM 监控

CubeShim 在后台持续监控 guest 的健康状态:

rust
// sandbox/sb.rs — monitor_vm()
async fn monitor_vm(&self, check_agent: bool) -> CResult<(Sender<()>, JoinHandle<()>)> {
    let client = health_ttrpc::HealthClient::new(conn);  // 健康检查 client

    let handle = tokio::spawn(async move {
        loop {
            // 检查 hypervisor 事件
            if let Ok(ev) = ch.try_wait_notify() {
                if ev == CH::NotifyEvent::VmShutdown {
                    // VM 已关闭,标记所有容器为 exited
                    *state = SandBoxState::Exited;
                    for (_, container) in containers.iter() {
                        container.notify_vm_shutdown().await;
                    }
                    break;
                }
            }

            // 定期 ping agent
            if check_agent && counter > interval {
                client.check(ctx, &req).await?;  // 健康检查 RPC
            }

            sleep(Duration::from_millis(1000)).await;
        }
    });

    Ok((tx, handle))
}

cube-runtime CLI

CubeShim/cube-runtime/ 提供了一个命令行工具,用于管理 VM 快照等操作:

bash
# 创建快照
cube-runtime snapshot create ...

# 登录到 guest
cube-runtime login ...

# Shell 补全
cube-runtime completions bash

这个工具通过 cube_hypervisor::VmmInstance API 与 hypervisor 交互,复用了与 shim 相同的底层通道。

小结

整个 guest → host 通信链路可以总结为:

层级组件协议说明
containerd ↔ ShimUDS (Unix Domain Socket)tRPC/gRPC标准 Shim v2 协议
Shim ↔ AgentvsocktRPC/gRPChypervisor 内部转发
Agent ↔ 容器pipe / PTY本地 I/O进程间通信

vsock 是整个架构的关键——它提供了一种不依赖 guest 网络栈的可靠通信通道,使得即使 guest 没有配置网络,shim 也能管理和控制容器。

延伸阅读