Абстрактный слой ФС
В статье про 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) на разделе завис бы. В настоящем коде поэтому на местах ошибок стоят лишь скупые комментарии вместо диагностического вывода.
Один договор, пять исполнителей
Интерфейс 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.
Было бы интересно увидеть ваши комментарии и улучшить статьи.
◀ Предыдущая статья Содержание Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением