Пришло время написать файловую систему. Файловая система сама себя не напишет. В этой половинке лабы мы таки реализуем файловую систему FAT32, прикрутим к ней драйвер SD-карты и чуть-чуть повзаимодействуем с ней через нашу интерактивную оболочку. Нулевая лаба Первая лаба: младшая половина и старшая половина Младшая часть. Продолжение под катом. В этой фазе мы будем реализовывать файловую систему FAT32. Исключительно read-only на данный момент. Основная работа будет вестисть в каталоге 2-fs/fat32. Данными на диске управляют одна или несколько файловых систем. Аналогично распределителям памяти, файловые системы отвечают за управление, выделение и освобождение памяти. С той лишь разницей, что это не быстрая оперативная память, а медленная и энергонезависимая память. Другими словами все изменения сохраняются на любой момент в будущем. В том числе и после перезагрузки компьютера. Есть много различных файловых систем. На Linux есть EXT4. На macOS есть HFS+ и APFS. На Windows есть NTFS. Некоторые файловые системы реализованны сразу для нескольких ОС. FAT32 — одна из таких. Она реализована для всех основных ОС включая Linux, macOS и Windows. Изначальна она использовалась в поздних версиях DOS и ранних версиях Windows. Главное приемущество FAT32 — вездесущность. Это одна из самых коросс-платформенных файловых систем. Для того, чтоб позволить более чем одной файловой системе находиться на диске, этот самый диск можно поделить на разделы. Каждый раздел можно независимо отформатировать для разных файловых систем. Чтоб разбить диск на разделы, на диске в определённое место записывается, где какой раздел начинается, где он закачивается и тип файловой системы, что этот раздел использует. Одной из широко распространённых систем является Master Boot Record (главная загрузочная запись) или просто MBR во имя краткости. MBR содержит в себе таблицу из четырёх записей, описывающих разделы. При этом некоторые разделы можно не объявлять как используемые. Есть чуть более современные схемы разделения вроде GPT, который помимо прочего поддерживает более четырёх разделов. В этом задании мы будем реализовывать код чтения MBR с диска, который в свою очередь включает один раздел FAT32. Эту комбинацию использует наша малинка: тоже MBR и тоже FAT32. На вот этой диаграмме показана физическая компоновка дискового раздела с MBR и FAT32:  В PDF-ке структуры FAT содержится вся необходимая информация о размерах и содержимом этих самых структур. Вместе с минимально необходимым описанием. Мы будем использовать документ при реализации нашей файловой системы. Помимо этого полезно будет изучить соответсвующую статью из википедии. MBR всегда находится в нулевом секторе диска. MBR содержит четыре записи разделов. Каждая из этих записей содержит в себе: тип раздела, смещение раздела в секторах и разные флаги вроде того, является ли этот раздел загрузочным. Все остальные поля вроде CHS (цилиндр, головка, сектор) можно целиком и полностю игнорировать. Так поступает большинство современных реализаций. Стоит ещё заметить, что тип раздела для FAT32 равен 0xB или 0xC. Первый сектор раздела FAT32 содержит расширенный блок параметров BIOS. Сокращённо EBPB. Сам этот блок начинается с блока параметров BIOS или BPB. Вместе они определяют все необходимые параметры компоновки файловой системы FAT. Есть одна область в EBPB, на которую стоит обратить отдельное внимание. Та которая определяет количество зарезервированных секторов (number of reserved sectors). Это смещение от начала раздела FAT32 в секторах, где FAT могут быть найдены. Сразу после последнего FAT будет область, содержащая данные для кластеров. Сейчас мы подробнее рассотрим FAT-ы, область данных, кластеры и вот это всё. Все данные, которые хранятся в файловой системе FAT, разделяются на кластеры. В EBPB есть поле, из которого можно найти, сколько в каждом кластере секторов (number of sectors per cluster). Нумерация кластеров начинается с цифры 2. Как видно из диаграммы, данные для кластера 2 расположены в начале области данных. Данные для кластера 3 расположены сразу после кластера 2 и далее в таком духе. FAT расшифровывается как file allocation table. Таблица распределения файлов. Исходя из названия FAT это таблица (массив) записей FAT. В FAT32 каждая такая записть имеет размер в 32 бита. Размер же всей этой таблицы определяется полями sectors per FAT и bytes per sectors из EPBP. Для избыточности в файловой системе может быть более одного FAT (во имя пресвятого бекапа!). Количество таблиц так же можно найти в EPBP. Смотреть поле number of FATs. Помимо записей за номерами 0 и 1 каждая из FAT-записей определяет статус кластера. Записть за номером 2 определяет статус кластера 2. Запить 3 определяет статус кластера 3. И далее по списку. Каждому кластеру свою FAT-запись. Записи 0 и 1 скорее всего такие: Помимо этих двух записей все остальные соотвесвуют определённому кластеру из области данных. Хотя FAT-записи имеют полный размер в 32 бита, используются только 28 бит. Верхние 4 бита игнорируются. И значения могут быть такие: Кластеры образуют цепочки кластеров. По сути это связанный список из кластеров. Если кластер используется для данных, то его значение содержит либо ссылку на следующий кластер, либо является маркером EOC, указывающем конец цепочки. В качестве примера рассмотрим диаграммку с 8-ю FAT-записями:  Кластеры раскрашены по цветам так, чтоб можно было проще разобраться, что к какой цепочке принадлежит. Первые две записи это ID и EOC. Запись 2 указывает, что соответсвующий кластер является кластером данных и эта цепочка (зелёная) размером в один кластер. Запись 3 указывает, что кластер 3 содержит данные и следующим в цепочке (синей) будет кластер 5 с данными, который ссылается на кластер 6, который эту цепочку обрывает. Аналогичным образом кластеры 7 и 5 образуют цепочку (красная). Кластер за номером 8 свободен и не используется. Цепочка кластеров — это данные для файла или каталога. Каталог — суть специальный файлик, в котором содержатся имена файлов и все прочие метаданные. Внутри каталок представляет собой массив каталожных записей. Каждая такая запись содержит имя, стартовый кластер и является ли эта запись каталогом или просто файлом. Есть один специальный каталог, который не связан с записями в других каталогах. Корневой каталог. Стартовый кластер для корнегого каталога можно найти в EBPB. Через это всё можно определить место всех других файлов и каталогов. По историческим причинам каждая запись физического каталога можно интерпретировать аж двумя разными способами. Поле атрибутов так же указывает на один из этих способов. Вот эти две вариации: Длинное имя файла (LFN) добавлено в FAT32 для того, чтоб использовать имена файлов длиннее 11 символов. Если запись имеет имя длинной более 11 символов, то ей предшествуют записи LFN. При этом эти записи не сортированы физически. Вместо этого они содержат поле для того, чтоб определить последовательность. Таким образом на физический порядок записей LFN полагаться не получится. Прежде чем продолжить надо разобраться со структурами FAT. После этого постарайтесь ответить на следующие вопросы: Написание файловой системы является достаточно сурьёзным делом. FAT32 даже при том, что мы её будем только читать, не исключение. Предоставленный код в крейте 2-fs/fat32 обеспечивает в основном базовую структуру, но многие дизайнерские решения и большая часть реализации целиком принадлежит вам. Сейчас займёмся описанием того, что уже готово. Почитайте код из каталога fat32/src. Там можно найти модуль traits. Точкой входа будет traits/mod.rs. Там можно найти примерно семь трейтов и одну структурку. При реализации файловой системы мы в том числе будем реализовывать это всё. Там есть одна структура Dummy, которая обеспечивает фиктивную реализацию большинства трейтов. Этот тип можно использовать как заглушку. Если присмотреться к коду, то эта заглушка используется в некоторых местах. Может и вам пригодится. Советую читать код из traits/ в следующем порядке: Доступ к диску напрямую — это достаточно дорогая операция. По этому весь доступ будет выполняться на кешированных секторах. Структуру CachedDevice можно найти в файле vfat/cache.rs. Оная обеспечивает прозрачный и явный доступ к кешам сектора. По сути это обёртка над BlockDevice, которая внутри себя использует HashMap в качестве хранилища. Ключём в HashMap будет номер сектора. Как только вы реализуете CachedDevice, его можно прозрачно использовать как кешированную версию BlockDevice. В дополнение предоставляются методы get() и get_mut(), которые позволяют напрямую ссылаться на кешированные сектора. Помимо этого структура CachedDevice обязана следить за соответствием между логическими секторами и физическими секторами, которые определяются EBPB. Для этого предоставлен метод virtual_to_physical(). Этот самый метод следует использовать для того, чтоб определить сколько физических секторов потребуется прочитать для данного логического сектора. Файл util.rs содержит один полезный трейт и его реализацию для срезов (&[T]) и динамических массивов (Vec<T>). Это можно использовать для переноса одного в другое при сохранении определённых условиях. Например для того, чтоб скастовать &[u32] в &[u8] можно использовать вот такое: Структуру MasterBootRecord можно найти в файле mbr.rs. Она отвечает за чтение и анализ MBR из BlockDevice. Аналогично можно использовать структуру BiosParameterBlock. Её можно найти в файле vfat/ebpb.rs. Она отвечает за чтение и анализ BPB и EBPB раздела FAT32. Структуру Shared<T> из vfat/shared.rs можно использовать для безопасного мутабельного доступа типу T. Пригодится при реализации файловой системы. Особенно когда нам потребуется возможность совместного доступа к ФС из разных частей кода. Прежде чем продолжить, убедитесь, что понимаете, как и зачем пригодится использование Shared<T>. Само ядро файловой системы можно найти в файле vfat/vfat.rs. Очевидно это структура VFat. Как можно заметить, структура содержит в себе CachedDevice. Реализация должна обернуть предоставленный BlockDevice в CachedDevice. Частичная реализация свойств FileSystem для типа &Shared<VFat> уже присутствует. Помимо этого можно заметить, что метод from() возвращает Shared<VFat>. Основная задача — завершить реализацию метода from() и некоторых необходимых свойств FileSystem для &Shared<VFat>. Это тянет за собой реализацию остальных структур, которые реализуют необходимые кусочки свойств файловой системы. Ещё каталоге vfat/ можно найти: При реализации файловой системы надо будет дополнить всё это необходимым кодом. Не бойтесь дополнять любые из этих структур методами, которые кажутся вам необходимыми. Однако не изменяйте ни одно из предоставленных определений трейтов или сигнатур у существующих методов. Прочитайте сейчас весь код начиная с vfat.rs и убедитесь, что вы понимаете, что там происходит. Теперь у нас есть всё необходимое для реализации файловой системы FAT32. Вы можете заниматься реализацией в том порядке, в котором вам больше нравиться. Мы предоставляем некоторый набор достаточно строгих тестов для проверки реализации. Перед запуском тестов запустите make clean && make fetch в каталоге 2-fs. Оно загрузит несколько файликов в 2-fs/files/resources/. Эти файлы используются модульными тестами. В этом каталоге вы найдёте образы, которые содержат внутри MBR, EBPB и FAT32, а так же хеши, которые используются для проверки разных частей реализации. Возможно вы найдёте полезным проанализировать образы при помощи hex-редакторов вроде *Bless в Linux или Hex Fiend на macOS. Тесты можно запустить при помощи cargo test. Для того, чтоб увидеть отладочные сообщения можно выполнить cargo test -- --nocapture. Это предотвращает перехват stdout и stderr. Кроме того вы можете свободно добавлять собственные тесты в том количестве, в котором сочтёте необходимым. Чтоб предотвратить конфликты слияния рекомендуется добавить тесты в файлик с именем, отличным от tests.rs. Рекомендуется также следовать вот этим правилам: Вы можете делать всё в том порядке, в каком хотите. Но вот такой порядок рекомендуем: После того, как ваша реализация пройдёт через все юнит-тесты и будет работать так, как от неё ожидают, вы можете насладиться этой маленькой победой. Вы реализовали целую файловую систему! После того, как эйфория пройдёт, можно переходить к следующему этапу. В этой части мы будем взаимодействовать с существующим драйвером контроллера SD-карты для Raspbrerry Pi 3, используя Foreign function interface или FFI для краткости. О FFI в Rust можно читнуть в главе 19.1 книги по Rust. Помимо этого мы создадим глобальный дескриптор для файловой системы в нашей операционной системе. Работать будем в основном в os/kernel/src/fs. FFI в Rust позволяет коду взаимодействовать с программным обеспечением, написанным на других языках программирования и наоборот. Внешние, по отношению к Rust, элементы объявляются в блоке extern: Тут объявляется внешняя функция outside_function и внешняя глобальная переменная outside_global. Использовать их можно следующим образом: Обратите внимание, что тут требуется использовать блок unsafe. Rust требует этого, поскольку он не может гарантировать правильность указанных объявлений. Компилятор слепо подставляет эти вызовы функций и взаимодействия с переменными. Другими словами, как и в любых других случаях использования небезопасного кода, Rust предполагает, что вы всё сделали правильно. При этом всём на этапе линковки символы outside_function и outside_global должны существовать. Иначе программа не соберётся. Для вызова функции Rust из внешнего кода, местоположение функции (адрес в памяти) должно быть экспортировано в качестве определённого символа. Внутри Rust может свободно искажать (mangles) символы, которые присваиваются функциям. Для управления версиями и всем таким. Получается, что по умолчанию нельзя узнать заранее, какой символ будет присвоен каждой функции и следовательно мы не сможем вызвать эту функцию из внешнего кода. Для предотвращения этого произвола процесса мы можем добавить атрибут #[no_mangle]: Затем программа на (например) Няшном Си может вызвать эту функцию таким образом: Мы предоставили предварительно скомпилированную библиотеку с драйвером SD-карты как os/kernel/ext/libsd.a. Помимо этого эта библиотека включена в процесс сборки. Т.е. библиотека уже связана с ядром. Кроме того в os/kernel/src/sd.rs предоставлены объявления всего, что экспортирует эта библиотека. Сама библиотека зависит от функции wait_micros, которую она ожидает найти в нашем ядрышке. Функция должна отправлять процессор в сон на указанное количество микросекунд. Вам нужно будет создать и экспортировать эту функцию для успешной линковки. В Няшном Си объявление этой функции выглядит следующим образом: Задача — обернуть внешний небезопасный API в безопасный Rust-код. Реализуйте структуру Sd, которая инициализирует контроллер SD-карты в методе new(). Затем реализуйте трейт BlockDevice для Sd. Вам нужно будет использовать unsafe для взаимодействия с внешними элементами. Проверьте свою реализацию, вручную прочитав MBR прямо из kmain. Убедитесь, что прочитанные байтики соответсвуют ожидаемым. Когда всё заработает так, как ожидается, переходите к следущему разделу. В этой части мы будем инициализировать глобальную файловую систему для использования нашим ядром. Основная работа в kernel/src/fs/mod.rs. Как и аллокатор памяти, файловая система является глобальным ресурсом. Мы хотим, чтоб оно было доступно всегда и везде. Для того, чтоб это работало, мы создали глобальную переменную static FILE_SYSTEM: FileSystem в файле kernel/src/kmain.rs. Как и аллокатор, наша ФС начинает свою работу в неинициализированном состоянии. На текущий момент у нас есть файловая система и драйвер диска. Пришло время связать их вместе. Доделайте реализацию структуры FileSystem из kernel/src/fs/mod.rs, используя при этом файловую систему FAT32 и наши биндинги к драйверу SD-карты. Вы должны инициализировать файловую систему при помощи Sd (реализующей BlockDevice) в функции initialize(). Затем реализуйте трейт FileSystem для структуры, переведя все вызовы на VFat. И в конце убедитесь, что инициализируете файловую систему из kmain после аллокатора памяти. Проверьте свою реализацию, распечатав содежримое корневого каталога (/) вашей SD-карты. Как только всё заработает так, как вы ожидаете — переходите к следующему этапу. В этой фазе будем реализовывать команды cd, pwd, ls и cat для нашей интерактивной строки. Работа ведётся в файлике os/kernel/src/shell.rs. Вероятно вы уже знакомы с понятием рабочего каталога. Текущий рабочий каталог (cwd от current working directory) — это такой каталог, от которого рассчитываются относительные пути к файлам. Например если cwd равно /a, то если обратиться к файлу hello этот самый файл будет искаться под именем /a/hello. Если cwd переключить на /a/b/c, то доступ к файлу hello будет аналогичен доступу к файлу /a/b/c/hello. Символ / может быть добавлен к началу любого пути и в таком случае оный будет считаться абсолютным, а не относительным. Другими словами не будет использовать текущий рабочий каталог. Таким образом обращение к /hello всегда будет ссылаться на файл с именем hello в корневом каталоге вне зависимости от текущего рабочего каталога. В нашей оболочке текущий рабочий каталог можно будет изменить при помощи команды cd <dir>. Например ежели запустить команду cd /hello/there, то cwd станет равным /hello/there. Если после этого запустить cd you, то cwd станет равным cd /hello/there/you. Большинство операционных систем предоставляют специальный системный вызов для изменения рабочего каталога процесса. Поскольку наша ОС ещё не имеет процессов и системных вызовов, мы будем следить за изменениями cwd непосредсвенно силами нашей интерактивной оболочки. Вы реализуете четыре команды, которые позволяют взаимодействовать с файловой системой посредством интерактивной оболочки: cd, pwd, ls и cat. В контексте этого задания они определяются следующим образом: Все не абсолютные пути должны быть обработаны относительно текущего рабочего каталога. Все абсолютные пути должны обрабатываться от корня. Пример действия команд можно посмотреть в гифке выше. Когда вы будете реализовывать эти команды — можете выводить ошибки или содержимое каталогов любым способом, каким сочтёте нужным. Главное, чтоб вся необходимая информация присутствовала. Расширьте os/kernel/src/shell.rs реализацией этих четырёх команд. Используйте мутабельный PathBuf для того, чтоб отслеживать текущий рабочий каталог. Этот самый PathBuf должен изменяться каждым вызовом cd. Будет полезно создать функции с общей сигнатурой для каждой из ваших команд. Для дополнительного уровня типобезопасности можете выделить трейт, как абстракцию для команд и реализовывать этот трейт для каждой команды. После того, как вы реализовали, протестировали и проверили все четыре команды с учётом указанных спецификаций — задача этой лабы решена. Поздравляем! Такие дела. Rust Evangelist