Традиционная файловая система в Linux - это модуль ядра. Написать его означает работать в kernel space: никаких привычных библиотек, никакой стандартной отладки, любая ошибка с разыменованием указателя уронит всю систему. Для большинства задач, где нужна файловая система с нестандартной логикой, это неприемлемый порог входа. Именно эту стену сломал FUSE, придуманный Миклошем Середи в 2001 году.

FUSE - Filesystem in Userspace - это мост между ядром и обычным пользовательским процессом. Логика файловой системы живёт в userspace-программе без каких-либо особых привилегий. Ядро маршрутизирует файловые операции через FUSE-модуль к этой программе и передаёт ответ обратно. Для пользователя и для любого приложения смонтированная FUSE-файловая система выглядит и ведёт себя как обычный каталог. Что именно происходит при обращении к файлам - прозрачно для вызывающей стороны.

Архитектура FUSE и путь запроса от пользователя к обработчику

Понять FUSE проще через конкретный сценарий. Пользователь выполняет ls /mnt/myfuse. Это обращение через glibc уходит в системный вызов, попадает в VFS (Virtual File System) ядра. VFS видит, что /mnt/myfuse смонтирована как FUSE-файловая система, и передаёт запрос модулю fuse.ko. Модуль упаковывает запрос и отправляет его через символьное устройство /dev/fuse в userspace-процесс, зарегистрировавшийся как обработчик этой файловой системы. Процесс-обработчик выполняет нужную логику - читает из базы данных, делает HTTP-запрос, формирует список файлов на лету - и возвращает ответ обратно через /dev/fuse. Модуль ядра передаёт ответ в VFS, VFS отдаёт результат вызвавшему процессу.

Весь этот маршрут занимает дополнительный round-trip между ядром и userspace по сравнению с нативной файловой системой. Это главная цена FUSE - каждый системный вызов включает переключение контекста дважды. На практике для большинства сценариев эта цена незаметна: сетевые файловые системы (sshfs, s3fs) лимитированы сетью, архивные файловые системы лимитированы декомпрессией, виртуальные файловые системы лимитированы обращениями к источнику данных.

# Проверить загружен ли модуль FUSE
lsmod | grep fuse

# Загрузить если нет
modprobe fuse

# Установить пакеты для работы с FUSE
apt install fuse3 libfuse3-dev      # Debian/Ubuntu
dnf install fuse3 fuse3-devel       # Fedora/RHEL

