手写迷你文件系统:基于 FUSE + Go 实现自定义 FS
目录
学习文件系统最好的方式就是自己写一个。本文使用 FUSE + Go,实现一个支持基本读写操作的内存文件系统,过程中深入理解 inode、block 等核心概念。
一、为什么要手写文件系统?
1.1 学习目的
理解文件系统,是理解以下技术的基础:
- 容器存储卷 (Volume)
- 分布式存储 (Ceph, GlusterFS)
- 对象存储 (S3 FUSE)
- 数据库存储引擎
1.2 FUSE 的优势
FUSE (Filesystem in Userspace) 允许在用户态实现文件系统:
- 不需要写内核模块
- 开发调试方便
- 可以用高级语言 (Go, Python, Rust)
二、文件系统核心概念
2.1 Inode
Inode (Index Node) 是文件系统的核心数据结构,存储文件的元数据:
type Inode struct {
ID uint64 // inode 编号
Type InodeType // 文件 or 目录
Size uint64 // 文件大小
Mode os.FileMode // 权限
Uid, Gid uint32 // 所有者
Atime time.Time // 访问时间
Mtime time.Time // 修改时间
Ctime time.Time // 创建时间
Links uint32 // 硬链接数
Blocks []uint64 // 数据块索引
}关键点:
- 文件名不在 inode 里!文件名在目录项中
- 一个 inode 可以有多个文件名(硬链接)
ls -i可以查看 inode 编号
2.2 Block
Block 是存储数据的基本单位,通常 4KB:
const BlockSize = 4096
type Block struct {
Data [BlockSize]byte
}文件的内容被切分成多个 Block 存储。
2.3 目录
目录本质上也是一个文件,内容是"目录项"列表:
type DirEntry struct {
Name string // 文件名
Inode uint64 // 指向的 inode
}
// 目录的内容就是 []DirEntry2.4 关系示意
┌─────────────────────────────────────────┐
│ Superblock │
│ (文件系统元信息: block 数量, inode 数量) │
└─────────────────────────────────────────┘
│
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Inode 1 │ │ Inode 2 │ │ Inode 3 │
│ (根目录) │ │ (文件a) │ │ (文件b) │
│ Type=Dir│ │ Type=Reg│ │ Type=Reg│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Block 0 │ │ Block 1 │ │ Block 2 │
│ DirEntry│ │ 文件内容 │ │ 文件内容 │
│ a→Inode2│ │ "hello" │ │ "world" │
│ b→Inode3│ └─────────┘ └─────────┘
└─────────┘三、FUSE 架构
3.1 工作原理
┌──────────────────────────────────────────────────────────┐
│ User Space │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Your App │───▶│ glibc │───▶│ FUSE 用户态程序 │ │
│ │ (cat, ls)│ │ open/read│ │ (你写的代码) │ │
│ └──────────┘ └────┬─────┘ └────────▲─────────┘ │
│ │ │ │
└───────────────────────│────────────────────│──────────────┘
│ VFS │ /dev/fuse
▼ │
┌───────────────────────────────────────────────────────────┐
│ Kernel │
│ ┌──────────────────────────────────────────────────┐ │
│ │ FUSE 内核模块 │ │
│ │ (把 VFS 调用转发到用户态,再把结果返回) │ │
│ └──────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘3.2 需要实现的操作
| 操作 | 对应系统调用 | 说明 |
|---|---|---|
| Lookup | open(dir) | 在目录中查找文件 |
| Getattr | stat | 获取文件属性 |
| Readdir | readdir | 列出目录内容 |
| Read | read | 读取文件内容 |
| Write | write | 写入文件内容 |
| Create | creat/open | 创建新文件 |
| Mkdir | mkdir | 创建目录 |
| Unlink | unlink | 删除文件 |
| Rmdir | rmdir | 删除目录 |
四、Go 实现
4.1 依赖
使用 bazil.org/fuse 库:
go get bazil.org/fuse4.2 数据结构
package main
import (
"os"
"sync"
"time"
)
type InodeType int
const (
TypeFile InodeType = iota
TypeDir
)
type MemFS struct {
mu sync.RWMutex
inodes map[uint64]*Inode
data map[uint64][]byte // inode -> 文件内容
nextIno uint64
}
type Inode struct {
ID uint64
Type InodeType
Mode os.FileMode
Size uint64
Atime time.Time
Mtime time.Time
Children map[string]uint64 // 目录项: 名字 -> inode
}
func NewMemFS() *MemFS {
fs := &MemFS{
inodes: make(map[uint64]*Inode),
data: make(map[uint64][]byte),
nextIno: 2,
}
// 创建根目录 (inode 1)
fs.inodes[1] = &Inode{
ID: 1,
Type: TypeDir,
Mode: os.ModeDir | 0755,
Atime: time.Now(),
Mtime: time.Now(),
Children: make(map[string]uint64),
}
return fs
}4.3 实现 FUSE 接口
import (
"bazil.org/fuse"
"bazil.org/fuse/fs"
"context"
)
// Dir 实现目录节点
type Dir struct {
fs *MemFS
inode *Inode
}
func (d *Dir) Attr(ctx context.Context, attr *fuse.Attr) error {
attr.Inode = d.inode.ID
attr.Mode = d.inode.Mode
attr.Atime = d.inode.Atime
attr.Mtime = d.inode.Mtime
return nil
}
func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
d.fs.mu.RLock()
defer d.fs.mu.RUnlock()
childIno, ok := d.inode.Children[name]
if !ok {
return nil, fuse.ENOENT
}
child := d.fs.inodes[childIno]
if child.Type == TypeDir {
return &Dir{fs: d.fs, inode: child}, nil
}
return &File{fs: d.fs, inode: child}, nil
}
func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
d.fs.mu.RLock()
defer d.fs.mu.RUnlock()
var entries []fuse.Dirent
for name, ino := range d.inode.Children {
child := d.fs.inodes[ino]
var t fuse.DirentType
if child.Type == TypeDir {
t = fuse.DT_Dir
} else {
t = fuse.DT_File
}
entries = append(entries, fuse.Dirent{
Inode: ino,
Name: name,
Type: t,
})
}
return entries, nil
}
func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) {
d.fs.mu.Lock()
defer d.fs.mu.Unlock()
ino := d.fs.nextIno
d.fs.nextIno++
now := time.Now()
inode := &Inode{
ID: ino,
Type: TypeFile,
Mode: req.Mode,
Atime: now,
Mtime: now,
}
d.fs.inodes[ino] = inode
d.fs.data[ino] = []byte{}
d.inode.Children[req.Name] = ino
file := &File{fs: d.fs, inode: inode}
return file, file, nil
}
func (d *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) {
d.fs.mu.Lock()
defer d.fs.mu.Unlock()
ino := d.fs.nextIno
d.fs.nextIno++
now := time.Now()
inode := &Inode{
ID: ino,
Type: TypeDir,
Mode: req.Mode | os.ModeDir,
Atime: now,
Mtime: now,
Children: make(map[string]uint64),
}
d.fs.inodes[ino] = inode
d.inode.Children[req.Name] = ino
return &Dir{fs: d.fs, inode: inode}, nil
}4.4 文件操作
// File 实现文件节点
type File struct {
fs *MemFS
inode *Inode
}
func (f *File) Attr(ctx context.Context, attr *fuse.Attr) error {
attr.Inode = f.inode.ID
attr.Mode = f.inode.Mode
attr.Size = f.inode.Size
attr.Atime = f.inode.Atime
attr.Mtime = f.inode.Mtime
return nil
}
func (f *File) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
f.fs.mu.RLock()
defer f.fs.mu.RUnlock()
data := f.fs.data[f.inode.ID]
if req.Offset >= int64(len(data)) {
return nil
}
end := req.Offset + int64(req.Size)
if end > int64(len(data)) {
end = int64(len(data))
}
resp.Data = data[req.Offset:end]
return nil
}
func (f *File) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error {
f.fs.mu.Lock()
defer f.fs.mu.Unlock()
data := f.fs.data[f.inode.ID]
// 扩展文件大小
newLen := int(req.Offset) + len(req.Data)
if newLen > len(data) {
newData := make([]byte, newLen)
copy(newData, data)
data = newData
}
copy(data[req.Offset:], req.Data)
f.fs.data[f.inode.ID] = data
f.inode.Size = uint64(len(data))
f.inode.Mtime = time.Now()
resp.Size = len(req.Data)
return nil
}4.5 主程序
func main() {
mountpoint := "/tmp/memfs"
os.MkdirAll(mountpoint, 0755)
c, err := fuse.Mount(mountpoint, fuse.FSName("memfs"), fuse.Subtype("memfs"))
if err != nil {
log.Fatal(err)
}
defer c.Close()
fmt.Printf("已挂载到 %s\n", mountpoint)
fmt.Println("按 Ctrl+C 退出")
memfs := NewMemFS()
// 处理信号,优雅卸载
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fuse.Unmount(mountpoint)
}()
err = fs.Serve(c, &FS{memfs: memfs})
if err != nil {
log.Fatal(err)
}
}
type FS struct {
memfs *MemFS
}
func (f *FS) Root() (fs.Node, error) {
return &Dir{fs: f.memfs, inode: f.memfs.inodes[1]}, nil
}五、测试运行
# 编译运行
go build -o memfs
./memfs &
# 测试文件操作
cd /tmp/memfs
echo "hello world" > test.txt
cat test.txt
mkdir subdir
ls -la
# 卸载
fusermount -u /tmp/memfs六、进阶扩展
6.1 持久化到磁盘
// 将 inode 和 data 序列化到磁盘
func (fs *MemFS) SaveToDisk(path string) error {
f, _ := os.Create(path)
defer f.Close()
return gob.NewEncoder(f).Encode(fs)
}
func LoadFromDisk(path string) (*MemFS, error) {
f, _ := os.Open(path)
defer f.Close()
var fs MemFS
err := gob.NewDecoder(f).Decode(&fs)
return &fs, err
}6.2 网络文件系统
// 将读写操作转发到远程服务器
func (f *File) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
// 通过 gRPC 从远程读取
data, err := f.client.ReadFile(ctx, &pb.ReadRequest{
Inode: f.inode.ID,
Offset: req.Offset,
Size: int64(req.Size),
})
resp.Data = data.Content
return err
}七、总结
| 概念 | 实现 |
|---|---|
| Inode | 结构体存储元数据 |
| Block | byte 切片存储数据 |
| 目录 | map[name]inode |
| FUSE | 实现 Attr/Read/Write 等接口 |
收获:
- 深入理解了 inode 和目录项的关系
- 了解了 VFS 到 FUSE 的调用链路
- 为理解分布式存储打下基础
相关文章