AI FUSE + Go: ковка собственной виртуальной файловой системы на блочном устройстве

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 822
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1


В этой статье подробно разбирается создание пользовательской файловой системы с помощью FUSE и языка Go. На реальном примере мы пройдём путь от установки окружения до реализации чтения, записи, метаданных и параллельного доступа. В процессе встретятся живые комментарии, личные наблюдения и советы, которые помогут избежать распространённых подводных камней.

Введение
Давно хотел понять, как сделать “файловую систему в файле” или на блочном устройстве, чтобы потом подключить её к любому Linux-серверу. Оказалось, что комбинация FUSE и Go — отличный вариант для быстрой прототипировки без костылей на C. В этой статье я расскажу о своём опыте, забавных факапах и главных открытиях на пути к рабочей системе.

Почему FUSE и Go — идеальный дуэт


FUSE (Filesystem in Userspace) позволяет запускать код файловой системы в пространстве пользователя, не лезя в ядро. Go же приносит удобную модель конкурентности, сборщик мусора и понятный синтаксис. Вместе они дают возможность писать надёжный код в сотни строк, а не в тысячи.

Подготовка окружения и зависимости


На Ubuntu/Debian всё просто:

sudo apt update
sudo apt install golang-go libfuse-dev
export GO111MODULE=on
mkdir -p ~/go/src/fusefs && cd ~/go/src/fusefs
go mod init github.com/you/fusefs
go get bazil.org/fuse
go get bazil.org/fuse/fs

Здесь bazil.org/fuse — наиболее “гуёвый” биндинг к libfuse.

Черновой набросок проекта


Создаём файл main.go с минимальным кодом:

// main.go
package main

import (
"bazil.org/fuse"
"bazil.org/fuse/fs"
"context"
"log"
)

func main() {
conn, err := fuse.Mount(
"/mnt/fusefs",
fuse.FSName("fusefs"),
fuse.Subtype("customfs"),
fuse.LocalVolume(),
fuse.VolumeName("GoFUSE"),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()

err = fs.Serve(conn, FS{})
if err != nil {
log.Fatal(err)
}
}

Здесь FS{} — наш корневой узел, который реализует интерфейс fs.FS.

Реализация корневого узла и каталогов


Немного магии — делаем корневой каталог:

type FS struct{}

func (FS) Root() (fs.Node, error) {
return &Dir{
entries: map[string]fs.Node{
"hello.txt": &File{data: []byte("Привет, FUSE + Go!\n")},
},
}, nil
}

type Dir struct {
entries map[string]fs.Node
}

func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) error {
a.Mode = os.ModeDir | 0o755
return nil
}

func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
if node, ok := d.entries[name]; ok {
return node, nil
}
return nil, fuse.ENOENT
}

func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
var list []fuse.Dirent
for name := range d.entries {
list = append(list, fuse.Dirent{Name: name})
}
return list, nil
}

Таким образом мы описали каталог с одной текстовой “заглушкой”.

Реализация файловых операций (чтение/запись)


Добавим в File методы чтения:

type File struct {
data []byte
mu sync.Mutex
}

func (f *File) Attr(ctx context.Context, a *fuse.Attr) error {
a.Mode = 0o644
a.Size = uint64(len(f.data))
return nil
}

func (f *File) ReadAll(ctx context.Context) ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.data, nil
}

Для записи надо внедрить интерфейс fs.HandleWriter:

func (f *File) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error {
f.mu.Lock()
defer f.mu.Unlock()
end := int(req.Offset) + len(req.Data)
if end > len(f.data) {
newData := make([]byte, end)
copy(newData, f.data)
f.data = newData
}
copy(f.data[req.Offset:], req.Data)
resp.Size = len(req.Data)
return nil
}

Теперь после монтирования можно echo "test" > /mnt/fusefs/hello.txt и читать обратно.

Обработка метаданных и времён


Часто нужно указывать Atime, Mtime, Ctime. Добавим их:

func (f *File) Attr(ctx context.Context, a *fuse.Attr) error {
a.Mode = 0o644
a.Size = uint64(len(f.data))
a.Atime = time.Now()
a.Mtime = time.Now()
a.Ctime = time.Now()
return nil
}

Но по-честному стоит хранить времена в структуре и обновлять их при записи.

Параллельный доступ и конкурентность


Go отлично управляет конкурентными запросами, но нужно не забывать про мьютексы:

type SafeFile struct {
data []byte
mu sync.RWMutex
}

// В ReadAll используем RLock, а в Write — Lock.

Без этого при стресс-тестах получим расслоение данных или паники.

Производительность: буферизация и кеш


FUSE по умолчанию делает много системных вызовов. Чтобы ускорить:


  • Реализовать блоки и кешировать их в памяти.


  • Использовать fuse.WritebackCache() при монтировании.


  • Оптимизировать ReadAll на большие файлы, отпочковывая req.Offset и req.Size.
Ломаем и чинить: типичные ошибки


  1. EBUSY при монтировании — не размонтировали старый инстанс.


  2. Проблемы с правами — проверяйте опции монтирования: fusermount -u /mnt/fusefs.


  3. Падения из‑за неправильного Attr — следите, чтобы поля структуры fuse.Attr были корректными.
Расширение: снапшоты и снапшот‑директории


Можно хранить снимки состояния:

type SnapshotDir struct {
parent *Dir
snap []byte // сериализованный дамп
}

Выгружать образ в файл и потом монтировать его как виртуальное устройство.

Резюме по опыту


Первые полдня я пытался переписать пример с C, потом забросил и сделал на Go ещё за 2 часа. Главное — не бояться экспериментов и не лезть сразу в оптимизацию без профилировщика.
 
Сверху Снизу