Каждый, кто хоть раз монтировал Google Drive как локальную папку или работал с Windows-разделом из Linux через ntfs-3g, пользовался FUSE, возможно даже не зная об этом. За привычным жестом "открыть папку с удалённым диском" скрывается инфраструктура, которая позволяет любой программе, написанной на Python, Go, Rust или C++, притворяться настоящей файловой системой перед ядром. И делать это без единой строки кода в пространстве ядра.
Это звучит почти как магия. Ядро является закрытой, привилегированной средой. Файловые системы традиционно жили там: ext4, XFS, btrfs написаны в виде модулей ядра и работают с прямым доступом к железу. FUSE не отменяет этот порядок. Он строит мост через него.
Два мира и один канал через который они общаются
Любая Linux-система делит мир на два кольца. Пространство ядра является привилегированной зоной, где живут драйверы, планировщик и файловые системы. Пространство пользователя охватывает всё, что запускается как обычный процесс: браузер, редактор, терминал. Между ними существует жёсткая граница, и любой системный вызов пересекает её, выполняет работу в ядре и возвращает результат обратно.
FUSE состоит из двух частей: модуля ядра fuse.ko и демона в пространстве пользователя. Убедиться, что модуль загружен, можно командой:
lsmod | grep fuse
Модуль регистрирует три типа файловых систем в VFS: fuse, fuseblk и fusectl, а также создаёт символьное устройство /dev/fuse. Это устройство и есть тот канал, через который ядро и пользовательский демон разговаривают друг с другом.
Когда процесс монтирует FUSE-файловую систему, он регистрирует себя как обработчик этой точки монтирования. С этого момента любой системный вызов к файлам внутри неё проходит по следующему маршруту: приложение вызывает read(), write() или stat(), VFS принимает вызов и понимает, что путь ведёт в FUSE-раздел, модуль ядра упаковывает запрос в структуру fuse_request и кладёт его в очередь, процесс-инициатор переходит в состояние ожидания, FUSE-демон читает запрос из /dev/fuse, выполняет свою логику и записывает ответ обратно, ядро будит процесс и возвращает ему результат.
Посмотреть смонтированные FUSE-разделы в системе можно так:
mount | grep fuse
Типичный вывод выглядит примерно так: gvfsd-fuse on /run/user/1000/gvfs type fuse.gvfsd-fuse. Это и есть живой FUSE-демон, обслуживающий файловые операции для GNOME Virtual File System.
Цена четырёх переключений контекста и где она ощущается
Нативная файловая система требует двух переключений контекста на операцию: вход в ядро через системный вызов и возврат обратно. FUSE требует четырёх: приложение переходит в ядро, ядро передаёт управление FUSE-демону, демон возвращает ответ ядру, ядро возвращает результат приложению. Каждое переключение обходится примерно в 1-5 микросекунд.
Замерить это самостоятельно несложно. Команда ниже измеряет латентность stat() на нативном ext4:
strace -T stat /etc/hosts 2>&1 | grep statx
Тот же вызов на SSHFS-разделе занимает на порядок больше, потому что уходит по сети. По данным сравнительных тестов FUSE оказывается примерно на 60-80% медленнее нативных файловых систем для операций с метаданными. Конкретные цифры при последовательном чтении: нативный ext4 выдаёт около 550 МБ/с, FUSE в passthrough-режиме около 420 МБ/с, NTFS-3G порядка 380 МБ/с, SSHFS в локальной сети около 110 МБ/с.
Для рабочих нагрузок с тысячами мелких файлов замедление ощущается физически. Например, запуск rsync на SSHFS-разделе с десятками тысяч файлов будет заметно медленнее, чем на локальном диске, именно из-за оверхеда на каждый stat() и readdir(). Для последовательного чтения крупных файлов, когда в игру вступает кеширование страниц ядра, разница значительно меньше.
Как реализуется FUSE-демон и что нужно написать разработчику
Написание собственной файловой системы через FUSE сводится к реализации набора callback-функций. На Python простейшая read-only файловая система, показывающая один файл hello.txt, выглядит так:
import os
import errno
from fuse import FUSE, Operations
class HelloFS(Operations):
def getattr(self, path, fh=None):
if path == '/':
return {'st_mode': 0o40755, 'st_nlink': 2}
if path == '/hello.txt':
return {'st_mode': 0o100444, 'st_size': 13}
raise OSError(errno.ENOENT, '', path)
def readdir(self, path, fh):
return ['.', '..', 'hello.txt']
def read(self, path, length, offset, fh):
data = b'Hello, FUSE!\n'
return data[offset:offset + length]
if __name__ == '__main__':
FUSE(HelloFS(), '/mnt/hello', foreground=True)
Смонтировать это можно командой:
python3 hello_fs.py
После этого в /mnt/hello появится файл hello.txt, который читается любым приложением как обычный файл. Размонтировать раздел после работы нужно командой:
fusermount -u /mnt/hello
Критически важная деталь: каждый callback должен выбрасывать OSError с соответствующим errno при ошибке. Если getattr возвращает None вместо OSError(errno.ENOENT) для несуществующего пути, команда ls зависнет, потому что ядро будет ждать ответа от демона бесконечно.
На C++ через libfuse структура та же, но разработчик заполняет структуру fuse_operations указателями на функции. Производительность C++-реализации при равной логике значительно выше, поскольку исчезает оверхед интерпретатора. Собрать простой пример на C++ можно так:
gcc -Wall hello_fuse.c $(pkg-config fuse3 --cflags --libs) -o hello_fuse
./hello_fuse /mnt/hello
Google Drive SSHFS и сценарии где FUSE оправдывает медлительность
SSHFS является одним из самых популярных FUSE-инструментов. Он монтирует удалённую директорию по SSH как локальную папку:
sshfs user@remote:/home/user/data /mnt/remote -o reconnect,ServerAliveInterval=15
После этой команды /mnt/remote выглядит как обычная локальная папка. Под капотом каждый read() превращается в SFTP-запрос, который уходит по SSH, возвращается с данными и отдаётся ядру. Производительность ограничена сетью, а не оверхедом FUSE, поэтому замедление от лишних переключений контекста здесь несущественно.
Google Drive через google-drive-ocamlfuse монтируется аналогично:
google-drive-ocamlfuse /mnt/gdrive
Каждый вызов read() транслируется в HTTP-запрос к API Google Drive. Результат буферируется и возвращается ядру. Запись работает в обратную сторону. Пользователь открывает файл в любом редакторе, редактор вызывает стандартный open() и read(), и не знает ничего об HTTP и OAuth2 под капотом.
NTFS-3G является самым известным примером FUSE в production. Смонтировать Windows-раздел с правом записи можно так:
ntfs-3g /dev/sdb1 /mnt/windows -o rw,uid=1000,gid=1000
Присутствует в большинстве Linux-дистрибутивов по умолчанию и обрабатывает миллиарды операций ежедневно на компьютерах с двойной загрузкой.
Оптимизации которые существуют в FUSE и как разработчики борются с задержками
FUSE предоставляет несколько рычагов управления производительностью. Кеширование атрибутов позволяет ядру не ходить к демону за метаданными при каждом обращении. Включить его при монтировании можно так:
./my_fs /mnt/point -o attr_timeout=30,entry_timeout=30
Это означает: кешировать результаты getattr и lookup на 30 секунд. Для read-only файловых систем или редко меняющихся данных это даёт кратный выигрыш в скорости команд вроде ls -la на директориях с тысячами файлов.
Writeback caching накапливает операции записи в буфере ядра и сбрасывает их к демону пачками. Включается опцией:
./my_fs /mnt/point -o writeback_cache
Для FUSE-файловых систем поверх сети это критически важная оптимизация: вместо сотен мелких запросов данные отправляются блоками, и реальная пропускная способность записи вырастает в несколько раз.
Многопоточный режим включается опцией max_threads. Без неё демон обрабатывает запросы последовательно, и параллельные обращения из нескольких процессов встают в очередь. С ней несколько запросов обслуживаются одновременно:
./my_fs /mnt/point -o max_threads=8
На Python многопоточность ограничена GIL, поэтому для реальной параллельности потребуется либо переход на pyfuse3 с asyncio, либо вынести тяжёлую логику в отдельные процессы через multiprocessing.
Где FUSE не подходит и что выбрать в сценариях с высоким IOPS
Базы данных с высоким IOPS, системы с требованиями к латентности в единицы микросекунд, рабочие нагрузки с метаданными, меняющимися тысячи раз в секунду: FUSE в этих задачах не является правильным выбором.
Для случаев, где нужна максимальная производительность с гибкостью userspace, существует virtiofs. Это файловая система для виртуальных машин, которая использует общую память между гостем и хостом вместо /dev/fuse, устраняя большую часть оверхеда переключений контекста. Для контейнерных окружений overlayfs, встроенный в ядро, решает задачу послойных файловых систем значительно эффективнее FUSE-аналогов.
Но для задач, где на первом месте стоит гибкость, FUSE остаётся инструментом без конкурентов по соотношению простоты разработки к функциональности результата. Написать файловую систему, монтирующую S3-бакет, шифрующую данные на лету или представляющую базу данных как дерево файлов, можно за несколько сотен строк кода без каких-либо знаний о внутреннем устройстве ядра. Именно в этой точке пересечения доступности и мощи FUSE живёт уже больше двадцати лет, и ни один из появившихся за это время альтернативных подходов не смог занять его нишу целиком.