Добавить в корзинуПозвонить
Найти в Дзене
Триалогия

Пишем операционную систему Триалогия - Абстрактный слой ФС

В статье про VFS виртуальная файловая система разрешила путь в пару, номер раздела и абсолютный путь, и затем вызывала fs_read_file или fs_fopen. Эти функции живут здесь, в абстрактном слое ФС. Этот слой задаёт другой вопрос, чем VFS и именно это его определяет. VFS спрашивает "какой файл?". Абстрактный слой спрашивает "какая файловая система лежит на этом разделе и как с ней говорить?". Это переводчик между верхним миром, который знает лишь пути и номера разделов, и пятью конкретными драйверами ФС, каждый из которых говорит на своём языке. Сверху всё выглядит одинаково; вниз он вызывает и передает данные нужному драйверу. По сути fs_read_file, это один switch по типу ФС раздела. Больше магии тут нет, и это хорошо: uint8_t* fs_read_file(uint8_t partition, const char* path, uint32_t base_location, uint32_t* out_size) { // 0. mount? -> войти заново с переведённым путём (рекурсивно) // ... fs_translate_mount(...) ... // 1. выбрать раздел ДО блокировки (без блокировки) if (!select_part
Оглавление
Триалогия - Пишем операционную систему
Триалогия - Пишем операционную систему

Абстрактный слой ФС

В статье про VFS виртуальная файловая система разрешила путь в пару, номер раздела и абсолютный путь, и затем вызывала fs_read_file или fs_fopen. Эти функции живут здесь, в абстрактном слое ФС. Этот слой задаёт другой вопрос, чем VFS и именно это его определяет.

Какой вопрос задаёт этот слой

VFS спрашивает "какой файл?". Абстрактный слой спрашивает "какая файловая система лежит на этом разделе и как с ней говорить?". Это переводчик между верхним миром, который знает лишь пути и номера разделов, и пятью конкретными драйверами ФС, каждый из которых говорит на своём языке. Сверху всё выглядит одинаково; вниз он вызывает и передает данные нужному драйверу.

Абстрактный слой ФС - Диспетчер

Абстрактный слой ФС - Диспетчер
Абстрактный слой ФС - Диспетчер

По сути fs_read_file, это один switch по типу ФС раздела. Больше магии тут нет, и это хорошо:

uint8_t* fs_read_file(uint8_t partition, const char* path,

uint32_t base_location, uint32_t* out_size) {

// 0. mount? -> войти заново с переведённым путём (рекурсивно)

// ... fs_translate_mount(...) ...

// 1. выбрать раздел ДО блокировки (без блокировки)

if (!select_partition(partition)) return NULL;

// 2. запереть раздел (одна блокировка НА раздел)

mutex_get(&partition_spinlock[partition], true);

uint8_t* result;

// 3. раздать конкретной файловой системе

switch (partition_get_filesystem(partition)) {

case PARTITION_FILESYSTEM_TRIAFS: result = triafs_read_file(path, base_location, out_size); break;

case PARTITION_FILESYSTEM_FAT: result = fat_read_file(partition, path, base_location, out_size); break;

case PARTITION_FILESYSTEM_EXT: result = ext_read_file(path, base_location, out_size); break;

case PARTITION_FILESYSTEM_ISO9660: result = iso9660_read_file(path, base_location, out_size); break;

case PARTITION_FILESYSTEM_CDDA: result = cdda_read_file(path, base_location, out_size); break;

default: *out_size = 0; result = NULL; break;

}

mutex_release(&partition_spinlock[partition]);

return result;

}

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

Таблица разделов (partitions)

Откуда partition_get_filesystem знает ответ? Из небольшой таблицы, заполняемой при загрузке, connected_partitions[] (до десяти записей в настоящий момент). Каждая запись описывает один обнаруженный раздел:

struct connected_partition_info_t {

uint8_t medium_type; // какой носитель (IDE / AHCI / ...)

uint8_t medium_number; // какой его привод

uint8_t filesystem; // TRIAFS / FAT / EXT / ISO9660 / CDDA

uint32_t first_sector; // где раздел начинается на носителе

uint8_t partition_label[11];

uint8_t* filesystem_specific_info; // напр. кэш суперблока

uint32_t num_of_sectors; // размер раздела

} __attribute__((packed));

medium_type и medium_number говорят, где раздел физически лежит, filesystem говорит, что на нём, а first_sector плюс num_of_sectors говорят, какой диапазон носителя ему принадлежит. Откуда берутся эти записи, тема следующей статьи о разделах.

Сначала раздел выбрать , потом заблокировать

Сначала раздел выбрать , потом заблокировать
Сначала раздел выбрать , потом заблокировать

В коде выше бросился в глаза порядок: сперва select_partition, потом блокировка. Это не дело вкуса, а оплачено дорогой ценой - потерей времени. select_partition идёт без блокировки и ставит лишь собственный контекст хранилища потока (StorageContext).

uint8_t select_partition(uint8_t partition_number) {

if (connected_partitions[partition_number].medium_type == NO_MEDIUM)

return false;

// без блокировки: у каждого потока свой StorageContext

Thread* thread = get_current_thread();

StorageContext* ctx = &thread->storage_context;

thread_select_storage_medium(ctx,

connected_partitions[partition_number].medium_type,

connected_partitions[partition_number].medium_number);

return true;

}

Почему без блокировки? Раньше была глобальная переменная "текущий носитель". При двух потоках на разных разделах они перезаписывали друг друга, и один вдруг читал с чужого диска, тихая порча данных, которая выглядела точь в точь как сломанная файловая система. Сегодня каждый поток (thread) несёт свой StorageContext, поэтому выбору больше не нужна блокировка.

Сама блокировка, это partition_spinlock[partition], по одной на раздел, а не одна общая. Так I/O на разных разделах идёт одновременно и блокировки не сталкиваются. Два правила здесь обязательны: select_partition обязан стоять до блокировки, иначе нарушает иерархию блокировок и под блокировкой никогда не должно быть printf или LOG, блокировка осталась бы захваченной навсегда (пропустил бы mutex_release) и каждый другой поток (thread) на разделе завис бы. В настоящем коде поэтому на местах ошибок стоят лишь скупые комментарии вместо диагностического вывода.

Один договор, пять исполнителей

Один interface, пять исполнителей - Триалогия
Один interface, пять исполнителей - Триалогия

Интерфейс fs_, это договор (interface) - те же функции (fs_read_file, fs_write_file, fs_create_folder, потоковые fs_fopen/fs_fread/…) и те же коды ошибок (FS_SUCCESS, FS_ERROR_NOT_FOUND, FS_ERROR_READ_ONLY, FS_ERROR_DISK_FULL и так далее). Пять драйверов исполняют его: TriaFS, FAT и EXT умеют читать и писать, ISO9660 и CDDA только читать.

Чтобы попытка записи на раздел только для чтения не падала глубоко внутри драйвера, is_filesystem_write() ловит её прямо на входе и сразу возвращает FS_ERROR_READ_ONLY. Так вызывающий всегда получает один и тот же ясный ответ, какая бы файловая система ни лежала ниже.

Потоки (stream), этажом ниже

У потокового API (stream API) из статьи про VFS тоже есть здесь двойник. Хэндл потока VFS оборачивал FS-хэндл, а этот FS-хэндл, это снова лишь обёртка вокруг совсем конкретного хэндла соответствующей файловой системы (выговорил... я повернулся посмотреть не повернулась ли она чтоб посмотреть .... и т.д.)

struct fs_file_handle {

uint8_t filesystem_type; // какой тип ФС (TRIAFS, FAT, ...)

void* fs_specific_handle; // TriaFS_OpennedFile* или fat_stream_handle_t*

uint8_t partition;

uint32_t flags;

};

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

Правда жизни

Одну фишку я выше в коде лишь слегка показал - трансляцию монтирования. В самом начале каждой операции fs_translate_mount проверяет - не ведёт ли путь через mount на совсем другой раздел. Если да, функция вызывает себя заново с переведённым путём. Это изящно, но это надо держать в голове, ведь операция так может оказаться на другом разделе, чем ожидалось. А правило "никакого printf под блокировкой" это не теоретический риск, а прямо из исправлений deadlock в настоящем коде.

Что дальше

Абстрактный слой пользовался таблицей connected_partitions[] будто она просто есть. Но это не так, кто-то должен при загрузке просканировать диски, прочитать таблицы разделов и выяснить, какая файловая система лежит в каком диапазоне секторов. Это следующая часть - Разделы (MBR и GPT). Дальше идут слой Storage и драйверы IDE и AHCI.

Было бы интересно увидеть ваши комментарии и улучшить статьи.

Предыдущая статья Содержание Следующая статья

*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением