1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/openharmony-third_party_littlefs

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
DESIGN.md 92 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 28.11.2024 18:14 a21bfae

Дизайн LittleFS

LittleFS — небольшая отказоустойчивая файловая система, предназначенная для микроконтроллеров.

   | | |     .---._____
  .-----.   |          |
--|o    |---| littlefs |
--|     |---|          |
  '-----'   '----------'
   | | |

Изначально LittleFS была создана в качестве эксперимента для изучения дизайна файловых систем в контексте микроконтроллеров. Вопрос был следующим: как создать файловую систему, устойчивую к потере питания и износу флэш-памяти без использования неограниченной памяти?

В этом документе рассматривается высокоуровневый дизайн LittleFS, его отличия от других файловых систем и принятые проектные решения. Для получения подробной информации о низкоуровневых деталях, касающихся каждого бита на диске, обратитесь к SPEC.md.

Проблема

Целевые встроенные системы LittleFS обычно представляют собой 32-разрядные микроконтроллеры с примерно 32 КБ ОЗУ и 512 КБ ПЗУ. Часто они сочетаются с микросхемами SPI NOR flash объёмом около 4 МБ. Эти устройства слишком малы для Linux и большинства существующих файловых систем, поэтому требуется код, написанный специально с учётом размера.

Сама по себе флэш-память представляет собой интересную технологию со своими особенностями и нюансами. В отличие от других форм хранения данных, запись во флэш-память требует двух операций: стирания и программирования. Программирование (установка битов в 0) относительно дёшево и может быть очень детализированным. Стирание же (установка битов в 1), напротив, требует дорогостоящей и разрушительной операции, которая и дала название флэш-памяти. На Wikipedia можно найти больше информации о том, как именно работает флэш-память.

Чтобы сделать ситуацию ещё более раздражающей, эти встроенные системы часто теряют питание в любое время. Обычно код микроконтроллера прост и реактивен, без концепции процедуры выключения. Это создаёт большую проблему для постоянного хранилища, где неудачная потеря питания может повредить хранилище и сделать устройство невосстановимым.

Это оставляет нам три основных требования к встроенной файловой системе.

  1. Устойчивость к потере питания — в этих системах питание может быть потеряно в любой момент. Если потеря питания повредит какие-либо постоянные структуры данных, это может привести к тому, что устройство станет невосстановимым. Встроенная файловая система должна быть спроектирована так, чтобы восстанавливаться после потери питания во время любой операции записи.

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

  3. Ограниченное использование RAM/ROM — если вышеперечисленных требований было недостаточно, эти системы также имеют очень ограниченные объёмы памяти. Это препятствует использованию многих существующих дизайнов файловых систем, которые могут полагаться на относительно большие объёмы оперативной памяти для временного хранения метаданных файловой системы.

Для ПЗУ это означает, что мы должны сделать наш дизайн простым и повторно использовать пути кода, когда это возможно. Для ОЗУ у нас есть более строгое требование: всё использование ОЗУ ограничено. Это означает, что использование ОЗУ не увеличивается по мере изменения размера или количества файлов в файловой системе. Это создаёт уникальную проблему, поскольку даже предположительно простые операции, такие как обход файловой системы, становятся удивительно сложными.

Существующие дизайны?

Итак, что уже существует? Конечно, существует множество различных файловых систем, однако они часто заимствуют и перенимают функции друг у друга. Если мы посмотрим на устойчивость к потере питания и выравнивание износа, то сможем сузить их до нескольких вариантов.

Во-первых, у нас есть нерезистентные блочные файловые системы, такие как FAT и ext2. Это самые ранние дизайны файловых систем и зачастую самые простые. Здесь хранилище разделено на блоки, а каждый файл хранится в наборе блоков. Без модификаций эти файловые системы не являются устойчивыми к потере питания, поэтому обновление файла — это просто перезапись блоков на месте.

               .--------.
               |  root  |
               |        |
               |        |
               '--------'
               .-'    '-.
              v          v
         .--------.  .--------. |  |  | B  |
|---|---|---|
|  |  |   |  | 
|  |  |   |  |
| '--------' '--------'|
|. -'    '-.  . -'    '-.|
|v         v  v         |
|.--------. .--------.|
| C       | D       |
|         |        |
|         |        |
|'--------' '--------'|

Из-за своей простоты такие файловые системы обычно являются самыми быстрыми и маленькими. Однако недостаток устойчивости к сбоям питания не велик, а жёсткая привязка расположения хранилища и данных лишает файловую систему возможности управлять износом.

  1. В совершенно другом направлении у нас есть файловые системы журналирования, такие как JFFS, YAFFS и SPIFFS. Расположение хранилища не привязано к фрагменту данных, вместо этого всё хранилище используется для циклического журнала, который дополняется каждым изменением, внесённым в файловую систему. Запись добавляет новые изменения, а чтение требует обхода журнала для восстановления файла. Некоторые файловые системы ведения журналов кэшируют файлы, чтобы избежать затрат на чтение, но это достигается за счёт оперативной памяти.
                                                             v
.--------.--------.--------.--------.--------.--------.--------.--------.|
        C        | new B  | new A  |                 |   A    |   B    |
                       |        |        |->               |        |        |
                       |        |        |                 |        |        |
'--------'--------'--------'--------'--------'--------'--------'--------'|

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

Основным недостатком является производительность. Если мы посмотрим на сборку мусора, процесс очистки устаревших данных из конца журнала, я ещё не видел чисто файловой системы журналирования, которая не имела бы одной из этих двух затрат:

1. O(n²) время выполнения
2. O(n) оперативная память

SPIFFS здесь представляет особый интерес, поскольку он использует тот факт, что повторные программы для флэш-памяти NOR являются атомарными и маскирующими. Это очень изящное решение, однако оно ограничивает тип поддерживаемого хранилища.

3. Возможно, наиболее распространённым типом файловой системы является журналируемая файловая система — потомок, возникающий при объединении блочной файловой системы с файловой системой журналирования. ext4 и NTFS являются хорошими примерами. Здесь мы берём обычную блочную файловую систему и добавляем ограниченный журнал, где отмечаем каждое изменение перед его выполнением.

journal .--------.--------| C'| D'| | E'| | |-> | | | | | | |'--------'--------| .-' '-. v v .--------. .--------.| A | B | | | | | | | '--------' '--------'| .'-' .-' '-.| v v v| .--------. .--------. .--------.| C | D | E | | | | | | | | | '--------' '--------' '--------'|


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

К сожалению, у журналируемых файловых систем есть пара проблем. Они довольно сложны, так как фактически работают две файловые системы параллельно, что приводит к увеличению размера кода. Также они не предлагают защиты от износа из-за тесной связи между расположением хранилища и данными.

4. Последнее, но... Не в последнюю очередь у нас есть файловые системы с копированием при записи (COW), такие как btrfs и ZFS. Они очень похожи на другие блочные файловые системы, но вместо обновления блока на месте все обновления выполняются путём создания копии с изменениями и замены любых ссылок на старый блок нашим новым блоком. Это рекурсивно продвигает все наши проблемы вверх, пока мы не достигнем корня нашей файловой системы, который часто хранится в очень маленьком журнале.
           .--------.                  .--------.
           |  root  |       write      |new root|
           |        |        ==>       |        |
           |        |                  |        |
           '--------'                  '--------'
           .-'    '-.                    |    '-.
          |  .-------|------------------'        v
          v v        v                       .--------.
     .--------.  .--------.                  | new B  |
     |   A    |  |   B    |                  |        |
     |        |  |        |                  |        |
     |        |  |        |                  '--------'
     '--------'  '--------'                  .-'    |
     .-'         .-'    '-.    .------------|------'
    |           |          |  |             v
    v           v          v  v        .--------.

.--------. .--------. .--------. | new D | | C | | D | | E | | | | | | | | | | | | | | | | | '--------' '--------' '--------' '--------'


Файловые системы COW интересны. Они предлагают производительность, аналогичную производительности блочных файловых систем, при этом им удаётся выполнять атомарные обновления без хранения изменений данных непосредственно в журнале. Они даже разъединяют место хранения данных, что создаёт возможность для выравнивания износа.

Ну, почти. Неограниченное движение обновлений вверх вызывает некоторые проблемы. Поскольку обновления в файловой системе COW не останавливаются, пока не достигнут корня, обновление может каскадно привести к большему количеству операций записи, чем потребовалось бы для исходных данных. Кроме того, движение вверх фокусирует эти операции записи в блоке, который может износиться гораздо раньше, чем остальная часть файловой системы.

## littlefs

Так что же делает littlefs?

Если мы посмотрим на существующие файловые системы, есть два интересных шаблона проектирования, которые выделяются, но каждый имеет свой собственный набор проблем. Ведение журнала обеспечивает независимую атомарность, но имеет плохую производительность во время выполнения. А структуры данных COW, которые хорошо работают, продвигают проблему атомарности вверх.

Можем ли мы обойти эти ограничения?

Рассмотрим ведение журнала. Оно имеет либо квадратичную сложность по времени выполнения O(n²), либо линейную по объёму используемой памяти O(n). Мы не можем избежать этих затрат, *но* если мы установим верхний предел размера, мы, по крайней мере, сможем предотвратить превращение теоретической стоимости в проблему. Это опирается на суперсекретный хакерский приём в информатике, где вы можете притвориться, что любая алгоритмическая сложность равна O(1), ограничив ввод.

В случае структур данных COW мы можем попробовать немного изменить определение. Допустим, наша структура COW копируется не после одной записи, а после *n* записей. Это не меняет большинство свойств COW (при условии, что вы можете писать атомарно!), но это предотвращает движение износа вверх. Этот вид копирования при ограниченном количестве записей (CObW) всё ещё фокусирует износ, но на каждом уровне мы делим распространение износа на *n*. При достаточно большом *n* (> коэффициент ветвления) распространение износа больше не является проблемой.

Видите, к чему это ведёт? Ведение журнала и COW по отдельности являются несовершенными решениями и имеют недостатки, ограничивающие их полезность. Но если мы объединим их, они смогут взаимно решить проблемы друг друга.

Это идея, лежащая в основе littlefs. На уровне субблоков littlefs состоит из небольших двухблочных журналов, которые обеспечивают атомарное обновление метаданных в любом месте файловой системы. На уровне суперблока littlefs представляет собой дерево блоков CObW, которое можно выселить по требованию. Есть ещё некоторые незначительные проблемы. Небольшие логи могут быть дорогими с точки зрения хранения, в худшем случае небольшой лог стоит в 4 раза больше, чем размер исходных данных.

Структуры CObW требуют эффективного распределителя блоков, поскольку выделение происходит при каждой записи. И всё ещё остаётся проблема поддержания постоянного использования оперативной памяти.

## Пары метаданных

Пары метаданных — это основа littlefs. Это небольшие двухблочные логи, которые позволяют выполнять атомарные обновления в любом месте файловой системы.

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

Мы могли бы сделать наши логи больше двух блоков, но следующая задача — как мы храним ссылки на эти логи? Поскольку сами блоки стираются во время записи, использовать структуру данных для отслеживания этих блоков сложно. Простое решение здесь — хранить адреса двух блоков для каждой пары метаданных. У этого есть дополнительное преимущество: мы можем независимо изменять блоки в паре метаданных и не уменьшаем детализацию наших блоков для других операций.

Чтобы определить, какой блок метаданных самый последний, мы сохраняем счётчик ревизий, который сравниваем, используя арифметику последовательностей (очень удобно для избежания проблем с целочисленным переполнением). Удобно, что этот счётчик ревизий также даёт нам примерное представление о том, сколько стираний произошло в блоке.

Указатель пары метаданных: {блок 0, блок 1} | '--------------------. '-. | Диск v v .--------.--------.--------.--------.--------.--------.--------.--------. | | |метаданные| |метаданные| | | | |блок 0 | |блок 1 | | | | | | | | | '--------'--------'--------'--------'--------'--------'--------'--------' '--. .----' v v Пара метаданных .----------------.----------------. | ревизия 11 | ревизия 12 | Блок 1 является |--------------|--------------| самым последним | A | A'' | |--------------|--------------| |


