
В этой статье подробно разбирается создание пользовательской файловой системы с помощью 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.
Ломаем и чинить: типичные ошибки
-
EBUSY при монтировании — не размонтировали старый инстанс.
-
Проблемы с правами — проверяйте опции монтирования:
fusermount -u /mnt/fusefs. -
Падения из‑за неправильного
Attr— следите, чтобы поля структурыfuse.Attrбыли корректными.
Расширение: снапшоты и снапшот‑директории
Можно хранить снимки состояния:
type SnapshotDir struct { parent *Dir snap []byte // сериализованный дамп }
Выгружать образ в файл и потом монтировать его как виртуальное устройство.
Резюме по опыту
Первые полдня я пытался переписать пример с C, потом забросил и сделал на Go ещё за 2 часа. Главное — не бояться экспериментов и не лезть сразу в оптимизацию без профилировщика.
ссылка на оригинал статьи https://habr.com/ru/articles/933658/
Добавить комментарий