Go-FUSE 教程


1. 概述

Go-FUSE 是用于 FUSE 内核模块的 Go 原生绑定。

与其它 FUSE 库相比,该库的亮点是:

  1. 全面和最新的协议支持(截至 7.12.28)。
  1. 性能可与 libfuse 媲美。

1.1. macOS 支持

虽然该库的主要开发者(hanwen@)没有 Mac 进行测试,但接受补丁,以使 Go-FUSE 可以在 Mac 上运行。

1.2. 示例

mkdir /tmp/mountpoint
example/zipfs/zipfs /tmp/mountpoint file.zip &
ls /tmp/mountpoint
fusermount -u /tmp/mountpoint 
mkdir /tmp/mountpoint
example/loopback/loopback -debug /tmp/mountpoint /some/other/directory &
ls /tmp/mountpoint
fusermount -u /tmp/mountpoint

2. fs

2.1. 设计

2.2. api

fs 为构建树状文件系统提供基础设施。

2.2.1. 文件系统实现的结构

为创建文件系统,需要先为文件系统树的节点定义类型。

type myNode struct {
	fs.Inode
}

// Node 类型必须是 InodeEmbedders
var _ = (fs.InodeEmbedder)((*myNode)(nil))

// Node 类型需要实现一些文件系统操作,比如 Lookup
var _ = (fs.NodeLookuper)((*myNode)(nil))

func (n *myNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
	ops := myNode{}
	out.Mode = 0755
	out.Size = 42
	return n.NewInode(ctx, &ops, fs.StableAttr{Mode: syscall.S_IFREG}), 0
}

方法名的灵感来自系统调用名,所以使用 Listxattr 而不是 ListXAttr。

通过在树根上调用 mount 的方式,挂载文件系统:

server, err := fs.Mount("/tmp/mnt", &myNode{}, &fs.Options{})
..
// 开始服务文件系统
server.Wait()

2.2.2. 错误处理

所有错误报告必须使用 syscall.Errno 类型。这是带有预定义错误码的整数,其中值 0 (OK)应该用于表示成功。

2.2.3. 文件系统概念

FUSE API 与用于在内核中定义文件系统的 Linux 内部 VFS API 非常相似。因此理解一些术语很有用。

文件内容:在常规文件中存储的原始字节。

路径:以 / 分隔的字符串路径,描述节点在文件系统树中的位置。比如:

dir1/file

表示路径 root → dir1 → file。

从树根到特定节点可能有多条路径,称为硬链接。比如:

	  root
	  /  \
	dir1 dir2
	  \  /
	  file

Inode:(“索引节点”)指向文件内容,存储文件或目录的元数据(大小、时间戳)。每个 inode 都有类型(目录、符号链接、常规文件等)和标识(64 位数字,对文件系统而言唯一)。目录可以有子目录。

内核中的 inode 在 Go-FUSE 中表示为 Inode 类型。

虽然常见的 OS API 用路径(字符串)表达,但文件系统的精确语义最好用 Inode 描述。这允许指定在极端情况下怎么做,比如将数据写入已删除文件时。

文件描述符:打开文件返回的句柄。文件描述符始终引用单个 Inode。

目录项(DirEnt):目录项将(父 inode 编号,名称字符串)元组映射到子节点,因此表示父/子关系(或不存在)。在 Go-FUSE 中没有等价类型,但是 Lookup 操作的结果本质上是目录项,内核将其放入缓存中。

2.2.4. 内核缓存

内核缓存来自 FUSE 进程的若干信息:

  1. 文件内容:在 Open 中使用 fuse.FOPEN_KEEP_CACHE 返回标记启用。ReadCache 和 WriteCache 操作文件内容,使用 Inode.NotifyContent 使该缓存失效。
  1. 文件属性(大小、修改时间等):使用 fuse.AttrOutfuse.EntryOut 中的超时属性字段控制。在 GetattrLookup 中填充这些字段。
  1. 目录项(FS 树中的父子关系):使用 fuse.EntryOut 中的超时字段控制。使用 Inode.NotifyEntryInode.NotifyDelete 使其失效。如果没有目录项超时,那么每次操作文件 "a/b/c" 都必须先对 "a"、"a/b" 和 "a/b/c" 进行查找,成本很高,因为涉及内核和 FUSE 进程之间的上下文切换。未成功的目录项查找也可以通过在 Lookup 返回 ENOENT 时设置超时进行缓存。libfuse C 库默认为属性和目录项指定 1 秒的超时时间,但对负项没有超时。在 Go-FUSE 中,可以通过在挂载时设置选项实现,比如:
sec := time.Second
opts := fs.Options{
	EntryTimeout: &sec,
	AttrTimeout: &sec,
}

2.2.5. 锁

网络文件系统支持通过 GetlkSetlkSetlkw 系列方法实现锁定。它们支持对常规文件的区域进行锁定。

2.2.6. 并行

内核中的 VFS 层被优化为高度并行,并行影响 FUSE 文件系统:多个 FUSE 操作并行运行可能导致竞态条件。强烈建议以并行发起文件操作的方式,测试 FUSE 文件系统,并且使用竞态检测器排除数据竞争。

2.2.7. 死锁

Go 运行时将 Goroutine 复用到操作系统线程上,并且假设一些系统调用不会阻塞。当从服务文件系统的同一进程访问文件系统时(比如在单元测试中),可能导致死锁,特别是当 GOMAXPROCS=1 时。Go 运行时假设系统调用不会阻塞,但实际上是由 Go-FUSE 进程服务的。

以下死锁是已知的:

  1. 使用 fork/exec 序列生成子进程:进程将自己分叉为父进程和子进程。父进程等待子进程发出 exec 失败或成功的信号,而子进程则准备调用 exec()。子进程中触发 FUSE 请求的任何设置步骤都可能导致死锁。

1.a. 如果子进程已指定目录,那么子进程将 chdir 到该目录。这将在目录上生成 ACCESS 操作。

可以通过禁用 ACCESS 操作避免这种死锁:在 Access 中实现中返回 syscall.ENOSYS,并且确保在初始化子进程前触发调用它。

1.b. 如果子进程继承文件,并且子进程使用 dup3() 重新映射文件描述符。如果目的 fd 恰好由 Go-FUSE 支持,那么 dup3() 调用将隐式关闭该 fd,同时生成 FLUSH 操作。比如:

f1, err := os.Open("/fusemnt/file1")
// f1.Fd() == 3
f2, err := os.Open("/fusemnt/file1")
// f2.Fd() == 4

cmd := exec.Command("/bin/true")
cmd.ExtraFiles = []*os.File{f2}
// f2 (fd 4) is moved to fd 3. Deadlocks with GOMAXPROCS=1.
cmd.Start()

通过确保指向 FUSE 挂载的文件描述符和传递给子进程的文件描述符不重叠的方式,可以避免这种死锁,比如,在上面的示例之前插入以下内容:

for {
	f, _ := os.Open("/dev/null")
	defer f.Close()
	if f.Fd() > 3 {
		break
	}
}
  1. Go 运行时使用 epoll 系统调用了解哪些 Goroutine 可以响应 I/O。运行时假设 epoll 不会阻塞,但是如果文件位于 FUSE 文件系统上,内核将生成 POLL 操作。为防止这种情况发生,Go-FUSE 在挂载时禁用 POLL 操作码。为确保已经如此,请调用 WaitMount。

2.2.8. 动态地发现文件系统

文件系统数据通常不能全部放入 RAM,因此内核必须动态地发现文件系统:当输入并列出目录内容时,内核向 FUSE 服务询问正在读/写的文件和目录,并且在内存不足时忘记文件系统的部分内容。

动态文件系统的两个重要操作是:

  1. Lookup,NodeLookuper 接口的一部分用于发现目录的各个子目录
  1. Readdir, NodeReaddirer 接口的一部分用于列出目录的内容。

2.2.9. 静态内存文件系统

对于小型的只读文件系统,正确使用 Lookup 的锁定机制非常繁琐,因此 Go-FUSE 提供简化构建此类文件系统的功能。

可以通过 OnAdd 方法构造整个树,而不是动态地发现 FS 树。然后,内存中的树形结构成为实际来源。这意味着 Go-FUSE 必须记住 Inode,即便内核不再对它们感兴趣。可以通过从根节点的 OnAdd 方法实例化“持久的” Inode 的方式实现。参阅 ZipFS 示例,获取可运行示例。