Пришло время написать файловую систему. Файловая система сама себя не напишет. В этой половинке лабы мы таки реализуем файловую систему FAT32, прикрутим к ней драйвер SD-карты и чуть-чуть повзаимодействуем с ней через нашу интерактивную оболочку.


Нулевая лаба


Первая лаба: младшая половина и старшая половина


Младшая часть. Продолжение под катом.


Фаза 2: 32-битные липиды


В этой фазе мы будем реализовывать файловую систему 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:


MBR и FAT32


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


Master Boot Record


MBR всегда находится в нулевом секторе диска. MBR содержит четыре записи разделов. Каждая из этих записей содержит в себе: тип раздела, смещение раздела в секторах и разные флаги вроде того, является ли этот раздел загрузочным. Все остальные поля вроде CHS (цилиндр, головка, сектор) можно целиком и полностю игнорировать. Так поступает большинство современных реализаций. Стоит ещё заметить, что тип раздела для FAT32 равен 0xB или 0xC.


Extended Bios Parameter Block


Первый сектор раздела 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 и далее в таком духе.


File Allocation Table


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 скорее всего такие:


  • Запись 0: 0xFFFFFFFN, который ID.
  • Запись 1: Маркер конца цепочки кластеров (EOC).

Помимо этих двух записей все остальные соотвесвуют определённому кластеру из области данных. Хотя FAT-записи имеют полный размер в 32 бита, используются только 28 бит. Верхние 4 бита игнорируются. И значения могут быть такие:


  • 0x?0000000: Пустой неиспользуемый кластер.
  • 0x?0000001: Зарезервировано.
  • 0x?0000002-0x?FFFFFEF: Кластер данных. Конкретное значение — следующий кластер в цепочке.
  • 0x?FFFFFF0-0x?FFFFFF6: Зарезервировано.
  • 0x?FFFFFF7: Зарезервированный или испорченый кластер.
  • 0x?FFFFFF8-0x?FFFFFFF: Последний кластер в цепочке. Должен быть маркером EOC.

Цепочка кластеров


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


В качестве примера рассмотрим диаграммку с 8-ю FAT-записями:


Цепочка кластеров


Кластеры раскрашены по цветам так, чтоб можно было проще разобраться, что к какой цепочке принадлежит. Первые две записи это ID и EOC. Запись 2 указывает, что соответсвующий кластер является кластером данных и эта цепочка (зелёная) размером в один кластер. Запись 3 указывает, что кластер 3 содержит данные и следующим в цепочке (синей) будет кластер 5 с данными, который ссылается на кластер 6, который эту цепочку обрывает. Аналогичным образом кластеры 7 и 5 образуют цепочку (красная). Кластер за номером 8 свободен и не используется.


Каталоги и записи


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


Есть один специальный каталог, который не связан с записями в других каталогах. Корневой каталог. Стартовый кластер для корнегого каталога можно найти в EBPB. Через это всё можно определить место всех других файлов и каталогов.


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


  • Обычная каталожная запись. (regular directory entry)
  • Запись для длинного имени файла. (long file name entry)

Длинное имя файла (LFN) добавлено в FAT32 для того, чтоб использовать имена файлов длиннее 11 символов. Если запись имеет имя длинной более 11 символов, то ей предшествуют записи LFN. При этом эти записи не сортированы физически. Вместо этого они содержат поле для того, чтоб определить последовательность. Таким образом на физический порядок записей LFN полагаться не получится.


Итак


Прежде чем продолжить надо разобраться со структурами FAT. После этого постарайтесь ответить на следующие вопросы:


Каким образом определить, содержит ли первый сектор MBR-структуру? [mbr-magic]

Первый сектор диска может не содержать MBR. Каким образом можно определить есть ли там MBR или его там нет?



Каково максимальное количество кластеров FAT32? [max-clusters]

