Маршрутизатор, промышленный контроллер, умная камера - всё это устройства, в которых флэш-память одновременно и дорога, и уязвима. Каждый цикл записи сокращает ресурс NAND-чипа, каждый внезапный сброс питания грозит повреждением корневой файловой системы. Инженеры, проектирующие подобные устройства, рано или поздно приходят к одному и тому же решению: корневая файловая система должна быть сжатой, неизменяемой и устойчивой к отключению питания. SquashFS закрывает эту задачу лучше, чем что-либо другое в мире Linux. А там, где нужна и запись - в игру вступает OverlayFS.
Прежде чем браться за инструменты, полезно понять, что именно делает SquashFS привлекательным на фоне конкурентов. Ещё недавно альтернативами служили cramfs и romfs, но оба формата давно устарели и практически вышли из активного применения. SquashFS поддерживается ядром начиная с версии 2.6.29, размер файловой системы ограничен значением 2^64 байт, максимальный размер одного файла составляет 2 ТиБ. Файловая система умеет экспортироваться через NFS, поддерживает расширенные атрибуты xattr и сжимает не только данные файлов, но и метаданные - таблицы inode и директорий. Средний сжатый inode занимает около 8 байт.
Почему read-only root - это не ограничение, а архитектурное решение
Интуитивно кажется, что файловая система только для чтения - это компромисс, вынужденная жертва ради экономии места. На самом деле всё ровно наоборот. Неизменяемый корень означает, что система после каждой перезагрузки находится в точно известном, верифицированном состоянии. Никаких накопленных конфигурационных мутаций, никаких полуустановленных пакетов, никаких следов сбойного обновления. Каждый запуск - чистый лист поверх гарантированно корректного образа.
Для встраиваемых устройств это принципиально. Если питание пропало в момент записи - SquashFS-образ не пострадал, он физически не менялся. Единственное, что требует защиты при таком подходе - это writable-слой поверх образа, и этот слой можно сделать небольшим, быстрым и легко заменяемым.
SquashFS при этом использует собственные кэши для метаданных и фрагментов, тогда как блоки данных файлов декомпрессируются и кэшируются через стандартный page cache ядра. Это означает, что повторно запрошенные данные не декомпрессируются заново - они живут в памяти как обычные страницы.
Алгоритмы сжатия - выбор под конкретный процессор и флэш
SquashFS поддерживает шесть алгоритмов сжатия: gzip, lzo, lz4, xz, lzma и zstd. Каждый из них занимает свою нишу, и выбор между ними - это не вопрос вкуса, а инженерный расчёт.
Вот как выглядит сравнение алгоритмов по ключевым характеристикам на реальных данных:
Самый скромный по плотности - lz4, он даёт примерно двукратное сжатие (~2.1x), зато декомпрессия летит на ~218 МБ/с. Gzip и zstd уровня 5 сжимают сопоставимо - около ~2.9x, но zstd при этом быстрее: ~198 МБ/с против ~128 МБ/с у gzip. Стоит поднять zstd до уровня 15 - и картина меняется разительно: сжатие дотягивается до ~3.1x, а скорость декомпрессии вырастает до ~224 МБ/с. xz держит пальму первенства по плотности (~3.4x), но платит за это скоростью - всего ~35-40 МБ/с на декомпрессии.
Цифры показывают неочевидный факт: zstd на уровнях 10-15 обгоняет xz по скорости декомпрессии почти в 6 раз, при этом проигрывая ему по плотности лишь на 8-10%. Для ARM-процессора с тактовой частотой 400-600 МГц разница в скорости декомпрессии между xz и zstd ощущается как разница между приемлемой загрузкой и раздражающей задержкой при каждом обращении к файлу.
lz4 - полная противоположность xz. Степень сжатия у него скромная, но скорость декомпрессии достигает ~218 МБ/с. Это выбор для систем, где время старта критично, а флэш-памяти достаточно. На очень старых одноядерных процессорах без аппаратных ускорителей lz4 нередко оказывается единственным практичным вариантом.
gzip остаётся разумным выбором по умолчанию там, где нужна гарантированная совместимость с любым ядром, включая патчи для очень старых версий.
Ключевые опции mksquashfs - от базового образа до тонкой настройки
Установка инструментов:
# Debian/Ubuntu
sudo apt install squashfs-tools
# Fedora/RHEL
sudo dnf install squashfs-tools
Базовая команда для создания образа корневой файловой системы из директории /rootfs:
mksquashfs /rootfs rootfs.sqfs \
-comp zstd \
-Xcompression-level 15 \
-b 256K \
-e /rootfs/proc \
-e /rootfs/sys \
-e /rootfs/dev \
-e /rootfs/tmp \
-e /rootfs/run \
-e rootfs.sqfs
Флаг -e исключает псевдофайловые системы и сам создаваемый образ - это обязательный шаг, иначе mksquashfs попытается запаковать в образ proc или sys, что не имеет смысла и может привести к неожиданным результатам.
Параметр -b 256K увеличивает размер блока сжатия с дефолтных 128 КиБ до 256 КиБ. Больший блок даёт лучшую степень сжатия за счёт того, что компрессор видит больший контекст данных, но при случайном доступе к небольшому файлу придётся декомпрессировать больший блок. Для последовательного чтения - однозначный выигрыш, для сценариев с частым случайным доступом - нужно тестировать конкретно.
Принудительная нормализация прав и владельцев (важно при кросс-компиляции, когда образ собирается на хост-машине с другими uid/gid):
mksquashfs /rootfs rootfs.sqfs \
-comp zstd \
-Xcompression-level 10 \
-force-uid root \
-force-gid root \
-noappend \
-e /rootfs/proc /rootfs/sys /rootfs/dev /rootfs/tmp
Флаг -noappend принципиален при повторной сборке: без него mksquashfs добавит данные к существующему образу поверх старого содержимого, а не пересоздаст его.
Проверить содержимое образа без распаковки:
unsquashfs -l rootfs.sqfs | head -50
Проверить статистику сжатия:
unsquashfs -stat rootfs.sqfs
Вывод покажет точные размеры сжатого и несжатого образа, процент компрессии, количество инодов и фрагментов. Эту информацию полезно сохранять рядом с образом при каждой сборке.
OverlayFS - слой записи поверх неизменяемого ядра
Смонтировать SquashFS в режиме только для чтения просто:
mount -t squashfs -o ro rootfs.sqfs /mnt/root
Но система, которая не может писать даже в /tmp или /var/run, практически неработоспособна. Именно здесь OverlayFS становится недостающим звеном. OverlayFS - это объединяющая файловая система: она представляет пользователю единый вид, который является результатом наложения одной файловой системы поверх другой. Нижний слой (lowerdir) - неизменяемый SquashFS, верхний (upperdir) - writable tmpfs или отдельный ext4-раздел.
Три правила OverlayFS, которые нужно знать до того, как писать первый mount:
- workdir обязан находиться на той же файловой системе, что и upperdir, и должен быть пустым при монтировании.
- upperdir должна поддерживать создание расширенных атрибутов trusted.* - именно через них OverlayFS хранит whiteout-маркеры и метаданные copy-up.
- NFS не подходит в роли upperdir из-за ограничений на d_type в ответах readdir.
Классическая схема для встраиваемой системы с tmpfs как writable-слоем:
# Монтируем SquashFS как нижний слой
mount -t squashfs -o ro /dev/mtdblock0 /mnt/lower
# Создаём необходимые директории в tmpfs
mount -t tmpfs tmpfs /mnt/rw
mkdir -p /mnt/rw/upper /mnt/rw/work
# Монтируем объединённую файловую систему
mount -t overlay overlay \
-o lowerdir=/mnt/lower,upperdir=/mnt/rw/upper,workdir=/mnt/rw/work \
/mnt/root
После такого монтирования всё, что записывается в /mnt/root, попадает в /mnt/rw/upper. Оригинальный SquashFS-образ остаётся нетронутым. При перезагрузке tmpfs исчезает - система возвращается в исходное состояние.
Когда файл из нижнего (read-only) слоя модифицируется, происходит copy-up: файл сначала копируется из lowerdir в upperdir, и все последующие изменения применяются уже к этой копии. Оригинал в lowerdir остаётся нетронутым. Операция copy-up выполняется один раз на файл - следующие обращения к уже скопированному файлу работают напрямую с upperdir без накладных расходов.
При удалении файла из lowerdir OverlayFS не трогает оригинал. Вместо этого в upperdir создаётся специальный whiteout-файл - символический маркер, который говорит объединённой файловой системе игнорировать соответствующую запись в нижнем слое. Аналогично работает и удаление директорий: вместо whiteout создаётся "непрозрачная" директория, полностью скрывающая содержимое lowerdir по тому же пути.
Постоянный writable-слой и схема с несколькими lowerdir
Если изменения должны сохраняться между перезагрузками, tmpfs заменяется постоянным разделом. Разумная схема разбивки флэша для роутера или промышленного устройства выглядит так:
/dev/mtdblock0 -> SquashFS (rootfs, read-only, ~50-200 МБ)
/dev/mtdblock1 -> ext4 или f2fs (writable overlay, ~32-128 МБ)
Сборка postоянного overlay при старте системы через init-скрипт:
#!/bin/sh
# /etc/init.d/overlay-mount
LOWER=/mnt/lower
UPPER=/mnt/persist/upper
WORK=/mnt/persist/work
MERGED=/
mount -t squashfs -o ro /dev/mtdblock0 $LOWER
mount -t ext4 /dev/mtdblock1 /mnt/persist
# Создаём директории, если их нет
mkdir -p $UPPER $WORK
mount -t overlay overlay \
-o lowerdir=$LOWER,upperdir=$UPPER,workdir=$WORK \
$MERGED
OverlayFS поддерживает несколько нижних слоёв, разделённых двоеточием. Это удобно, когда образ системы состоит из нескольких компонентов - базового rootfs и дополнительных пакетов-оверлеев:
mount -t overlay overlay \
-o lowerdir=/mnt/packages:/mnt/base,upperdir=/mnt/rw/upper,workdir=/mnt/rw/work \
/mnt/root
Порядок слоёв важен: правило "левый выше правого" означает, что файл из /mnt/packages перекрывает одноимённый файл из /mnt/base. Верхний writable-слой перекрывает всех.
Нижняя файловая система не обязана быть writable. Нижним слоем может быть даже другой overlayfs - это открывает возможность создавать цепочки слоёв произвольной глубины.
Проверка конфигурации и диагностика
Убедиться, что SquashFS поддерживается текущим ядром:
zcat /proc/config.gz | grep -i squash
# или
grep -i squash /boot/config-$(uname -r)
Нужная строка: CONFIG_SQUASHFS=y (вкомпилировано) или CONFIG_SQUASHFS=m (модуль). Поддержку конкретных алгоритмов сжатия показывают строки CONFIG_SQUASHFS_XZ, CONFIG_SQUASHFS_ZSTD и аналогичные.
Загрузить модуль overlay, если он не загружен автоматически:
modprobe overlay
lsmod | grep overlay
Просмотр активных overlay-монтирований:
mount | grep overlay
# или подробнее
findmnt -t overlay
Вывод findmnt компактно показывает дерево монтирований с параметрами lowerdir, upperdir и workdir - гораздо удобнее, чем парсить вывод mount вручную. Если нужно узнать, какой файл реально обслуживает запрос - из lowerdir или уже скопирован в upperdir:
# Файл в merged: где он физически?
stat /mnt/root/etc/hostname
# Содержимое upperdir (все изменения с момента монтирования)
find /mnt/rw/upper -type f
find /mnt/rw/upper -name '.wh.*' # whiteout-маркеры удалённых файлов
Whiteout-файлы начинаются с префикса .wh. - например, .wh.hostname означает, что файл hostname был удалён в merged-представлении, хотя физически продолжает существовать в lowerdir.
Связка SquashFS и OverlayFS - это не просто техническое ухищрение для экономии нескольких мегабайт флэша. Это архитектурный принцип, который формирует принципиально иное отношение к надёжности системы. Корневая файловая система превращается из изменяемого состояния в неизменяемый эталон, а writable-слой становится тонкой и контролируемой оберткой поверх него. Обновление системы при таком подходе - это не "патч поверх патча", а замена одного образа другим. Откат занимает ровно столько времени, сколько нужно для монтирования предыдущего образа.