首页
/ Moby 仓库中的 Go vsock 库解析:用 Linux VM Socket(AF_VSOCK)打通 hypervisor 与虚拟机

Moby 仓库中的 Go vsock 库解析:用 Linux VM Socket(AF_VSOCK)打通 hypervisor 与虚拟机

2026-09-07 23:50:10作者:钟日瑜

导读

本篇文章围绕 Moby(Docker 引擎)仓库中 vendored 的 mdlayher/vsock Go 库(v1.3.0)展开:它以 Linux VM Sockets(AF_VSOCK)为底层协议,为在 hypervisor 与其虚拟机之间进行高效通信提供了一组与标准库 net 完全兼容的 net.Conn / net.Listener / net.Addr 实现。读完本文,你将理解 vsock 的 context ID 地址模型、掌握 Listen / Dial / ContextID 等核心 API 的用法与底层 Linux 系统调用链,并能据此在虚拟机管理场景中直接编写或改造 Go 通信程序。需要说明的是,该库在本仓库中作为间接依赖被引入(见 go.mod),下述 API 与实现证据均取自本仓库 vendor 目录 下的真实源码,可放心对照查阅。

1. vsock 是什么:VM Sockets 的核心背景

Moby 仓库 vendored 的 README 开门见山地说明了该包的定位:

Package vsock provides access to Linux VM sockets (AF_VSOCK) for communication between a hypervisor and its virtual machines. MIT Licensed.

也就是说,vsock 包是对 Linux VM Sockets 这一内核协议族(AF_VSOCK)的 Go 封装,其典型场景是 hypervisor(宿主机/VMM)与虚拟客户机之间的数据交换,例如:

  • 宿主机上一个管理进程与客户机内的 agent 通信;
  • 客户机内进程回连宿主机上运行的服务;
  • 在**不依赖网络栈(无 IP 分配)**的情况下提供低开销的通信通道。

VM Socket 的核心地址模型是 context ID(CID)——每个具备 vsock 能力的实体(hypervisor、宿主机进程、虚拟机)都拥有一个唯一的整数 CID;通信时用 (contextID, port) 二元组寻址,这与 TCP 的 (IP, port) 寻址模型一一对应,但对虚拟化场景做了专门优化。包文档 doc.go 明确承诺了与标准库的兼容契约:

The types in this package implement interfaces provided by package net and may be used in applications that expect a net.Listener or net.Conn.

  • *Addr implements net.Addr
  • *Conn implements net.Conn
  • *Listener implements net.Listener

这意味着只要代码依赖的是 net.Conn / net.Listener 这类标准接口,就可以把 vsock 连接"无缝"替换进去,例如复用在 net/http、通用 RPC 或自定义协议栈之上。

2. 仓库定位:Moby 中 vendored 的 v1.3.0 间接依赖

在动笔编写代码之前,先明确该库在本仓库中的存在形态。Moby 的模块依赖文件 go.mod 第 238 行声明:

github.com/mdlayher/vsock v1.3.0 // indirect

对应地在 vendor 清单 vendor/modules.txt 中记录了其 vendored 元信息,而实际源码位于 vendor/github.com/mdlayher/vsock/ 目录,包含如下文件:

文件 职责
README.md 包简介与稳定性声明
doc.go 包级文档与 net 接口兼容性契约
vsock.go 跨平台公开 API:常量、Addr/Conn/Listener、错误归一化
conn_linux.go Linux 下 Dial 的系统调用实现(socket/connect
listener_linux.go Linux 下监听实现(bind/listen/accept
fd_linux.go Linux 下读取本机 CID(/dev/vsock + ioctl)
vsock_others.go 非 Linux 平台的占位实现
CHANGELOG.md 版本演进记录

两点值得注意的仓库事实:

  1. go.mod 中标注为 // indirect,说明它是被 Moby 的依赖图间接引入的。对本仓库主代码路径(排除 vendor 与测试)的源码检索未发现对 github.com/mdlayher/vsock 的直接 import,因此可以推断:本文所介绍的是该第三方库本身的能力,而非 Moby 某个具体业务模块的用法。
  2. 平台强相关:整个库只有 Linux 有真实实现,其余平台(见 vsock_others.go)的所有函数都直接返回 vsock: not implemented on <GOOS>,因为 AF_VSOCK 本身就是 Linux 内核特性。

3. 地址模型:CID 常量与 Addr 类型

3.1 三个保留 CID 常量

v sock.go 在文件顶部定义了三种被 Linux 保留的特殊 context ID,其源码注释提供了权威语义:

常量 语义(源自源码注释)
Hypervisor 0x0 hypervisor 进程自身通信。注意注释特别强调:这不是指宿主机上运行的普通进程,多数用户应该选择 Host
Local 0x1 本机上的配对 socket 通信。它可作为 UNIX socket 的替代品,在单机测试 vsock 应用时非常有用。
Host 0x2 与宿主机上 hypervisor 之外的进程通信。这是"从客户机 dial 宿主机上运行的服务进程"时的正确选择。

这组常量定义在 vsock.go#L13-L28。理解"Hypervisor ≠ Host"这一区分至关重要:客户机内部看到 0x0 只指向 VMM 自己,而客户机要连到宿主机上的普通守护进程时必须使用 0x2

3.2 Addr:端点地址

// An Addr is the address of a VM sockets endpoint.
type Addr struct {
	ContextID, Port uint32
}

vsock.go#L317-L344。它实现了 net.Addr 接口:

  • Network() 固定返回 "vsock"(错误信息中的网络名,见源码中的 network 常量);
  • String() 返回人类可读的 cid:port 形式,并对特殊 CID 做标注,例如:hypervisor(0):1234local(1):8080host(2):80;而对普通 CID(虚拟机)则呈现为 vm(<cid>):<port>

4. 核心 API 纵览

结合 vsock.godoc.go,整个包的公开面可以概括为 5 个函数 + 3 个核心类型:

4.1 构造函数

函数签名 说明
Listen(port uint32, cfg *Config) (*Listener, error) 打开一个面向连接的 net.Listener。它内部先调用 ContextID() 自动推断本机 CID,再委托给 ListenContextIDvsock.go#L76-L84)。
ListenContextID(contextID, port uint32, cfg *Config) (*Listener, error) 显式指定 CID 的监听版本,面向进阶场景(例如想监听在 Local CID 上做自测),见 vsock.go#L91-L102
FileListener(f *os.File) (*Listener, error) 从已打开的 os.File 还原出一个 Listener,可配合 systemd socket activation 等外部机制使用(CHANGELOG v1.1.0 引入),见 vsock.go#L111-L119
Dial(contextID, port uint32, cfg *Config) (*Conn, error) 面向连接地拨号到某个 vsock 监听者(vsock.go#L177-L188)。从宿主机连虚拟机需填 VM 的 CID;从虚拟机回连宿主机时按需选择 HypervisorHost
ContextID() (uint32, error) 读取本机的 vsock context ID,同时可作为系统是否支持 vsock 的自检手段:若内核模块缺失、无访问权限或不支持,将返回错误(vsock.go#L351-L359)。

关于 port0:源码 vsock.go#L66-L67 注释说明——允许内核自动分配端口,之后通过 Listener.Addr() 拿到实际监听地址。底层实现见 5.2 节的 VMADDR_PORT_ANY

4.2 Listener / Conn / Config

  • *Listenervsock.go#L121-L161)实现 net.ListenerAccept() 返回的连接一定可以断言为 *ConnAddr() 返回监听地址;Close() 只停止监听、不会关闭已 Accept 的连接;还支持 SetDeadline
  • *Connvsock.go#L190-L275)实现 net.Conn 的全部方法:Read/WriteLocalAddr/RemoteAddr、三个 Set*DeadlineClose;此外额外提供:
    • CloseRead() / CloseWrite():分别关闭读半部/写半部;
    • SyscallConn() (syscall.RawConn, error):实现 syscall.Conn,供需要直接操作底层 fd(例如设置 SO_* 选项)的高级场景使用。返回的 rawConn 会把内部调用包装成统一的 net.OpError
  • *Configvsock.go#L56-L59):目前是空结构体,是官方为 v1.x 时代预留的扩展位;源码注释明确写着构造函数的 cfg 形参传 nil 即用默认配置(CHANGELOG v1.0.0 也指出"vsock.Config 目前没有选项,所有调用点传 nil 即可")。

5. 从 API 到内核:Linux 实现走读

README 只有简短定位,真正的技术细节沉淀在平台实现文件中。下面顺着调用链把源码读一遍。

5.1 拨号 DialAF_VSOCK + SOCK_STREAM

conn_linux.go 展示了 Linux 下 dial 的完整流程:

  1. socket.Socket(unix.AF_VSOCK, unix.SOCK_STREAM, 0, "vsock", nil) 创建面向字节流的 vsock socket;
  2. 构造目标地址 &unix.SockaddrVM{CID: cid, Port: port} 并执行 Connect
  3. 通过 Getsockname / Connect 返回的远端地址构造 Conn,其中本地与远端地址都被解析成 Addr{ContextID, Port} 结构。

一个值得留意的实现细节:源码注释提到在某些 CI 环境中 getpeername(2) 返回 nil,因此当 rsa == nil 时退化为直接使用传入的 sa 合成远端地址,保证跨环境行为一致。

5.2 监听 Listen:bind + listen + accept

listener_linux.go 对应实现:

  1. 同样先创建 AF_VSOCK/SOCK_STREAM socket;
  2. port == 0,改写为 unix.VMADDR_PORT_ANY,即让内核自动分配端口;
  3. Bind(&unix.SockaddrVM{CID: cid, Port: port})
  4. Listen(unix.SOMAXCONN) 设置内核默认的最大等待队列;
  5. 任一步失败都会关闭 socket 再返回错误,避免 fd 泄漏。

Accept(同文件 listener_linux.go#L31-L48)从 accept 返回的 *unix.SockaddrVM 中取出对端 CID/Port,构造远端 Addr

另外,fileListenerlistener_linux.go#L90-L103)把任意 os.File 包装为 socket,并通过 Getsockname 校验地址族确实为 SockaddrVM,防止误把一个 TCP 或其他类型的 fd 包装成 vsock Listener——若类型不符会返回 EINVAL 类型的系统调用错误。

5.3 读取本机 CID:/dev/vsock + ioctl

fd_linux.go 给出了 ContextID() 的实现——打开 /dev/vsock 设备文件后执行 IOCTL_VM_SOCKETS_GET_LOCAL_CID ioctl

f, err := os.Open(devVsock) // devVsock = "/dev/vsock"
...
return unix.IoctlGetUint32(int(f.Fd()), unix.IOCTL_VM_SOCKETS_GET_LOCAL_CID)

因此也可以反推出该库的运行前提:/dev/vsock 必须存在(宿主机与客户机内核都需加载 vsock/virtio_vsock 相关模块,且设备节点可访问)。任何一次 Dial/Listen 若报与 /dev/vsock 相关的 PathError,源码在错误归一化时会有意不展开外层错误(见 vsock.go#L386-L391),让调用者能看到"设备打不开/权限不足"这类真实根因,而不是笼统的 permission denied

5.4 非 Linux 平台:明确的"未实现"语义

v sock_others.go(build tag !linux)为所有公开操作提供统一占位实现,返回:

vsock: not implemented on <GOOS>

这种"平台能力即编译期事实"的写法(而非运行时 panic)保证了代码可以在任意平台正常编译,行为则由运行时错误清晰表达。

6. 手写一个最小 vsock 服务端与客户端

下面基于上述真实 API 给出可直接套用的最小示例(API 签名与语义均取自 vsock.go,示例仅作说明用途)。

6.1 服务端:echo 监听

package main

import (
	"fmt"
	"io"
	"log"
	"net"

	"github.com/mdlayher/vsock"
)

func main() {
	// Listen 会自动推断本机 CID 并绑定到指定端口;
	// 想要自动分配端口可传 0,之后通过 ln.Addr() 查询。
	ln, err := vsock.Listen(1024, nil)
	if err != nil {
		log.Fatalf("listen on vsock port 1024: %v", err)
	}
	defer ln.Close()
	log.Printf("listening on %s", ln.Addr()) // 形如 vm(3):1024 / local(1):1024

	for {
		c, err := ln.Accept()
		if err != nil {
			log.Fatalf("accept: %v", err)
		}
		go echo(c)
	}
}

func echo(c net.Conn) {
	defer c.Close()
	log.Printf("peer connected: %s", c.RemoteAddr())
	if _, err := io.Copy(c, c); err != nil { // 把读到的内容原样写回
		log.Printf("echo error: %v", err)
	}
}

代码要点:

  • 服务端无需关心自己是宿主机还是虚拟机——Listen 自动绑定"本机"的 CID,之后对端按其可达性寻址即可;
  • Listen(0, nil) 支持自动端口分配,适合交给 systemd socket activation 或服务发现场景;
  • 返回值就是标准 net.Listener,因此可以原样传给 http.Serve(ln, ...) 等上层组件。

6.2 客户端:按寻址方向选择 CID

// 场景 A:宿主机进程 -> 客户机内的服务。
// 需要知道目标 VM 被 hypervisor 分配的 context ID(此处用变量 vmCID 表示)。
c, err := vsock.Dial(vmCID, 1024, nil)
if err != nil {
	log.Fatalf("dial guest: %v", err)
}
defer c.Close()

// 场景 B:客户机进程 -> 宿主机上的普通服务进程。
c, err := vsock.Dial(vsock.Host, 1024, nil) // 注意:不是 vsock.Hypervisor
if err != nil {
	log.Fatalf("dial host: %v", err)
}

6.3 单机自测:Local CID 是最佳搭档

由于 Local(CID 0x1)允许同一台机器上的两个 socket 互相通信,它是无需任何虚拟机即可单机验证 vsock 应用逻辑的方式:

// 服务端监听在本机 Local CID 上,端口自动分配。
ln, err := vsock.ListenContextID(vsock.Local, 0, nil)
if err != nil {
	log.Fatal(err)
}
defer ln.Close()
port := ln.Addr().(*vsock.Addr).Port // 取回自动分配的端口

// 客户端用同样的 Local CID 拨号。
c, err := vsock.Dial(vsock.Local, port, nil)
if err != nil {
	log.Fatal(err)
}

如果运行前想先确认环境是否支持 vsock,直接调用 vsock.ContextID() 即可:成功返回说明本机具备 vsock 能力,失败则通常是内核模块未加载、/dev/vsock 不存在或设备访问被拒绝。

7. 错误处理的标准化:把一切收敛为 net.OpError

为了让 vsock 连接与标准 net 连接在使用体验上完全一致,库内实现了一个集中的错误归一化函数 opErrorvsock.go#L364-L433)。它做的事情包括:

  1. 展开包装错误:对 *os.PathError,除与 /dev/vsock 相关者外都取其内部错误;
  2. 统一语义化错误
    • io.EOFENOTCONN("transport not connected")统一映射为 io.EOF
    • os.ErrClosedEBADF 或文本含 use of closed 的错误统一映射为 net.ErrClosed(与 net.TCPConn 对已关闭文件的表现对齐);
  3. 按操作类型填充 net.OpErrorSource/Addr 字段
    • dial/read/write/close 等把本地地址放 Source、远端地址放 Addr
    • listen/accept/set 等只放本地地址。

于是调用方可以像处理 TCP 错误那样使用 net.Error 接口、判断超时/关闭语义,而不必感知底层是 UNIX errno 还是文件系统错误。实现上的另一处细节(见 vsock.go#L190-L193):*Conn 同时满足 net.Connsyscall.Conn,为原始 fd 操作保留后门。

8. 稳定性与版本演进:为什么 v1 API 值得信赖

v sock README 的 Stability 章节 给出了三条明确承诺,这也是把它引入 vendor 依赖的工程决策依据:

  1. 稳定 v1 API:未来任何破坏性变更都会以发布新 major 版本(v2、v3…)为代价,特性与修复继续落在 v1.x.x 系列中;
  2. 紧跟上游 Go 策略:只支持最近两个 major 版本的 Go,以保证关键特性与 bug 修复可用;
  3. 每次发布之间的变化以 CHANGELOG.md 为准。

结合本仓库实际 vendored 的 v1.3.0 与 CHANGELOG.md 记录,可以看到演进脉络:

  • v1.0.0:稳定首发。Dial/Listen 增加可选 *Config 参数以预留扩展空间;新增 ListenContextID 支持显式 CID 监听。
  • v1.0.1:修复非阻塞 connect(2) 的错误处理——通过检查 SO_ERROR socket 选项正确处理(Dial 内发起连接时的竞态),并固化测试。
  • v1.1.0:新增 vsock.FileListener,支持从外部(如 systemd socket activation)传入的 fd 构造 Listener。
  • v1.1.1:修复非 UNIX 平台(Windows 等)的编译问题(对 Linux 为无操作改动,为非 Linux 用户提供更友好的体验);这也是最后一个支持 Go 1.17 及以下的版本。
  • v1.2.0 / v1.2.1:要求 Go 1.18+,得以使用新版本 x/sys 等依赖;继续推进依赖更新与测试覆盖。
  • v1.3.0(本仓库内版本):更新依赖并要求 Go 1.25;错误处理全面改用 net.ErrClosed;测试中新增对 ENETUNREACHETIMEDOUT 的校验。

上述 Go 版本要求的演进,与 README 中"只支持最近两个 major 版本 Go"的声明一脉相承——如果使用本包,请务必保持 Go toolchain 足够新。

9. 使用注意事项小结

围绕本仓库证据,归纳几条实践中需要留意的约束:

  • 仅限 Linux:非 Linux 平台所有操作返回 not implementedvsock_others.go)。
  • 依赖内核能力:宿主机与虚拟机内核需要 vsock 相关模块,且 /dev/vsock 可访问;可先通过 vsock.ContextID() 自检。
  • CID 语义别混用Hypervisor(0) 只对 hypervisor 自身进程;访问宿主机普通进程用 Host(2);单机联调用 Local(1)vsock.go#L13-L28)。
  • 注意 Go 版本要求:v1.3.0 要求 Go 1.25+,升级或 vendoring 时需同步评估 toolchain 约束。
  • 在 Moby 仓库中的角色:它是被间接引入的第三方依赖(go.mod 标注 // indirect),而非 Moby 自身业务代码直接使用的模块;本文所有用法均基于该库自身导出的公开 API,可独立在任意 Go 项目中 import "github.com/mdlayher/vsock" 使用。

如果需要在 Moby 这类大型 Go 工程里维护或升级 vsock,推荐优先阅读 vendor/github.com/mdlayher/vsock/CHANGELOG.md 核对破坏性变更,再依据 go.mod 中记录的版本执行升级。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388