Дизайн FAT32 подразумевает некоторое количество ограничений. Какое максимальное количество кластеров в FAT32 и откуда эти ограничения происходят? А если взять FAT16, то там будут те же самые ограничения или другие?



Какой максимальный размер одного файла? [max-file-size]

Есть ли какие либо ограничения на максимальный размер файла? Если есть, то каков максимальный размер файла и что определяет эту границу?

Подсказка: Посмотрите на структуру записи в каталоге.



Как определить, перед нами запись LFN или другая? [lfn-identity]

Если внимательно посмотреть на записи в каталоге, то какие именно байтики определяют, LFN перед нами или обычная запись? Конкретно, какие это байты и какие у них должны быть значения?



Каким образом можно найти /a/b/c.txt [manual-lookup]

Не забывая про EBPB, опишите все шаги, которые вы предпримите для того, чтоб найти начальный кластер для файла /a/b/c.txt.

Структура кода


Написание файловой системы является достаточно сурьёзным делом. FAT32 даже при том, что мы её будем только читать, не исключение. Предоставленный код в крейте 2-fs/fat32 обеспечивает в основном базовую структуру, но многие дизайнерские решения и большая часть реализации целиком принадлежит вам.


Сейчас займёмся описанием того, что уже готово. Почитайте код из каталога fat32/src.


Трейты файловой системы


Там можно найти модуль traits. Точкой входа будет traits/mod.rs. Там можно найти примерно семь трейтов и одну структурку. При реализации файловой системы мы в том числе будем реализовывать это всё.


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


Советую читать код из traits/ в следующем порядке:


  • BlockDevice из traits/block_device.rs. Файловая система будет отвязана генериками от физического/виртуального хранилища. Другими словами файловая система будет работать на любом устройстве, пока это самое устройство реализует BlockDevice. В процессе реализации/тестирования можно использовать реализацию BlockDevice поверх обычного файла. Во имя удобства конечно же! А вот на малинке мы в BlockDevice завернём драйвер SD-карты вместе с контроллером EMMC и этим всем. Разницы при этом почти не заметим.
  • File, Dir и Entry из traits/fs.rs. Эти трейты определяют, какими минимальными свойствами должны обладать файл, каталог или их обобщение в файловой системе. Обратите внимание на зависимость их друг от друга. Например свойства Entry используют ассоциированный с ним тип File.
  • FileSystem из traits/fs.rs. Данный трейт определяет свойства файловой системы. В том числе и через привязку к остальным трейтам. Например требует тип, реализующий File для этой файловой системы. Таким образом гарантируется, что для каждой реализации FileSystem есть только одна реализация File, Dir и Entry.
  • Metadata и Timestamp из traits/metadata.rs. Каждая Entry должна быть связана с некоторыми метаданными, которые позволяют получать сведения о файле или каталоге. За эти метаданные отвечает Metadata. А Timestamp в свою очередь определяет набор свойств для определённых моментов во времени. Этот трейт используется для таких штук, как время создания файла.

Кеш-устройство



Доступ к диску напрямую — это достаточно дорогая операция. По этому весь доступ будет выполняться на кешированных секторах. Структуру CachedDevice можно найти в файле vfat/cache.rs. Оная обеспечивает прозрачный и явный доступ к кешам сектора. По сути это обёртка над BlockDevice, которая внутри себя использует HashMap в качестве хранилища. Ключём в HashMap будет номер сектора. Как только вы реализуете CachedDevice, его можно прозрачно использовать как кешированную версию BlockDevice. В дополнение предоставляются методы get() и get_mut(), которые позволяют напрямую ссылаться на кешированные сектора.


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


Полезности


Файл util.rs содержит один полезный трейт и его реализацию для срезов (&[T]) и динамических массивов (Vec<T>). Это можно использовать для переноса одного в другое при сохранении определённых условиях. Например для того, чтоб скастовать &[u32] в &[u8] можно использовать вот такое:


use util::SliceExt;

let x: &[u32] = &[1, 2, 3, 4];
assert_eq!(x.len(), 4);

let y: &[u8] = unsafe { x.cast() };
assert_eq!(y.len(), 16);

MBR и EBPB


Структуру MasterBootRecord можно найти в файле mbr.rs. Она отвечает за чтение и анализ MBR из BlockDevice. Аналогично можно использовать структуру BiosParameterBlock. Её можно найти в файле vfat/ebpb.rs. Она отвечает за чтение и анализ BPB и EBPB раздела FAT32.


Shared


Структуру Shared<T> из vfat/shared.rs можно использовать для безопасного мутабельного доступа типу T. Пригодится при реализации файловой системы. Особенно когда нам потребуется возможность совместного доступа к ФС из разных частей кода. Прежде чем продолжить, убедитесь, что понимаете, как и зачем пригодится использование Shared<T>.


Файловая система


Само ядро файловой системы можно найти в файле vfat/vfat.rs. Очевидно это структура VFat. Как можно заметить, структура содержит в себе CachedDevice. Реализация должна обернуть предоставленный BlockDevice в CachedDevice.


Что за VFAT?

VFAT — это ещё одна файловая система от Microsoft, которая является предшественником FAT32. По разным историческим причинам это стало синонимом FAT32. Мы продолжим эту глупую традицию с не всегда корректными названиями.

Частичная реализация свойств FileSystem для типа &Shared<VFat> уже присутствует. Помимо этого можно заметить, что метод from() возвращает Shared<VFat>. Основная задача — завершить реализацию метода from() и некоторых необходимых свойств FileSystem для &Shared<VFat>. Это тянет за собой реализацию остальных структур, которые реализуют необходимые кусочки свойств файловой системы.


Ещё каталоге vfat/ можно найти:


  • error.rs. Содержит перечисление Error с возможными ошибками при инициализации FAT32.
  • file.rs. Содержит заготовку структуры File, которая должна реализовывать трейт traits::File.
  • dir.rs. Аналогично file.rs. Помимо этого содержит заготовки для структур в том виде, как они записаны на диске.
  • entry.rs. Содержит заготовку структуры Entry, которая должна реализовывать трейт traits::Entry.
  • metadata.rs. Содержит структуры Data, Time, Attributes для работы с сырыми свойствами файлов. И недописанные структуры Timestamp, Metadata, которые должны реализовывать соответствующие трейты из модуля traits.
  • fat.rs. Содержит структуру FatEntry. Эта самая структура обёртывает FAT-записи и может быть использована для лёгкого и непринуждённого считывания соответствующей FAT-записи.
  • cluster.rs. Содержит структуру кластера, которая обертывает физический номер кластера и может использоваться для чтения номера логического кластера.

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


Прочитайте сейчас весь код начиная с vfat.rs и убедитесь, что вы понимаете, что там происходит.


Реализация


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


Убедитесь, что обновили все предоставленные заготовки!

Убедитесь, что все ваши копии репозиториев находятся в актуальном состоянии. Стащите последние версии 2-fs и os с помощью git pull и поправьте всё необходимое.

Мы предоставляем некоторый набор достаточно строгих тестов для проверки реализации. Перед запуском тестов запустите 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.


Рекомендуется также следовать вот этим правилам:


  • Используйте осмысленные типы везде, где это возможно. Например вместо использования u16 для поля времени можно использовать структуру Time
  • Избегайте unsafe на столько, на сколько это возможно. Наша реализация использует в общей сложности четыре не-union строки с unsafe и три строки для обработки union. Ваша реализация должна стараться следовать этому.
  • Избегайте дублирования, используя всякие вспомогательные методы. Часто полезно вынести общий вариант поведения во вспомогательный метод. Постарайтесь делать это, когда оно имеет смысл.
  • Убедитесь, что ваша реализация не зависит от размера кластера или сектора. Не хардкорьте какие либо конкретные значения размеров сектора или размеров кластера. Ваша реализация должна работать с любыми размерами кластера и сектора, которые кратны 512 и взяты из EBPB.
  • Не буферезируйте дважды без необходимости. Убедитесь, что не читаете сектора в память, если они уже есть в кеше. Старайтесь использовать память без фанатизма.

