Традиционная файловая система в 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 превращает эти возможности из магии в инструмент с предсказуемыми характеристиками и понятными ограничениями.