# Посмотреть активные FUSE-соединения
ls /sys/fs/fuse/connections/
cat /sys/fs/fuse/connections/*/waiting

Файл waiting в директории соединения показывает число запросов, ожидающих обработки userspace-демоном. Если это значение постоянно ненулевое при активной нагрузке - демон не успевает обрабатывать очередь.

Готовые FUSE-реализации как рабочие инструменты

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

sshfs превращает удалённую директорию на любом SSH-сервере в локально смонтированный каталог. Это один из наиболее распространённых способов применения FUSE в повседневной работе:

apt install sshfs

# Смонтировать удалённый каталог локально
sshfs Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.:/path/to/dir /mnt/remote

# С явным указанием порта и ключа
sshfs -p 2222 -o IdentityFile=~/.ssh/id_ed25519 \
  user@remote:/data /mnt/remote

# Смонтировать только для текущего пользователя без root
sshfs user@host:/home/user /home/localuser/remote -o uid=$(id -u)

# Размонтировать
fusermount3 -u /mnt/remote

s3fs монтирует Amazon S3-совместимые хранилища как локальные каталоги:

apt install s3fs

# Сохранить credentials
echo "ACCESS_KEY:SECRET_KEY" > ~/.passwd-s3fs
chmod 600 ~/.passwd-s3fs

# Смонтировать bucket
s3fs mybucket /mnt/s3 -o passwd_file=~/.passwd-s3fs \
  -o url=https://s3.amazonaws.com

# Для MinIO или другого S3-совместимого хранилища
s3fs mybucket /mnt/s3 -o passwd_file=~/.passwd-s3fs \
  -o url=https://minio.example.com \
  -o use_path_request_style

archivemount открывает содержимое архивов как файловую систему без распаковки:

apt install archivemount

# Смонтировать tar.gz архив
archivemount backup.tar.gz /mnt/archive

# Просмотреть содержимое и скопировать нужные файлы
ls /mnt/archive
cp /mnt/archive/important-file.txt ~/

fusermount3 -u /mnt/archive

Права доступа и опции монтирования для FUSE-файловых систем

По умолчанию FUSE-файловая система, смонтированная непривилегированным пользователем, видна только ему. Это намеренное ограничение безопасности: FUSE-демон работает с правами монтирующего пользователя, и нет гарантий, что он корректно реализует разграничение прав для других пользователей.

Ряд важных опций монтирования:

# allow_other - разрешить доступ другим пользователям (требует /etc/fuse.conf)
# allow_root - разрешить доступ только root
# default_permissions - делегировать проверку прав ядру по стандартным битам
# nonempty - монтировать в непустой каталог
# ro - только для чтения

sshfs user@host:/data /mnt/remote -o allow_other,default_permissions,ro

# Разрешить непривилегированным пользователям использовать allow_other
echo "user_allow_other" >> /etc/fuse.conf

Для монтирования при загрузке через /etc/fstab FUSE-записи используют специальный синтаксис:

# Добавить в /etc/fstab для автомонтирования sshfs
user@host:/remote  /mnt/remote  fuse.sshfs  \
  noauto,x-systemd.automount,_netdev,users,\
  IdentityFile=/home/user/.ssh/id_ed25519,\
  allow_other,default_permissions  0  0

Опция x-systemd.automount создаёт automount-юнит systemd: файловая система монтируется при первом обращении, а не при загрузке, что удобно для сетевых FUSE-файловых систем.

Структура FUSE-обработчика и что нужно реализовать

Когда готовые решения не закрывают задачу, FUSE-обработчик пишется самостоятельно. Порог входа намеренно снижен: libfuse предоставляет два API. Высокоуровневый API работает с именами файлов и путями, низкоуровневый - с инодами. Для большинства задач достаточно высокоуровневого.

Минимальная рабочая файловая система на C реализует структуру fuse_operations с нужными обработчиками. Пример файловой системы, которая предоставляет один файл с динамически генерируемым содержимым:

#define FUSE_USE_VERSION 35
#include <fuse3/fuse.h>
#include <string.h>
#include <errno.h>
#include <time.h>

static const char *content = "Hello from FUSE\n";
static const char *filename = "/hello";

static int my_getattr(const char *path, struct stat *st,
                      struct fuse_file_info *fi) {
    memset(st, 0, sizeof(struct stat));
    if (strcmp(path, "/") == 0) {
        st->st_mode = S_IFDIR | 0755;
        st->st_nlink = 2;
    } else if (strcmp(path, filename) == 0) {
        st->st_mode = S_IFREG | 0444;
        st->st_nlink = 1;
        st->st_size = strlen(content);
    } else {
        return -ENOENT;
    }
    return 0;
}

static int my_readdir(const char *path, void *buf,
                      fuse_fill_dir_t filler, off_t offset,
                      struct fuse_file_info *fi,
                      enum fuse_readdir_flags flags) {
    filler(buf, ".", NULL, 0, 0);
    filler(buf, "..", NULL, 0, 0);
    filler(buf, "hello", NULL, 0, 0);
    return 0;
}

static int my_read(const char *path, char *buf, size_t size,
                   off_t offset, struct fuse_file_info *fi) {
    size_t len = strlen(content);
    if (offset >= (off_t)len) return 0;
    if (offset + size > len) size = len - offset;
    memcpy(buf, content + offset, size);
    return size;
}

static const struct fuse_operations ops = {
    .getattr = my_getattr,
    .readdir = my_readdir,
    .read    = my_read,
};

int main(int argc, char *argv[]) {
    return fuse_main(argc, argv, &ops, NULL);
}

Компиляция и запуск:

gcc -Wall hello_fuse.c -o hello_fuse $(pkg-config fuse3 --cflags --libs)

mkdir /tmp/myfuse
./hello_fuse /tmp/myfuse

cat /tmp/myfuse/hello
ls -la /tmp/myfuse

fusermount3 -u /tmp/myfuse

Производительность FUSE и когда overhead становится значимым

Накладные расходы FUSE складываются из двух переключений контекста на каждый системный вызов и накладных расходов на копирование данных через /dev/fuse. Для операций с данными (read, write) ядро FUSE поддерживает режим прямого ввода-вывода и запись в несколько потоков, что существенно снижает overhead.

Проверить параметры производительности активной FUSE-файловой системы:

# Размер ядерного буфера для FUSE-операций (обычно 128KB)
cat /sys/fs/fuse/connections/*/max_write
cat /sys/fs/fuse/connections/*/max_read

# Число одновременных запросов в очереди
cat /sys/fs/fuse/connections/*/max_background

# Замер производительности через fio
fio --name=fuse-test --filename=/mnt/myfuse/testfile \
  --rw=randread --bs=4k --size=1G --numjobs=4 --runtime=30

FUSE 3 принёс ряд оптимизаций производительности: поддержку параллельных запросов, кэширование атрибутов, улучшенный writeback cache. Для файловых систем с интенсивным IO разница между FUSE 2 и FUSE 3 с включённым writeback_cache может достигать 30-50% по пропускной способности.

# Включить writeback cache при монтировании
./my_fuse /mnt/point -o writeback_cache

# Включить параллельную обработку запросов
./my_fuse /mnt/point -o parallel_direct_writes

Отладка и диагностика FUSE-файловых систем

Обработчик FUSE запускается как обычный userspace-процесс, что делает его отладку значительно проще, чем отладку модуля ядра. gdb, valgrind, strace - всё работает штатно:

# Запустить в режиме отладки с выводом в stdout (без демонизации)
./my_fuse /mnt/point -f -d

# Трассировать системные вызовы обработчика
strace -p $(pgrep my_fuse) -e trace=read,write,ioctl

# Профилировать производительность обработчика
perf record -g -p $(pgrep my_fuse) -- sleep 30
perf report

# Проверить статистику через /proc
cat /proc/$(pgrep my_fuse)/status
cat /proc/$(pgrep my_fuse)/io

Флаг -d включает отладочный режим libfuse с выводом каждого входящего запроса и ответа. На экране появятся строки вида LOOKUP /path, GETATTR /file, READ offset=0 size=4096 - полная картина того, как ядро общается с обработчиком. Это незаменимо при реализации нового обработчика: видно каждое несоответствие между ожидаемым и реальным поведением.

FUSE стёр границу между "написать утилиту" и "написать файловую систему". Доступ к S3 как к каталогу, редактирование строк базы данных как текстовых файлов, монтирование зашифрованного контейнера, работа с удалённым сервером через привычный cp и vim - всё это доступно без единой строки кода ядра. Понимание архитектуры FUSE превращает эти возможности из магии в инструмент с предсказуемыми характеристиками и понятными ограничениями.