Файловую систему запилил, на ЕСП32

Портировал недавно PH7 PHP engine на ESP32. И обнаружил, что engine умеет не просто скрипты загружать в память и исполнять, а исполнять их без загрузки (на манер XIP). Но нужна поддержка mmap(). Которая есть в Линуксе, а вот в ESP32 - нету.

Сел писать свою ФС со свистелками, под это дело. Чтобы mmap() поддерживался. Дмуал - напишу за день-два (чо там писать, примеры, вон , лежат), в одном файлике. Провозился три недели, в результате.

Файловая система только для чтения. Чуть более POSIX, чем то, что есть на ESP32.

Надеюсь, спортировать ее когда-нибудь на STM32. Пока она работает на ESP32 и в режиме разработчика :slight_smile: на Windows/CYGWIN.

А, самое главное-то забыл сказать: образ файловой системы - это TAR архив. Соответственно, поддерживаются symlinks/hardlinks. Для создания образа файловой системы нужна лишь утилита tar.

Образ можно скачать\исправить\залить обратно, например.

Погоняю еще недельку (глюки половлю) и выложу в общий доступ. Может в основное дерево ESP-IDF удастся пролезть ;0)

README для любителей читать простыни.

TARFS — Read-only mmap-oriented filesystem over TAR images for firmware assets and runtime resources

1. Введение

TARFS — простая и быстрая файловая система только для чтения, разработанная специально для встраиваемых систем.
В качестве образа фаловой системы используется файл в формате TAR, который создает утилита GNU tar.

Вместо реализации полноценной файловой системы с поддержкой записи, TARFS предоставляет прямой zero-copy
доступ к стандартным POSIX TAR-архивам, расположенным в отображённой (memory-mapped) Flash-памяти.
Это позволяет использовать файлы любого размера практически без расхода оперативной памяти, сохраняя
совместимость с привычными POSIX-интерфейсами и стандартными инструментами для работы с TAR.

Большинство файлов в прошивке никогда не изменяются после компиляции. HTML-страницы, CSS, JavaScript,
изображения, сертификаты, шрифты, словари, таблицы, модели ML и другие ресурсы обычно создаются на этапе
сборки прошивки и затем только читаются. Использование полноценной файловой системы с поддержкой записи
для хранения таких данных зачастую избыточно.

2. Зачем нужна еще одна файловая система?

  1. Для хранения и быстрого доступа к неизменяемым данным в условиях ограниченной памяти при больщом количестве файлов. Например - к иконкам и прочим ассетам
    вашего firmware / UI, или к html страницам вашего embedded HTTP сервера. TARFS ориентирована на большое количество неизменяемых файлов и максимальную скорость доступа.

  2. Как porting layer для приложений, портируемых с Linux, которые ожидают “более-менее POSIX файловую систему”:
    например PH7 PHP Engine для ESP32 использует mmap() для выполнения скриптов PHP. Наличие mmap() позволяет нам работать с файлами любого размера не смотря на то, что RAM - ограниченный ресурс.

  3. Для повышения выживаемости и восстановления работоспособности (мы всегда сможем сделать mount()) после
    повреждений файловой системы (для устройств в “поле”)

  4. Для контроля целостности каждого файла внутри файловой системы

