gVisor FUSE 支持详解:In-Sandbox 与 External FUSE Server 两种模式的配置与实践
【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor
gVisor 作为容器应用内核(Application Kernel for Containers),在其用户态内核 Sentry 中完整实现了 FUSE(Filesystem in Userspace)协议栈,让用户态程序可以在沙箱内挂载并服务文件系统。本文以 g3doc/user_guide/fuse.md 为主线,系统讲解 gVisor 支持的两种 FUSE 工作模式——沙箱内 FUSE(In-Sandbox FUSE)与外部 FUSE 服务器(External FUSE Server),并结合仓库源码深入解析其底层通信机制、--pass-fd文件描述符传递、FUSE 挂载参数以及实测验证路径,帮助读者在 gVisor 沙箱中正确搭建和使用 FUSE 文件系统。
两种 FUSE 模式概览
gVisor 在沙箱内支持两种 FUSE 运行方式:
- In-sandbox FUSE(沙箱内 FUSE):FUSE 守护进程(daemon)与应用程序一同运行在沙箱内部,通过
/dev/fuse与 gVisor 内核通信。这是标准的 FUSE 模型,与常规 Linux 系统上的 FUSE 行为一致。 - External FUSE server(外部 FUSE 服务器):FUSE 服务器运行在宿主机上、沙箱之外,通过与沙箱之间传递的 socketpair 通信。当文件系统实现必须访问沙箱内不可用的资源(如宿主机上的特殊设备、凭据或本地磁盘)时,这种模式尤为适用。
两种模式共用同一套 FUSE 实现 代码库,区别仅在于请求/响应的传输层(transport)不同。
External FUSE Server:宿主机进程直接服务沙箱文件系统
外部 FUSE 服务器特性允许宿主机侧进程将 FUSE 文件系统"注入"gVisor 沙箱。宿主进程与沙箱通过 Unix socketpair 使用标准 FUSE 协议通信,从而避免了以往通过 I/O 代理(I/O proxy)机制暴露宿主机文件系统所带来的上下文切换性能开销。
工作原理
- 宿主机创建一个 Unix socketpair(
SOCK_SEQPACKET类型)。 - 将 socketpair 的一端通过
runsc run或runsc create的--pass-fd标志传入沙箱。 - 另一端交给运行在宿主机上的 FUSE 服务器进程。
- 沙箱内的应用程序使用传入的文件描述符挂载 FUSE 文件系统。
- 所有 FUSE 操作(read、write、lookup 等)都通过 socketpair 转发给宿主 FUSE 服务器,由后者执行实际的 I/O。
从源码层面看,这一路径由hostConnection实现(见 pkg/sentry/fsimpl/fuse/host_connection.go)。与使用沙箱内/dev/fuse设备不同,它直接将 FUSE 请求写入宿主 FD、并从宿主 FD 读取响应,从而让沙箱外的 FUSE 服务器可以服务该文件系统。其关键设计包括:
- 并发请求处理:多个请求可以同时在途(in-flight)。写入由
writeMu互斥锁串行化,而一个后台 reader goroutine 通过连接的completionsmap 将响应分发回对应的调用者(见readLoop与call的实现)。 - FUSE_INIT 握手:
InitSend会在宿主 FD 上同步完成 FUSE_INIT 握手,成功后启动后台 reader goroutine 进入并发处理阶段;握手期间请求体使用FUSE_KERNEL_VERSION/FUSE_KERNEL_MINOR_VERSION以及fuseDefaultMaxReadahead、fuseDefaultInitFlags等默认值构造。 - FD 归属管理:在 fusefs.go 的 getFilesystemHostFD 中,gVisor 会先
dup一份宿主 FD 归 FUSE 连接所有,并将导入路径设置的非阻塞标志清除(FUSE passthrough 连接使用同步阻塞 I/O)。 - 请求上限:连接默认允许最多
maxActiveRequestsDefault = 10000个活跃请求(见 fusefs.go),超出后请求在调用服务器时阻塞。
设置步骤
第 1 步:创建 socketpair 并启动 FUSE 服务器
宿主进程创建 socketpair,并将其中一端交给 FUSE 服务器:
# 示例:创建 socketpair 并将 FD 4 传给 FUSE 服务器。 # FUSE 服务器从其 FD 读取 FUSE 请求,并用标准 FUSE 协议 # (FUSEHeaderIn/Out 帧格式)做出响应。 ./my_fuse_server --fd=4 --backing-dir=/data/sharedFUSE 服务器必须实现 FUSE 内核协议:读取FUSEHeaderIn帧格式的请求,写入FUSEHeaderOut帧格式的响应。最低限度应处理FUSE_INIT、FUSE_GETATTR、FUSE_LOOKUP、FUSE_OPEN、FUSE_READ、FUSE_RELEASE和FUSE_ACCESS等操作码;FUSE_WRITE、FUSE_FLUSH、FUSE_STATFS、FUSE_CREATE等其他操作码可按需补充。
关于协议帧的处理细节,可参考 pkg/sentry/fsimpl/fuse/host_connection_test.go 中的echoServer测试辅助函数:它从服务器侧 FD 读取请求,解析FUSEHeaderIn,然后构造带FUSEHeaderOut(含Len、Error、Unique字段,其中Unique必须回填请求的Unique以完成请求-响应配对)的响应并写回。
第 2 步:将 FD 传入沙箱
使用--pass-fd标志把宿主侧 socketpair FD 映射进沙箱:
runsc run \ --pass-fd=3:100 \ --bundle=/path/to/bundle \ my-container格式为--pass-fd=HOST_FD:GUEST_FD。上例中宿主 FD 3 进入沙箱后成为 FD 100。--pass-fd标志可多次指定,以传递更多文件描述符。该标志由 runsc/cmd/run.go 与 runsc/cmd/exec.go 定义,说明为:"file descriptor passed to the container in M:N format, where M is the host and N is the guest descriptor (can be supplied multiple times)"。这些映射最终在 runsc/boot/loader.go 的createFDTable中被写入沙箱内进程的 FD 表。
第 3 步:在容器内挂载 FUSE 文件系统
沙箱内的应用程序引用传入的 FD 来挂载 FUSE 文件系统:
// 使用传入的文件描述符挂载。 mount("fuse", "/mnt/shared", "fuse", MS_NODEV | MS_NOSUID, "fd=100,user_id=0,group_id=0,rootmode=40000");或者在 shell 中等价执行:
mount -t fuse fuse /mnt/shared -o fd=100,user_id=0,group_id=0,rootmode=40000挂载参数详解
gVisor 的 FUSE 文件系统在 fusefs.go 的 parseOptions 中解析挂载选项,原文档列出的核心参数如下:
fd=N:沙箱内的文件描述符编号。必填,缺失时挂载直接返回EINVAL。user_id=UID:挂载所有者的 UID。必填,且必须映射到当前用户命名空间,未映射同样报EINVAL。group_id=GID:挂载所有者的 GID。必填,映射要求同user_id。rootmode=MODE:根 inode 的权限模式(八进制)。目录用40000(即S_IFDIR)。必填。
结合源码,gVisor 还支持以下可选项,可在配置时按需追加:
max_read=N:单次读取的最大字节数。未指定时默认math.MaxUint32;若显式指定且小于fuseMinMaxRead,会被抬升到该下限值。default_permissions:让内核基于属主与模式位执行标准 Unix 权限检查,而不是把检查交给服务器。allow_other:允许不拥有该 FUSE 挂载的进程访问它。
另外,挂载选项还受maxActiveRequests(默认 10000)约束,该值控制任意时刻活跃请求的上限。
端到端示例(Go)
以下是完整的 Go 示例,演示宿主侧设置:
// 为 FUSE 通信创建 socketpair。 fds, _ := unix.Socketpair(unix.AF_UNIX, unix.SOCK_SEQPACKET, 0) // fds[0] 进入沙箱,fds[1] 交给 FUSE 服务器。 sandboxFile := os.NewFile(uintptr(fds[0]), "fuse-sandbox") serverFD := fds[1] // 在宿主机上启动 FUSE 服务器,使用服务器侧 FD。 go myFuseServer.Serve(serverFD, "/data/backing") // 启动沙箱并将 FD 传入。 cmd := exec.Command("runsc", "run", "--pass-fd=3:100", // 宿主 FD 3 → 沙箱内 FD 100 "--bundle="+bundleDir, containerID, ) cmd.ExtraFiles = []*os.File{sandboxFile} // 子进程中成为 FD 3 cmd.Run()注意cmd.ExtraFiles的语义:ExtraFiles 中的文件在子进程中依次从 FD 3 开始编号,因此这里sandboxFile恰好对应--pass-fd=3:100中的宿主 FD 3。
限制
- 没有 /dev/fuse:外部路径不使用
/dev/fuse,应用程序直接用传入的 socketpair FD 挂载 FUSE。 - 仅限 FUSE 协议:宿主服务器必须实现原始 FUSE 内核协议。更上层的 FUSE 库(如 libfuse)通常期望
/dev/fuse,不经适配可能无法直接在 socketpair 上工作。
In-Sandbox FUSE:标准 FUSE 模型
gVisor 同样支持标准 FUSE 模型:FUSE 守护进程与应用程序都运行在沙箱内。守护进程打开/dev/fuse,应用程序使用返回的文件描述符挂载 FUSE 文件系统。这与常规 Linux 系统上的 FUSE 行为一致,由 gVisor 内核在内部处理 FUSE 协议。
在源码层面,/dev/fuse由 pkg/sentry/fsimpl/fuse/dev.go 中的fuseDevice(字符设备次设备号 229)与DeviceFD实现,DeviceFD实现了 vfs 的文件描述符接口,负责承载 FUSE 连接状态与请求队列。挂载时,fusefs.go 的 getFilesystemDeviceFD 会复用传入的DeviceFD,若连接尚未初始化则发送 FUSE_INIT 请求给沙箱内的 FUSE 守护进程。
两种模式的传输层抽象在 pkg/sentry/fsimpl/fuse/connection.go 中通过fuseConn接口统一:
deviceConn:面向沙箱内/dev/fuse路径,使用基于队列的机制——FUSE 守护进程从DeviceFD读取请求、写入响应;hostConnection:面向宿主 FD passthrough 路径,直接对宿主 FD 进行读写。
共享的connection结构则统一管理 FUSE 协议状态(目标协议版本为 FUSE 7.23)、活跃请求计数、请求-响应配对(completionsmap)、读写队列与初始化同步等。
仓库内的兼容性验证
仓库在 images/fuse/gcs/ 下提供了针对gcsfuse的 gVisor 兼容性测试套件(In-Sandbox FUSE 的实际用例),用于验证沙箱内 FUSE 的完整读写链路:
./images/fuse/gcs/run_test.sh脚本流程为:检查 gVisor(runsc)与 gcloud 凭据 → 创建临时 GCS bucket → 构建测试 Docker 镜像 → 使用 gVisor 运行测试容器 → 清理临时 bucket。容器内的 gcsfuse_test.sh 使用gcsfuse -implicit-dirs挂载 GCS bucket,并依次验证:
- 目录操作:mkdir / rmdir / readdir;
- 文件创建与基本 I/O:creat / open / write / read;
- 文件属性:stat(大小)/ utimens(touch)/ chmod / chown;
- 文件修改:truncate / append;
- 符号链接:symlink / readlink;
- 重命名:rename(文件与目录);
- 文件系统信息:statfs(df);
- 清理操作:unlink / rmdir。
这一测试套件覆盖了 FUSE 协议中大部分高频操作码,可作为在 gVisor 沙箱内自建 FUSE 文件系统时验证基本功能是否齐备的参考清单。
选型建议与小结
| 维度 | In-Sandbox FUSE | External FUSE Server |
|---|---|---|
| FUSE 服务器位置 | 沙箱内 | 宿主机 |
| 通信通道 | /dev/fuse(DeviceFD 队列机制) | 宿主 FD(socketpair,--pass-fd传入) |
| 适用场景 | 标准 FUSE 文件系统、与 libfuse 生态兼容 | 需访问沙箱外资源、避免 I/O 代理性能开销 |
| 服务器实现要求 | 遵循常规 FUSE 守护进程模型 | 实现原始 FUSE 内核协议(FUSEHeaderIn/Out 帧) |
选择哪种模式,主要取决于 FUSE 文件系统的实现是否需要访问沙箱内不可用的宿主资源,以及是否能接受实现原始 FUSE 协议的额外成本。无论哪种模式,gVisor 的 FUSE 实现 都统一在connection层管理协议状态,并通过可插拔的fuseConn传输层分发请求,保证了两条路径行为的一致性。
【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考