Вы можете делать всё в том порядке, в каком хотите. Но вот такой порядок рекомендуем:


  1. Реализуйте разбор MBR в mbr.rs. Вероятно для реализации потребуется использование unsafe. Но будет достаточно одной строчки. Скорее всего slice::from_raw_parts_mut() или mem::transmute(). mem::transmute() невероятно мощный инструмент. Избегайте его использования на столько, на сколько получается. В противном случае вы должны полностью понимать, что делаете. Для реализации Debug используйте debug_struct() из Formatter. Можете посмотреть предоставленую для CachedDevice реализацию Debug.
  2. Реализуйте разбор EBPB в ebpb.rs. Как и в случае с MBR, тут должно хватить одной строки с unsafe.
  3. Протестируйте реализации MBR и EBPB. Попробуйте написать тесты для более тщательного тестирования. Обратите внимание на реализацию BlockDevice для Cursor<&mut [u8]>. Кроме того вы можете красиво вывести структуру при помощи:

    println!("{:#?}", x);
  4. Реализуйте CachedDevice в vfat/cached.rs.
  5. Реализуйте VFat::from() в vfat/vfat.rs. Используйте MasterBootRecord, BiosParameterBlock и CachedDevice для реализации. Протестируйте ваши реализации также как MBR и EBPB.
  6. Реализуйте FatEntry в vfat/fat.rs.
  7. Реализуйте VFat::fat_entry, VFat::read_clusterи VFat::read_chain. Эти вспомогательные методы абстрагируют чтение из Cluster или цепочки кластеров в буфер. Для реализации этих методов вам возможно понадобятся некоторые дополнительные методы. Например вычисление сектора диска из номера кластера. Можете добавлять такие методы свободно. А ещё можно использовать метод VFat::fat_entry для реализации двух других.
  8. Допишите vfat/metadata.rs. Типы Date, Time и Attributes быть идентичны по структуре тем, что хранятся на диске. При их реализации обратитесь к описанию структур FAT. Типы Timestamp и Metadata не имеют аналогичной структуры на диске, но они служат более удобными абстракциями над исходными структурами на диске и будут полезны при реализации трейтов Entry, File и Dir.
  9. Реализуйте Dir в vfat/dir.rs и Entry в vfat/entry.rs.
    Начните с добавления необходимых полей к Dir, которые должны хранить начальный Cluster каталога и Shared<VFat>. Возможно вы захотите предоставить реалистичные свойства для типа File из vfat/file.rs. Кроме того будет полезным создать дополнительную структуру, которая реализует Iterator<Item=Entry> и возвращать эту структуру из метода entries(). При реализации entries() будет достаточно одной строки unsafe. Кроме того тут могут быть особенно полезны VecExt и SliceExt. Читайте описание структур FAT — там много информации, необходимой для реализации Dir.

    Разбор Entry
    Поскольку запись может быть либо записью LFN, либо обычной записью, придётся использовать union для представления записи с диска. Заготовка уже предоставлена в виде VFatDirEntry. Подробнее об объединениях в Rust можно читнуть в документации. Об объединениях вообще можно читнуть в википедии.

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

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

    И наконец нам потребуется декодировать символы в кодировке UTF-16 при анализе записей LFN. Для этого надо использовать функцию decode_utf16(). Будет полезно хранить символы UTF-16 в одном или нескольких массивах Vec<u16> при анализе длинного имени файла.
    Dir::find()

    Вы должны реализовать Dir::find() после того как реализуете traits::Dir для Dir. Обратите внимание на то, что Dir::find() должна быть независима от регистра символов. При этом реализация должна быть относительно короткой. Как один из вариантов можно использовать eq_ignore_ascii_case() для выполнения сравнения без учёта регистра символов.
  10. Реализуйте File в vfat/file.rs. Начните с добавления полей, которые хранят первый Cluster в цепочке и Shared<VFat>. Затем реализуйте traits::File для типа File. Возможно потребуется немного пофиксить entries() из Dir.
  11. Реализуйте VFat::open() в vfat/vfat.rs. Используйте components() для того, чтоб пройтись по всем компонентам Path. Обратите внимание, что реализация, предоставленная нашим вариантом std, не содержит каких либо методов, требующих поддержки со стороны операционной системы. Другими словами там нет всяких read_dir(), is_file(), is_dir() и многих других.

    Используйте метод Dir::find(). Реализация VFat::open() должна получаться достаточно короткой. Наша состоит из примерно 17 строк. Вы также можете счесть полезным добавление к Dir вспомогательных методов.

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