3. Что такого особенного она умеет?

  1. Особенного - ничего. Просто реализует более-менее POSIX файловую систему (POSIX в смысле
    интерфейса доступа). Разработана для микроконтроллеров с поддержкой адресуемой FLASH памяти, легко
    портируется (один платформо-зависимый файл), написана на языке Си. Примеры платформ, куда потенциально
    можно портировать TARFS:

    STM32, ESP32, SAMD21/SAMD51, RP2040, nRF52, RA4M1 (Arduino UNO R4), LPC17xx/LPC55xx и многие другие, которые
    поддерживают отображение flash памяти в адресное пространство.

    Файловая система может находится на флешке, или быть встроенной в бинарник прошивки (средствами линкера
    или прямым включением бинарных файлов в проект)

  2. Требования к памяти:
    TARFS написана для использования на микроконтроллерах, в условиях ограниченной памяти.
    Для своей работы TARFS использует 24 байта на файл\каталог. Файловая система с тысячей файлов будет
    занимать чуть меньше 24Кбайт памяти.

    Ограничения на количество активных mmap() - нет, т.к. mmap() в TARFS не аллокирует никаких ресурсов, а
    всего лишь возвращает указатель.
    При создании файлового индекса используются прямые указатели на имена хранящиеся в TAR файле,
    дополнительная память (RAM) под имена не выделяется. При работе файловой системы в обычном режиме
    никаких аллокаций\освобождений памяти не производится (за исключением операций mount()/unmount(),
    которые выделяют и освобождают память)

    Не фрагментирует память при работе (в большинстве случаев), т.к. совершает всего 2 (две) аллокации
    памяти при монтировании и не аллокирует память вовсе при нормальной работе.

  3. Более полная совместимость с POSIX, чем у существующих файловых систем в ESP-IDF:
    Поддержка mmap()/zero-copy: Файлы из файловой системы можно отобразить на память стандартной функцией mmap(). RAM при этом
    не используется, а файл становится доступен, как если бы это был байтовый массив.

    помимо mmap() поддерживаются readlink(): symlinks и hardlinks (и directory junctions, если архив создавался
    на хосте под управлением Windows). Поддерживаются UTF-8 в именах каталогов, файлов и ссылок. open() может
    открывать каталог, поддерживается POSIXный dirfd().

  4. Контроль целостности файлов внутри файловой системы (CRC64/ECMA-182):
    Контроль целостности файлов достигается встройкой специальной контрольной суммы в каждую запись TAR архива таким образом, чтобы это не влияло на
    сам архив: TAR файлы с контрольными суммами файлов все так же будут открываться обычными утилитами, предназначенными для работы с TAR архивами.
    Контрольные суммы встраиваются в неиспользуемые поля TAR заголовков, для “прошивки” контрольных сумм в
    любой TAR архив используется утилита tarsum.c: утилита считает контрольные суммы всех файлов входящих
    в архив (не только файлов но и прочих записей имеющих ненулевой размер) и записывает эти суммы в
    неиспользуемое пространство TAR файла.

  5. Устойчивость к повреждениям:
    Файловая система не имеет никакого центрального суперблока, поэтому повредить ее так, чтобы она
    совсем не работала - крайне сложно: повреждение отдельных записей TAR-архива не приводит к отказу
    файловой системы в целом. TARFS пропускает поврежденные записи и продолжает анализ архива, сохраняя
    доступ к неповрежденным данным.

    Устойчивость к “забытым” mmap() (когда забыли вызвать munmap() после окончания пользования файлом):
    прошивка НЕ может съесть память или ресурсы ксли нерадивый программист “забыл” сделать munmap().

  6. Скорость и предсказуемость:
    TARFS не использует объекты синхронизации, за исключением операций mount/unmount: весь код написан
    без блокировок, lockless (конечно, не без races, но они довольно специфичны и задокументированы).

    Для поиска inodes используется бинарный поиск по хэшу имени. Для readdir() & friends все inodes
    отсортированы и по хэшу и по алфавиту

    Операция : Время : Примечание
    ----------------------------------:------------:--------------------------
    mount() : O(3N)+O(k) : 3N==> три прохода по архиву + O(k) link resolution step
    open(), opendir() : O(log N) : бинарный поиск
    readlink() : O(k) : k - количество ссылок в файловой системе (hard+sym)
    read()/mmap()/close()/seek()/ итд : O(1) : чтение по готовому казателю
    telldir()/readdir()/ итд : O(1) : чтение по готовому казателю

Отличное!
Вынужден отметить, что этим путем уже ходили и есть несколько реализаций и именно ради mmap(), но все равно хорошо.
Можно приделать псевдо mmap() на С++ через перегрузку операторов, но это будет самообман :wink: , поскольку mmap() нужен исключительно для скорости доступа.

Спасибо. Хочу влезть в кодовую базу ESP-IDF со своими самоварами: tarfs и, воображаемая (еще на написал, не садился даже) In-Memory FS. На ESP32 памяти сейчас много стали ставить, использовать ее толком никто не использует на всю катушку. А там можно сделать RAM-диск, например. Для tmpfs или еще для чего. Для логов каких-нибудь. Да, после перезагрузки все будет обнуляться :).

А я, а я… а я тогда в следующей версии компрессию прикручу. И буду тягаться со squashfs или как ее там.

смысл “дешевой” mmap() в том, чтобы файл лежал одним блоком и непрерывно. ФС компактные и с записью так не умеют. Поэтому идут двумя путями- либо как ты - просто своя ФС, либо классика, но специально приготовленная - “в горшочке, на пару, с розмарином” ;)… чтобы файлы были плоскими.

Работает. Вроде. Надо гонять, а это - самое ленивое. Файлы можно просто покидать в каталог со скетчем и все вместе скомпилировать. Винигрет.

тут → GitHub - vvb333007/tarfs: TARFS Immutable file system for microcontrollers (ESP32 and alike) · GitHub

readme.md

как собрать\прошить образ файловой системы

пример использования в скетче, на есп32

Ссылки на примеры\описание , больше недействительны. А исправить я их уже не могу. Перепилил все под формат ардуино-библиотеки.

Запустил вот тест на ночь, по кругу. Читать файлы, каталоги, открывать, закрывть и прочее. Посмотрим, что будет утром. Если не помрет, убдем считать 0.1.0 состоявшимся релизом. Спокойной ночи, мне завтра нырять надо а там волны и ураган с дождем. Поедем датчики собирать поломанные.

о коллега, спалился, вам под полтос оказывается )))

48 :0). Как новенький. Только вот по утрам - как старенький :)))