Атомарность (один из видов устойчивости к потере питания) требует двух составляющих: избыточности и обнаружения ошибок. Обнаружение ошибок может быть обеспечено контрольной суммой, и в случае littlefs мы используем 32-битный CRC (циклический избыточный код).

Поддержание избыточности, с другой стороны, требует нескольких этапов.

1. Если наш блок не заполнен и размер программы достаточно мал, чтобы позволить нам добавить больше записей, мы можем просто добавить записи в журнал. Поскольку мы не перезаписываем исходные записи (помните, что переписывание флэш-памяти требует стирания), у нас всё ещё есть исходные записи, если мы потеряем питание во время добавления.

   Обратите внимание, что littlefs не поддерживает контрольную сумму для каждой записи. Многие файловые системы ведения журналов делают это, но это ограничивает то, что вы можете обновить за одну атомарную операцию. Вместо этого мы можем группировать несколько записей в коммит, который имеет общую контрольную сумму. Это позволяет нам обновлять несколько несвязанных частей метаданных, пока они находятся в одной паре метаданных.

2. Если наш блок полон записей, нам нужно как-то удалить устаревшие записи, чтобы освободить место для новых. Этот процесс называется сборкой мусора, но поскольку littlefs имеет несколько сборщиков мусора... **Есть ещё одна сложность, которая возникает при работе с небольшими логами.**

Амортизированная стоимость времени сборки мусора зависит не только от единовременных затрат (O(n²) для littlefs), но и от того, как часто происходит сборка мусора.

Рассмотрим два крайних случая:
1. Лог пуст, сборка мусора происходит один раз каждые n обновлений.
2. Лог полон, сборка мусора происходит **при каждом** обновлении.

Очевидно, что нам нужно быть более агрессивными, чем ждать, пока наша пара метаданных заполнится. По мере приближения пары метаданных к заполнению частота уплотнений растёт очень быстро.

Рассматривая проблему в общем виде, рассмотрим лог с n байтами для каждой записи, d динамическими записями (записи, которые устаревают во время сборки мусора) и s статическими записями (записями, которые необходимо скопировать во время сборки мусора). Если мы посмотрим на амортизированную сложность времени обновления этого лога, то получим следующую формулу:

cost = n + n (s / d+1).

Если мы позволим r быть отношением статического пространства к размеру нашего лога в байтах, мы найдём альтернативное представление количества статических и динамических записей:

s = r (размер/n);
d = (1 - r) (размер/n).

Подставляя их вместо d и s, получаем красивую формулу стоимости обновления записи с учётом степени заполнения лога:

cost = n + n (r (размер/n) / ((1-r) (размер/n) + 1)).

Предполагая, что размер записи составляет 100 байт в 4 КБ логе, мы можем построить график, используя размер записи, чтобы найти мультипликативную стоимость:

График стоимости обновления пары метаданных.

Таким образом, при использовании на 50% мы видим среднюю стоимость в 2 раза за обновление, а при использовании на 75% — уже в среднем в 4 раза за обновление.

Чтобы избежать такого экспоненциального роста, вместо ожидания заполнения нашей пары метаданных мы разделяем пару метаданных, когда превышаем предел в 50%. Мы делаем это лениво, ожидая, пока нам не понадобится уплотнить, прежде чем проверять, вписываемся ли мы в наш 50%-ный лимит. Это ограничивает накладные расходы сборки мусора до 2x стоимости времени выполнения, давая нам амортизированную сложность времени выполнения O(1).

**Если мы рассмотрим пары метаданных и связанные списки пар метаданных на высоком уровне, у них будет довольно хорошая стоимость времени выполнения.**

Предполагая n пар метаданных, каждая из которых содержит m записей метаданных, стоимость поиска конкретной записи имеет наихудшую сложность времени выполнения O(nm). Для обновления конкретной записи наихудшая сложность равна O(nm²), с амортизированной сложностью всего O(nm).

Однако разделение на 50% означает, что в лучшем случае наши пары метаданных будут заполнены только на 1/2. Если мы включим накладные расходы второго блока в нашу пару метаданных, каждая запись метаданных будет иметь эффективную стоимость хранения в 4 раза больше исходного размера. Я полагаю, пользователи были бы недовольны, если бы обнаружили, что могут использовать только четверть своего... **Пропускные списки CTZ**

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

**Пропускные списки CTZ** 

Пары метаданных предоставляют эффективный механизм атомарного обновления, но, к сожалению, имеют высокую стоимость хранения. Однако мы можем обойти эту стоимость, используя пары метаданных только для хранения ссылок на более плотные структуры данных с копированием при записи (COW).

Структуры данных с копированием при записи, также называемые чисто функциональными структурами данных, представляют собой категорию структур данных, в которых базовые элементы являются неизменяемыми. Для внесения изменений в данные необходимо создать новые элементы, содержащие копию обновлённых данных, и заменить все ссылки ссылками на новые элементы. Как правило, производительность структуры данных COW зависит от того, сколько старых элементов можно повторно использовать после замены частей данных.

У littlefs есть несколько требований к структурам COW. Они должны быть эффективными для чтения и записи, но самое главное — они должны быть проходимы с постоянным объёмом оперативной памяти. В частности, это исключает B-деревья, которые нельзя пройти с постоянным объёмом ОЗУ, и B+-деревья, которые невозможно обновить операциями COW.

Итак, что мы можем сделать? Сначала рассмотрим хранение файлов в простом связанном списке COW. Добавление блока, который является основой для записи файлов, означает, что нам нужно обновить последний блок, чтобы он указывал на наш новый блок. Это требует операции COW, а значит, нам нужно обновить предпоследний блок, затем третий с конца и так далее, пока мы не скопируем весь файл.

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

Однако у обратного связанного списка есть довольно очевидная проблема. Перебор файла по порядку имеет временную сложность O(n²). Квадратичное время выполнения только для чтения файла! Это ужасно.

К счастью, мы можем добиться большего. Вместо односвязного списка littlefs использует многоуровневый связанный список, часто называемый пропускным списком. Однако, в отличие от наиболее распространённого типа пропускных списков, пропускные списки littlefs строго детерминированы и основаны на некоторых интересных свойствах инструкции подсчёта конечных нулей (CTZ).

Правила, которым следуют пропускные списки CTZ, заключаются в том, что для каждого n-го блока, где n делится на 2^i, этот блок содержит указатель на блок n-2^i. Это означает, что каждый блок содержит от 1 до log₂n указателей, которые пропускают разные предшествующие элементы пропускного списка.

Название происходит от интенсивного использования инструкции CTZ, которая позволяет нам эффективно вычислять степени двойки. Для данного блока n этот блок содержит ctz(n)+1 указателей. |--|        |--|        |  |        |
'--------'  '--------'  '--------'  '--------'  '--------'  '--------'

Дополнительные указатели позволяют нам перемещаться по структуре данных на диске гораздо эффективнее, чем в односвязном списке.

Рассмотрим путь от блока данных 5 к блоку данных 1. Можно увидеть, как полностью был пропущен блок данных 3:

.--------.  .--------.  .--------.  .--------.  .--------.  .--------.
| данные 0 |  | данные 1 |<-| данные 2 |  | данные 3 |  | данные 4 |<-| данные 5 |
|        |  |        |  |        |<-|        |--|        |  |        |
|        |  |        |  |        |  |        |  |        |  |        |
'--------'  '--------'  '--------'  '--------'  '--------'  '--------'

Путь к блоку данных 0 ещё быстрее, требуется только два перехода:

.--------.  .--------.  .--------.  .--------.  .--------.  .--------.
| данные 0 |  | данные 1 |  | данные 2 |  | данные 3 |  | данные 4 |<-| данные 5 |
|        |  |        |  |        |  |        |  |        |  |        |
|        |<-|        |--|        |--|        |--|        |  |        |
'--------'  '--------'  '--------'  '--------'  '--------'  '--------'

Мы можем найти сложность выполнения, посмотрев на путь к любому блоку из блока, содержащего наибольшее количество указателей. Каждый шаг вдоль пути делит пространство поиска для блока пополам, что даёт нам сложность выполнения O(log n). Чтобы добраться до блока с наибольшим количеством указателей, мы можем выполнить те же шаги назад, что приводит к сложности выполнения O(2 log n) = O(log n). Интересно отметить, что этот оптимальный путь возникает естественным образом, если мы жадно выбираем указатель, который покрывает наибольшее расстояние, не проходя мимо нашей цели.

Итак, теперь у нас есть структура данных COW, которая дешева для добавления со сложностью выполнения O(1), и может быть прочитана со сложностью в худшем случае O(n log n). Учитывая, что эта сложность также делится на объём данных, которые мы можем хранить в блоке, эта стоимость вполне разумна.


Это новая структура данных, поэтому у нас всё ещё есть несколько вопросов. Какова дополнительная нагрузка на память? Может ли количество указателей превышать размер блока? Как мы храним CTZ-пропускной список в наших парах метаданных?

Чтобы найти дополнительную нагрузку на память, мы можем рассмотреть структуру данных как несколько связанных списков. Каждый связанный список пропускает вдвое больше блоков, чем предыдущий, или, с другой точки зрения, каждый связанный список использует вдвое меньше памяти, чем предыдущий. По мере приближения к бесконечности дополнительная нагрузка на память образует геометрический ряд. Решение этого уравнения говорит нам о том, что в среднем наша дополнительная нагрузка на память составляет всего 2 указателя на блок.

Поскольку размер нашего файла ограничен шириной слова, которую мы используем для хранения размеров, мы также можем решить вопрос о максимальном количестве указателей, которое нам когда-либо понадобится хранить в блоке. Если мы установим дополнительную нагрузку от указателей равной размеру блока, то получим следующее уравнение. Обратите внимание, что как меньший размер блока (![B][bigB]), так и большая ширина слова (![w]) приводят к большей дополнительной нагрузке на память.

Решение уравнения для ![B][bigB] даёт нам минимальный размер блока для некоторых распространённых ширин слов:

  1. 32-битный CTZ-пропускной список => минимальный размер блока 104 байта
  2. 64-битный CTZ-пропускной список => минимальный размер блока 448 байтов

littlefs использует 32-битную ширину слова, поэтому наши блоки могут переполняться указателями, только если они меньше 104 байт. Это лёгкое требование, поскольку на практике большинство размеров блоков начинаются с 512 байт. Пока наш размер блока больше 104 байт, мы можем избежать дополнительной логики, необходимой для обработки переполнения указателей.

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

Однако вычисление пары индекса и смещения по размеру немного сложнее. Мы можем начать с суммирования, которое проходит через все блоки вплоть до нашего заданного размера. Пусть ![B][bigB] будет размером блока в Вот перевод текста на русский язык:

Пусть w будет шириной слова в битах, n — индексом блока в списке пропуска, а N — размером файла в байтах:

$N = \sum_{i=0}^{n}(B - (w/8)(ctz(i) + 1))$, где ctz — формула 3.

Это работает довольно хорошо, но требует $O(n)$ для вычисления, что приводит к полному времени чтения файла до $O(n^2 log n)$. К счастью, это суммирование не требует обращения к диску, поэтому практическое влияние минимально.

Однако, несмотря на интеграцию побитовой операции, мы можем фактически свести это уравнение к форме $O(1)$. Просматривая удивительный ресурс, которым является [Онлайн-энциклопедия целочисленных последовательностей (OEIS)], мне удалось найти A001511, который соответствует итерации инструкции CTZ, и A005187, которая соответствует её частичной сумме. К моему большому удивлению, оба они являются результатом простых уравнений, приводящих нас к довольно неожиданному свойству, которое связывает две, казалось бы, несвязанные побитовые инструкции:

$\sum_{i=0}^{n} (ctz(i)+1) = 2^n - popcount(n)$, где:

  • ctz($x$) — количество конечных нулевых битов в $x$;
  • popcount($x$) — число битов, равных единице в $x$.

Первоначальные тесты этого удивительного свойства, похоже, подтверждаются. По мере того как $n$ приближается к бесконечности, мы получаем среднее значение накладных расходов в 2 указателя, что соответствует нашему предположению ранее. Во время итерации функция popcount, кажется, справляется с отклонениями от этого среднего значения. Конечно, просто чтобы убедиться, я написал быстрый скрипт, который проверил это свойство для всех 32-битных целых чисел.

Теперь мы можем подставить в наше исходное уравнение, чтобы найти более эффективное уравнение для размера файла:

$N = Bn - (w/8)(2^n - popcount(n))$, где — формула 5.

К сожалению, функция popcount неинъективна, поэтому мы не можем решить это уравнение для нашего индекса. Но что мы можем сделать, так это решить для индекса $n'$, который больше $n$, с ошибкой, ограниченной диапазоном функции popcount. Мы можем многократно подставлять $n'$ в исходное уравнение до тех пор, пока ошибка не станет меньше нашего целочисленного разрешения. Как оказалось, нам нужно выполнить эту подстановку только один раз, что даёт нам следующую формулу для нашего индекса:

$n = floor((N-(w/8)popcount(N/(B-2w/8))) / (B-2w/8))$, где — формула 6.

Теперь, когда у нас есть наш индекс $n$, мы можем просто подставить его обратно в приведённое выше уравнение, чтобы найти смещение. Мы сталкиваемся с небольшой проблемой целочисленного переполнения, но мы можем избежать этого, немного переставив уравнение:

$off = N - (B-2w/8)n - (w/8)popcount(n)$, где — формула 7.

Наше решение требует довольно много математики, но компьютеры очень хороши в математике. Теперь мы можем найти как наш индекс блока, так и смещение из размера за $O(1)$, позволяя нам хранить списки пропуска CTZ только с указателем и размером.

Списки пропуска CTZ дают нам структуру данных COW, которую легко пройти за $O(n)$, можно добавить за $O(1)$ и можно прочитать за $O(n log n)$. Все эти операции работают в ограниченном объёме оперативной памяти и требуют только двух слов накладных расходов на блок. В сочетании с парами метаданных списки пропуска CTZ обеспечивают отказоустойчивость питания и компактное хранение данных. Освобождение памяти, которое происходит почти так же часто, как выделение блоков, является простой операцией no-op. Эта стратегия «брось это на пол» значительно снижает сложность управления структурами данных на диске, особенно при обработке условий с высоким риском ошибки.


Наш распределитель блоков должен эффективно находить свободные блоки. Можно было бы пройти через каждый блок в хранилище и проверить каждый из них по нашему дереву файловой системы; однако время выполнения было бы ужасным. Нам нужно каким-то образом собирать несколько блоков за один проход.

Рассматривая существующие проекты, некоторые более крупные файловые системы, которые используют аналогичную стратегию «брось его на пол», хранят растровое изображение всего хранилища в [ОЗУ]. Это хорошо работает, потому что растровые изображения удивительно компактны. Мы не можем использовать ту же стратегию здесь, поскольку она нарушает наше постоянное требование к ОЗУ, но мы можем быть в состоянии модифицировать эту идею в работоспособное решение.

.----.----.----.----.----.----.----.----.----.----.----.----.
| A  |    |root| C  | B  |         | D  |    | E  |         |
|    |    |    |    |    |         |    |    |    |         |
'----'----'----'----'----'----'----'----'----'----'----'----'
  1    0    1    1    1    0    0    1    0    1    0    0
 \---------------------------+----------------------------/
                             v
               bitmap: 0xb94 (0b101110010100)

Распределитель блоков в littlefs представляет собой компромисс между растровым изображением размером с диск и полным перебором. Вместо растрового изображения размером с хранилище мы отслеживаем небольшое растровое изображение фиксированного размера, называемое буфером опережающего просмотра. Во время выделения блоков мы берём блоки из буфера опережающего просмотра. Если буфер опережающего просмотра пуст, мы сканируем файловую систему для получения дополнительных свободных блоков, заполняя наш буфер опережающего просмотра. При каждом сканировании мы используем увеличивающееся смещение, обходя хранилище по кругу по мере выделения блоков.

Вот как может выглядеть выделение 4 блоков в достаточно загруженной файловой системе с 32-битным опережающим просмотром и общим количеством 128 блоков (512 КиБ хранилища, если блоки имеют размер 4 КиБ):

boot...         lookahead:
                fs blocks: fffff9fffffffffeffffffffffff0000
scanning...     lookahead: fffff9ff
                fs blocks: fffff9fffffffffeffffffffffff0000
alloc = 21      lookahead: fffffdff
                fs blocks: fffffdfffffffffeffffffffffff0000
alloc = 22      lookahead: ffffffff
                fs blocks: fffffffffffffffeffffffffffff0000
scanning...     lookahead:         fffffffe
                fs blocks: fffffffffffffffeffffffffffff0000
alloc = 63      lookahead:         ffffffff
                fs blocks: ffffffffffffffffffffffffffff0000
scanning...     lookahead:         ffffffff
                fs blocks: ffffffffffffffffffffffffffff0000
scanning...     lookahead:                 ffffffff
                fs blocks: ffffffffffffffffffffffffffff0000
scanning...     lookahead:                         ffff0000
                fs blocks: ffffffffffffffffffffffffffff0000
alloc = 112     lookahead:                         ffff8000
                fs blocks: ffffffffffffffffffffffffffff8000

Этот подход с опережающим просмотром имеет сложность выполнения O(n²) для полного сканирования хранилища; однако растровые изображения на удивление компактны, и на практике обычно требуется только один или два прохода, чтобы найти свободные блоки. Кроме того, производительность распределителя можно оптимизировать, регулируя размер блока или размер буфера опережающего просмотра, обменивая либо детализацию записи, либо ОЗУ на производительность распределителя.

Выравнивание износа

У распределителя блоков есть вторая роль: выравнивание износа.

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

littlefs имеет два метода защиты от износа:

  1. Обнаружение и восстановление после сбоя блоков
  2. Равномерное распределение износа между динамическими блоками

Восстановление после сбоя блока на самом деле не имеет ничего общего с самим распределителем блоков. Вместо этого он полагается на способность файловой системы обнаруживать и удалять сбойные блоки при их возникновении.

В littlefs довольно просто обнаружить сбойные блоки... Мы можем обнаружить, что новый блок также является плохим, но, будем надеяться, после повторения этого цикла мы в конечном итоге найдём новый блок, где запись будет успешной. Если нет, это означает, что все блоки в нашем хранилище являются плохими, и мы достигли конца срока службы нашего устройства. На этом этапе littlefs вернёт ошибку «недостаточно места». Технически это верно, так как больше нет хороших блоков, но в качестве дополнительного преимущества это также соответствует состоянию ошибки, ожидаемому пользователями данных динамического размера.

С другой стороны, ошибки чтения гораздо сложнее. У нас нет копии данных, хранящихся в оперативной памяти, поэтому нам нужен способ восстановить исходные данные даже после того, как они были повреждены. Одним из таких механизмов для этого являются коды с исправлением ошибок (ECC).

ECC — это расширение идеи контрольной суммы. В то время как контрольная сумма, такая как CRC, может обнаружить, что в данных произошла ошибка, ECC может обнаруживать и фактически исправлять некоторое количество ошибок. Однако существует предел тому, сколько ошибок может обнаружить ECC: граница Хэмминга. По мере приближения количества ошибок к границе Хэмминга мы всё ещё можем обнаруживать ошибки, но уже не можем исправить данные. Если мы достигли... | | 4 КиБ | | | | | | | | | | | | | | | '----------------' /

Мы можем внести несколько улучшений. Во-первых, вместо того чтобы давать каждому файлу свою собственную пару метаданных, мы можем хранить несколько файлов в одной паре метаданных. Один из способов сделать это — напрямую связать каталог с парой метаданных (или связанным списком пар метаданных). Это позволяет нескольким файлам легко делиться парой метаданных каталога для ведения журнала и снижает совокупные накладные расходы на хранение.

Строгое связывание пар метаданных и каталогов также даёт пользователям прямой контроль над использованием хранилища в зависимости от того, как они организуют свои каталоги.

Несколько файлов хранятся в паре метаданных, 4 байта стоят ~4 КиБ

   .----------------.
  .|    revision    |
  ||----------------|
  ||    A name      |
  ||   A skiplist  -----.
  ||----------------|   |  \
  ||    B name      |   |  +- metadata
  ||   B skiplist  ---. |  |  4x8 bytes
  ||----------------| | |  /  32 bytes
  ||    checksum    | | |
  ||----------------| | |
  ||       |        | | |
  ||       v        | | |
  |'----------------' | |
  '----------------'  | |
     .----------------' |
    v                   v

.----------------. .----------------. \
| A data | | B data | +- data | | | |----------------| / 4 bytes | | | | | | | | | | | | | | | + data block | | | | 4 КиБ | | | | |----------------| | | | | | | | | | | | | | | '----------------' '----------------' /

Второе улучшение, которое мы можем сделать, заключается в том, что мы замечаем, что для очень маленьких файлов наши попытки использовать списки пропуска CTZ для компактного хранения приводят к обратным результатам. Пары метаданных имеют стоимость хранения примерно в 4 раза больше, поэтому, если наш файл меньше 1/4 размера блока, на самом деле нет никакой пользы в хранении нашего файла вне нашей пары метаданных.

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

Встроенные файлы хранятся в паре метаданных, 4 байта стоят ~16 байт

.----------------. .| revision | ||----------------| || A name | || A skiplist ---. ||----------------| |
|| B name | | +- data || B data | | | 4x4 bytes ||----------------| | / 16 bytes || checksum | | ||----------------| | || | | | || v | | |'----------------' | '----------------' | .---------' v .----------------. | A data | | | | | | | | | | | | |

'----------------'

Как только файл превышает 1/4 размера блока, мы переходим к списку пропуска CTZ. Это означает, что наши файлы никогда не используют более чем в 4 раза накладные расходы на хранилище, уменьшаясь по мере роста файла.

Каталоги

Теперь нам просто нужны каталоги для хранения наших файлов. Как упоминалось выше, мы хотим строгого связывания каталогов и пар метаданных, но есть несколько сложностей, которые нам нужно устранить.

Сами по себе каждый каталог представляет собой связанный список пар метаданных. Это позволяет нам хранить неограниченное количество файлов в каждом каталоге, и нам не нужно... Мы не беспокоимся о сложности выполнения (runtime complexity) неограниченных журналов. Мы можем хранить другие указатели каталогов в наших парах метаданных, что даёт нам древовидную структуру каталогов, во многом похожую на то, что вы найдёте в других файловых системах.

            .--------.
           .| root   |
           ||        |
           ||        |
           |'--------'
           '---|--|-'
            .-'    '-------------------------.
           v                                  v
      .--------.        .--------.        .--------.
     .| dir A  |------->| dir A  |       .| dir B  |
     ||        |       ||        |       ||        |
     ||        |       ||        |       ||        |
     |'--------'       |'--------'       |'--------'
     '---|--|-'        '----|---'        '---|--|-'
      .-'    '-.            |             .-'    '-.
     v          v           v            v          v
.--------.  .--------.  .--------.  .--------.  .--------.
| file C |  | file D |  | file E |  | file F |  | file G |
|        |  |        |  |        |  |        |  |        |
|        |  |        |  |        |  |        |  |        |
'--------'  '--------'  '--------'  '--------'  '--------'

Основная сложность заключается, опять же, в обходе с постоянным объёмом оперативной памяти RAM. Дерево каталогов — это дерево, и печальный факт заключается в том, что вы не можете обойти дерево с постоянной RAM.

К счастью, элементы нашего дерева — это пары метаданных, поэтому, в отличие от списков пропуска CTZ, мы не ограничены строгими операциями COW. Одна вещь, которую мы можем сделать, — это связать список через наше дерево, явно обеспечивая дешёвый обход всей файловой системы.

            .--------.
           .| root   |-.
           ||        | |
   .-------||        |-'
   |       |'--------'
   |       '---|--|-'
   |        .-'    '-------------------------.
   |       v                                  v
   |  .--------.        .--------.        .--------.
   '->| dir A  |------->| dir A  |------->| dir B  |
     ||        |       ||        |       ||        |
     ||        |       ||        |       ||        |
     |'--------'       |'--------'       |'--------'
     '---|--|-'        '----|---'        '---|--|-'
      .-'    '-.            |             .-'    '-.
     v          v           v            v          v
.--------.  .--------.  .--------.  .--------.  .--------.
| file C |  | file D |  | file E |  | file F |  | file G |
|        |  |        |  |        |  |        |  |        |
|        |  |        |  |        |  |        |  |        |
'--------'  '--------'  '--------'  '--------'  '--------'

К сожалению, отказ от чистых операций COW создаёт некоторые проблемы. Теперь всякий раз, когда мы хотим манипулировать деревом каталогов, необходимо обновить несколько указателей. Если вы знакомы с проектированием атомарных структур данных, это должно вызвать множество красных флагов.

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

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

Добавление каталога в наше дерево:

         .--------.
        .| root   |-.
        ||        | |
.-------||        |-'
|       |'--------'
|       '---|--|-'
|        .-'    '-.
|       v          v
|  .--------.  .--------.
'->| dir A  |->| dir C  |
  ||        | ||        |
  ||        | ||        |
  |'--------' |'--------'
  '--------'  '--------'

выделение dir B
=>
         .--------.
        .| root   |-.
        ||        | |
.-------||        |-'
|       |'--------'
|       '---|--|-'
|        .-'    '-.
|       v          v
|  .--------.    .--------.
'->| dir A  |--->| dir C  |
  ||        | .->|        |
  ||        | | ||        |
  |'--------' | |'--------'
  '--------'  | '--------'
              |
   .--------. |
  .| dir B  |-'
  ||        |
  ||        |
  |'--------'
  '--------'

вставка dir B в связанный список
``` Создание «осиротевшего» элемента:

linked-list, creating an orphan => .--------. .| root |-. || | | .-------|| |-' | |'--------' | '---|--|-' | .-' '-------------. | v v | .--------. .--------. .--------. '->| dir A |->| dir B |->| dir C | || | || orphan!| || | || | || | || | |'--------' |'--------' |'--------' '--------' '--------' '--------'


Добавление каталога В в родительский каталог:

add dir B to parent directory => .--------. .| root |-. || | | .-------------|| |-' | |'--------' | '--|-|-|-' | .------' | '-------. | v v v | .--------. .--------. .--------. '->| dir A |->| dir B |->| dir C | || | || | || | || | || | || | |'--------' |'--------' |'--------' '--------' '--------' '--------'

Удаление каталога:

Removing a directory:

               .--------.
              .| root   |-.
              ||        | |
.-------------||        |-'
|             |'--------'
|             '--|-|-|-'
|        .------'  |  '-------.
|       v          v           v
|  .--------.  .--------.  .--------.
'->| dir A  |->| dir B  |->| dir C  |
  ||        | ||        | ||        |
  ||        | ||        | ||        |
  |'--------' |'--------' |'--------'
  '--------'  '--------'  '--------'

remove dir B from parent directory, creating an orphan
=>
         .--------.
        .| root   |-.
        ||        | |
.-------||        |-'
|       |'--------'
|       '---|--|-'
|        .-'    '-------------.
|       v                      v
|  .--------.  .--------.  .--------.
'->| dir A  |->| dir B  |->| dir C  |
  ||        | || orphan!| ||        |
  ||        | ||        | ||        |
  |'--------' |'--------' |'--------'
  '--------'  '--------'  '--------'

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

In addition to normal directory tree operations, we can use orphans to evict
blocks in a metadata pair when the block goes bad or exceeds its allocated
erases. If we lose power while evicting a metadata block we may end up with
a situation where the filesystem references the replacement block while the
threaded linked-list still contains the evicted block. We call this a
half-orphan.
           .--------.
          .| root   |-.
          ||        | |

.-------------|| |-' | |'--------' | '--|-|-|-' | .------' | '-------. | v v v | .--------. .--------. .--------. '->| dir A |->| dir B |->| dir C | || | || | || | || | || | || | |'--------' |'--------' |'--------' '--------' '--------' '--------'


Попытка записи в каталог В:

try to write to dir B => .--------. .| root |-. || | | .----------------|| |-' | |'--------' | '-|-||-|-' | .--------' || '-----. | v |v v | .--------. .--------. .--------. '->| dir A |---->| dir B |->| dir C | || |-. | | || | || | | | | || | |'--------' | '--------' |'--------' '--------' | v '--------' | .--------. '->| dir B | | bad | | block! | '--------'

oh no! bad block detected, allocate replacement => .--------. .| root |-. || | | .----------------|| |-' | |'--------' | '-|-||-|-' | .--------' || '-------. | v |v


Если бы у нас было какое-то глобальное состояние, мы могли бы также сохранить флаг и избегать поиска потерянных объектов, если бы не знали, что были специально прерваны во время манипуляции с деревом каталогов (предзнаменование!).

**Проблема перемещения**

У нас есть ещё одна проблема: проблема перемещения. Сформулировать проблему просто:

Как атомарно переместить файл между двумя каталогами?

В littlefs мы можем атомарно фиксировать каталоги, но мы не можем создать атомарную фиксацию, охватывающую несколько каталогов. Файловая система должна пройти как минимум через два различных состояния, чтобы завершить перемещение.

Что ещё хуже, перемещение файлов является распространённой формой синхронизации для файловых систем. Поскольку файловая система разработана для потери питания, важно правильно реализовать атомарное перемещение.

Итак, что мы можем сделать?

— Мы определённо не можем просто позволить потере питания привести к дублированию или потере файлов. Это может легко нарушить код пользователей и проявится только в крайних случаях. Мы могли позволить себе быть небрежными со связанным списком с потоками, потому что он не предназначен для пользователей, и мы можем обрабатывать угловые случаи внутри.

— Некоторые файловые системы распространяют операции COW вверх по дереву, пока не будет найден общий родитель. К сожалению, это плохо взаимодействует с нашим связанным деревом и возвращает проблему распространения износа вверх.

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

В конце концов, решение проблемы перемещения потребовало создания нового механизма совместного использования. **Глобальное состояние**

В littlefs глобальное состояние привело к появлению механизма, который называется «глобальное состояние».

Глобальное состояние — это небольшой набор состояний, которые можно обновить из любой пары метаданных. Объединение глобального состояния с возможностью обновления нескольких записей в одной фиксации даёт нам мощный инструмент для создания сложных атомарных операций.

Как работает глобальное состояние?

Глобальное состояние существует как набор дельт, распределённых по парам метаданных в файловой системе. Фактическое глобальное состояние можно построить из этих дельт путём применения операции XOR ко всем дельтам в файловой системе.

Чтобы обновить глобальное состояние из пары метаданных, мы берём известное глобальное состояние и применяем операцию XOR к нему, нашим изменениям и любой существующей дельте в паре метаданных. Фиксация этой новой дельты в паре метаданных фиксирует изменения в глобальном состоянии файловой системы.

Для повышения эффективности мы всегда храним копию глобального состояния в оперативной памяти. Нам нужно только перебрать наши пары метаданных и построить глобальное состояние, когда файловая система смонтирована.

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

Теперь мы можем решить проблему перемещения. Мы можем создать глобальное состояние, описывающее наше перемещение атомарно с созданием нового файла, и мы можем очистить это состояние перемещения атомарно с удалением старого файла. |
   '--------'

begin move, add reference in dir C, change gstate to have move
=>
               .--------.    gstate = moving file D in dir A (m1)
              .| root   |-.
              ||        | |
.-------------||        |-'
|             |'--------'
|             '--|-|-|-'
|        .------'  |  '-------.
|       v          v           v
|  .--------.  .--------.  .--------.
'->| dir A  |->| dir B  |->| dir C  |
  ||        | ||        | || gdelta |
  ||        | ||        | || =m1    |
  |'--------' |'--------' |'--------'
  '----|---'  '--------'  '----|---'
       |     .----------------'
       v    v
     .--------.
     | file D |
     |        |
     |        |
     '--------'

complete move, remove reference in dir A, change gstate to no move
=>
               .--------.    gstate = no move (m1^~m1)
              .| root   |-.
              ||        | |
.-------------||        |-'
|             |'--------'
|             '--|-|-|-'
|        .------'  |  '-------.
|       v          v           v
|  .--------.  .--------.  .--------.
'->| dir A  |->| dir B  |->| dir C  |
  || gdelta | ||        | || gdelta |
  || =~m1   | ||        | || =m1    |
  |'--------' |'--------' |'--------'
  '--------'  '--------'  '----|---'
                               v
                           .--------.
                           | file D |
                           |        |
                           |        |
                           '--------'

If, after building our global state during mount, we find information describing an ongoing move, we know we lost power during a move and the file is duplicated in both the source and destination directories. If this happens, we can resolve the move using the information in the global state to remove one of the files.

                 .--------.    gstate = moving file D in dir A (m1)
                .| root   |-.             ^
                ||        |------------> xor
.---------------||        |-'             ^
|               |'--------'               |
|               '--|-|-|-'                |
|        .--------'  |  '---------.       |
|       |            |             |      |
|       |     .----------> xor --------> xor
|       v     |      v      ^      v      ^
|  .--------. |  .--------. |  .--------. |
'->| dir A  |-|->| dir B  |-|->| dir C  | |
  ||        |-' ||        |-' || gdelta |-'
  ||        |   ||        |   || =m1    |
  |'--------'   |'--------'   |'--------'
  '----|---'    '--------'    '----|---'
       |     .---------------------'
       v    v
     .--------.
     | file D |
     |        |
     |        |
     '--------'

We can also move directories the same way we move files. There is the threaded linked-list to consider, but leaving the threaded linked-list unchanged works fine as the order doesn't really matter.

               .--------.    gstate = no move (m1^~m1)
              .| root   |-.
              ||        | |
.-------------||        |-'
|             |'--------'
|             '--|-|-|-'
|        .------'  |  '-------.
|       v          v           v
|  .--------.  .--------.  .--------.
'->| dir A  |->| dir B  |->| dir C  |
  || gdelta | ||        | || gdelta |
  || =~m1   | ||        | || =m1    |
  |'--------' |'--------' |'--------'
  '--------'  '--------'  '----|---'
                               v
                           .--------.
                           | file D |
                           |        |
                           |        |
                           '--------'

begin move, add reference in dir C, change gstate to have move
=>
                .--------.    gstate = moving dir B in root (m1^~m1^m2)
               .| root   |-.
               ||        | |
.--------------||        |-'
|              |'--------'
|              '--|-|-|-'
|        .-------'  |  '----------.
|       v           |              v
|  .--------.       |          .--------.
'->| dir A  |-.     |       .->| dir C  |
  || gdelta | |     |       | || gdelta |
  || =~m1   | |     |       | || =m1^m2 | |'--------' |     |       | |'--------'
  '--------'  |     |       | '---|--|-'
              |     |    .-------'   |
              |     v   v   |        v
              |  .--------. |    .--------.
              '->| dir B  |-'    | file D |
                ||        |      |        |
                ||        |      |        |
                |'--------'      '--------'
                '--------'

Complete move, remove reference in root, change gstate to no move
=>
             .--------.    gstate = no move (m1^~m1^m2^~m2)
            .| root   |-.
            || gdelta | |
.-----------|| =~m2   |-'
|           |'--------'
|           '---|--|-'
|        .-----'    '-----.
|       v                  v
|  .--------.          .--------.
'->| dir A  |-.     .->| dir C  |
  || gdelta | |     | || gdelta |
  || =~m1   | |     '-|| =m1^m2 |-------.
  |'--------' |       |'--------'       |
  '--------'  |       '---|--|-'        |
              |        .-'    '-.       |
              |       v          v      |
              |  .--------.  .--------. |
              '->| dir B  |--| file D |-'
                ||        |  |        |
                ||        |  |        |
                |'--------'  '--------'
                '--------'

Global state gives us a powerful tool we can use to solve the move problem. And the result is surprisingly performant, only needing the minimum number of states and using the same number of commits as a naive move. Additionally, global state gives us a bit of persistent state we can use for some other small improvements.

Conclusion

And that's littlefs, thanks for reading!

[ctz-formula1]:

Примечание: в запросе присутствуют элементы форматирования, которые не удалось распознать и воспроизвести в ответе.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/openharmony-third_party_littlefs.git
git@api.gitlife.ru:oschina-mirror/openharmony-third_party_littlefs.git
oschina-mirror
openharmony-third_party_littlefs
openharmony-third_party_littlefs
master