Седлаем SD-карту



В этой части мы будем взаимодействовать с существующим драйвером контроллера SD-карты для Raspbrerry Pi 3, используя Foreign function interface или FFI для краткости. О FFI в Rust можно читнуть в главе 19.1 книги по Rust. Помимо этого мы создадим глобальный дескриптор для файловой системы в нашей операционной системе. Работать будем в основном в os/kernel/src/fs.


Foregin Function Interface


FFI в Rust позволяет коду взаимодействовать с программным обеспечением, написанным на других языках программирования и наоборот. Внешние, по отношению к Rust, элементы объявляются в блоке extern:


extern {
    static outside_global: u32;
    fn outside_function(param: i16) -> i32;
}

Тут объявляется внешняя функция outside_function и внешняя глобальная переменная outside_global. Использовать их можно следующим образом:


unsafe {
    let y = outside_function(10);
    let global = outside_global;
}

Обратите внимание, что тут требуется использовать блок unsafe. Rust требует этого, поскольку он не может гарантировать правильность указанных объявлений. Компилятор слепо подставляет эти вызовы функций и взаимодействия с переменными. Другими словами, как и в любых других случаях использования небезопасного кода, Rust предполагает, что вы всё сделали правильно. При этом всём на этапе линковки символы outside_function и outside_global должны существовать. Иначе программа не соберётся.


Для вызова функции Rust из внешнего кода, местоположение функции (адрес в памяти) должно быть экспортировано в качестве определённого символа. Внутри Rust может свободно искажать (mangles) символы, которые присваиваются функциям. Для управления версиями и всем таким. Получается, что по умолчанию нельзя узнать заранее, какой символ будет присвоен каждой функции и следовательно мы не сможем вызвать эту функцию из внешнего кода. Для предотвращения этого произвола процесса мы можем добавить атрибут #[no_mangle]:


#[no_mangle]
fn call_me_maybe(ptr: *mut u8) { .. }

Затем программа на (например) Няшном Си может вызвать эту функцию таким образом:


void call_me_maybe(unsigned char *);

call_me_maybe(...);

Почему Rust не может гарантировать безопасность использования внешнего кода? [foreign-safety]

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



Почему Rust калечит символы? [mangling]

Няшный Си не занимается переименовыванием всего этого. C++ и Rust занимаются. Чем эти два языка отличаются, раз они требуют такого отношения к символам? Предоставьте конкретный пример того, что произойдёт, если Rust не будет это всё делать.

Драйвер SD-карты


Мы предоставили предварительно скомпилированную библиотеку с драйвером SD-карты как os/kernel/ext/libsd.a. Помимо этого эта библиотека включена в процесс сборки. Т.е. библиотека уже связана с ядром. Кроме того в os/kernel/src/sd.rs предоставлены объявления всего, что экспортирует эта библиотека.


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