Ну что, за ночь не повисло , не протекло. Пойдет. :).

Чтение из файла, функцией read() примерно в 4-5 раз быстрее, чем обычный memcpy().

Но только на ESP32S3 (см. config.h, макро CONFIG_TARFS_HAVE_OPTIMIZED_MEMCPY).

На обычной ЕСПшке скорость чтения, понятное дело, равна скорости memcpy(). Ну, в любом случае быстрее в разы, чем SPIFFS, LittleFS или FAT

По памяти:

Флешка:

код (секция .text) занимает чуть меньше 10к, скомпилированный под есп32. То-есть - ниочем.

ОЗУ:

Секция .bss - 24 байта., .data == 0 байт.

При монтировании файловой системы: RAM_Total == 128 байт + чуть-чуть + (количество файлов + количество каталогов)*24 байт.

Т.е. файловая система с тысячей файлов будет занимать около 24к RAM.

Чуть-чуть” == (количество файлов\каталогов, с длинной имени ровно 100 байт) * 100 + (количество файлов\каталогов, с длинной имени ровно 155 байт) * 155

opendir() выделяет себе память (ну потому что по другому никак нельзя - надо DIR * возвращать какой-то), около 275 байт на открытый каталог.

open()/close()/read()/pread()/mmap()/dupfd()/readdir()/seekdir()/telldir()/lseek()/stat()/fstat()… → не выделяют памяти вовсе.

Кстати.

В ESP-IDF есть бага в файловой системе. Ну как бага.. Гонка. Подвержены все файловые системы, кроме, разумеется, tarfs :).

Бага состоит в следующем - одновременный unmount() и open()\read()\pread()… обрушат систему. Ну, понятное дело, что не 100% - гонка все-таки. У них read() устроен так, что ему , дополнительным параметром передают void *context - некий контекст, который создала файловая система (фат, например). Unmount() удаляет этот контест. А функции чтения\записи\прочее - не проверяют (да и как они бы смогли?), что указатель может быть мертвенький уже. Классический use-after-free.

Я эту багу в тарфс обошел тем, что вместо указателя на память я передаю некий внутренний индекс, по которому я уже безопасно беру указатель.

Решето, короче. Но в трекер им я писать ничего не буду, там и так у них тикетов выписано миллион.

Вот, например, так выглядит реализация read() в официальном коде, SPIFFS:

spiffs_file SPIFFS_open(spiffs *fs, const char *path, spiffs_flags flags, spiffs_mode mode) {
  SPIFFS_API_DBG("%s '%s' "_SPIPRIfl "\n", __func__, path, flags);
  (void)mode;
  SPIFFS_API_CHECK_CFG(fs);
  SPIFFS_API_CHECK_MOUNT(fs);
  if (strlen(path) > SPIFFS_OBJ_NAME_LEN - 1) {
    SPIFFS_API_CHECK_RES(fs, SPIFFS_ERR_NAME_TOO_LONG);
  }
  SPIFFS_LOCK(fs);
...
...
...

Указатель *fs может умереть в любой момент, но этой функции на это наплевать. Да ей по сути, и на path == NULL наплевать. В FAT - аналогично.

Про 4-5 раз - я наврал.

Реальная скорость чтения файла - 26 мбайт в секунду на ESP32S3, вот в таком сценарии:

( Если читать по 10кбайт за раз, то будет еще быстрее, но это уже будет нереалистичный сценарий.)

// Бездумно читаем файлик размером  4114 КБайт, в цикле.
    char buf[1024];
    ssize_t n;

    int i = 0;
    while ((n = read(fd, buf, sizeof(buf))) > 0) {
        i++;
    }

расплата за спокойную жизнь - кортизол )))

знать бы как работает sqlite, то базу с таблицами для чтения открывать в твоей fs, для записи - кто к какой привык

По этому поводу вспомнился анекдот…
Стюардесса в салоне лайнера объявляет о то, что находится в самолете:

  • На первой палубе - багаж, на второй - бар, на третьей - поле для гольфа, на четвертой бассейн.
  • А теперь, господа, пристегнитесь. Сейчас со всей этой х…й мы попробуем взлететь.

Вы просто используйте все пространство, как один файл бд.

sqlite умеет работать с несколькими базами одновременно, я о том, что справочники держать в доступной только для чтения, раз скорость в ней неплохая

Вполне себе нормальный концепт. В больших системах частенько делабт несколько файловых систем - одну для чтения чего-то постоянного (заодно и не испортишь случайно), а для записи - FAT, а для /tmp вообще можно сделать аналог RAM-Диска.

Если sqlite можно так растащщить - типа, тут у тебя read-only индексы, а данные - воооон там, на другой файловой системе - то круто.

Поделите на две части. Вам как таковая файловая система не нужна.

можно открыть две базы сразу на разных файловых, у меня так сделано, одна на SD карте другая Littlefs, на SD таблицы только для чтения

там где справочники нужна высокая скорость чтения, надо будет попробовать