/*
 * Sleep for `us` microseconds.
 */
void wait_micros(unsigned int us);

Задача — обернуть внешний небезопасный API в безопасный Rust-код. Реализуйте структуру Sd, которая инициализирует контроллер SD-карты в методе new(). Затем реализуйте трейт BlockDevice для Sd. Вам нужно будет использовать unsafe для взаимодействия с внешними элементами. Проверьте свою реализацию, вручную прочитав MBR прямо из kmain. Убедитесь, что прочитанные байтики соответсвуют ожидаемым. Когда всё заработает так, как ожидается, переходите к следущему разделу.


Подсказка: На 64-битном ARM unsigned int из Няшного Си станет u32 в Rust.



Является ли ваша реализация потокобезопасной? [foreign-sync]

Предоставленный предварительно скомпилированный драйвер SD-карты использует глобальную переменную (sd_err) для отслеживания ошибок безо всякой синхронизации. Таким образом эта часть даже не пытается быть потокобезопасной. Как это влияет на правильность наших обвязок? Напомним, что мы должны поддерживать гарантии вокруг гонок данных в Rust при использовании unsafe-кода. Является ли наш связующий код потокобезопасным? Почему да или почему нет?
Подсказка: Скорее всего являются! (Если нет, то должны являться) Что реализует эти гарантии?


Файловая система


В этой части мы будем инициализировать глобальную файловую систему для использования нашим ядром. Основная работа в 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-карты. Как только всё заработает так, как вы ожидаете — переходите к следующему этапу.


Фаза 4: Mo’sh


В этой фазе будем реализовывать команды 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. В контексте этого задания они определяются следующим образом:


  • pwd (print the working directory). Распечатывает полный путь к текущему рабочему каталогу.
  • cd <directory> (change (working) directory). Изменяет текущий рабочий каталог на directory. Требует аргумента directory.
  • ls [-a] [directory] (list the files in a directory). Перечисляет все файлы в каталоге. -a и directory являются необязательными аргументами. Если передан флаг -a, то должны отображаться скрытые файлы. В противном случае такие файлы отображаться не должны. Если directory не передан, то отображаются записи из текущего рабочего каталога. В противном случае отображаются записи из каталога directory. Эти аргументы могут быть использованы вместе. Однако -a должен стоять перед directory. Недопустимые аргументы должны приводить к ошибкам. Кроме того должна выводиться ошибка, если каталог не сущесвует.
  • cat <path..> (concatenate files). Выводит содержимое файлов по указанным путям path один за другим. Требуется по меньшей мере один такой аргумент. Если путь не указывает на реально сущесвующий файл — выводить ошибку. Если файл содержит недопустимый контент в контексте кодировки UTF-8, то также выводить ошибку.

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


Реализация


Расширьте os/kernel/src/shell.rs реализацией этих четырёх команд. Используйте мутабельный PathBuf для того, чтоб отслеживать текущий рабочий каталог. Этот самый PathBuf должен изменяться каждым вызовом cd. Будет полезно создать функции с общей сигнатурой для каждой из ваших команд. Для дополнительного уровня типобезопасности можете выделить трейт, как абстракцию для команд и реализовывать этот трейт для каждой команды.


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


Убедитесь, что используете bin-аллокатор!

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



Подсказка: Используйте существующие методы PathBuf и Path для своих грязных целей.



Подсказка: Вам потребуется специально обратить внимание на .. и . при реализации cd.

Такие дела.

Комментарии (2)


  1. novoxudonoser
    09.04.2018 13:41
    +1

    MORE!!! Ты офигенен.


  1. sfxws2006
    10.04.2018 23:59

    Ваш цикл — прекрасен!

    Как вы думаете, насколько реально написать подобное для: ARM Cortex M4?

    Небезызвестный Japaric не мало усилий прилагает, чтобы подобное было возможно, однако интересно — какой предел необходимых ресурсов, чтобы «завелось».