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

Пишем операционную систему Триалогия - Файловые системы от read() до диска (обзор)

До сих пор речь шла о процессоре, памяти, синхронизации, то есть о том, что происходит внутри. Теперь вопрос, который касается каждого пользователя - как файл попадает с диска в программу? Ответ, это целая иерархия из слоёв, и эта часть, это обзор иерархия. Каждый отдельный слой потом получит свою глубокую подробную статью. Прочитать файл - звучит как одно действие, но это четыре очень разные задачи разом. Кто-то должен разобрать путь /docs/x.txt. Кто-то должен понимать конкретный формат, ведь FAT устроен иначе, чем EXT. Кто-то должен найти нужный раздел (partition). И кто-то должен забрать сырые секторы (row data) с нужного железа, со старого диска IDE иначе, чем с современного SATA SSD. Каждая из этих задач, это отдельный слой. Это позволяет держать каждый из них простым и, что мне важнее, заменяемым. Новая файловая система или новый дисковый драйвер встраивается, а другие слои этого не замечают. В самом верху находится VDS, управление FD (descriptoren). open() отдаёт тебе файловый
Оглавление
Триалогия - Пишем операционную систему
Триалогия - Пишем операционную систему

Файловые системы от read() до диска (обзор)

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

Зачем столько слоёв?

Прочитать файл - звучит как одно действие, но это четыре очень разные задачи разом. Кто-то должен разобрать путь /docs/x.txt. Кто-то должен понимать конкретный формат, ведь FAT устроен иначе, чем EXT. Кто-то должен найти нужный раздел (partition). И кто-то должен забрать сырые секторы (row data) с нужного железа, со старого диска IDE иначе, чем с современного SATA SSD. Каждая из этих задач, это отдельный слой. Это позволяет держать каждый из них простым и, что мне важнее, заменяемым. Новая файловая система или новый дисковый драйвер встраивается, а другие слои этого не замечают.

Стек файловой системы - Триалогия
Стек файловой системы - Триалогия

Слои файловой системы сверху вниз

В самом верху находится VDS, управление FD (descriptoren). open() отдаёт тебе файловый дескриптор, простое число, и VDS помнит для каждого процесса, на что указывает каждое из этих чисел. Для файла оно указывает на поток VFS. VDS может указывать и на совсем другое устройство - на консоль, на сетевую карту, но здесь я не буду останавливаться подробно и VDS получит свою полную статью. Важно знать - отдельного слоя блочного устройства у меня пока нет, файловые системы вызывают слой Storage напрямую.

Ниже идёт VFS, виртуальная файловая система. Она понимает пути.

Из /dev/hd0p0:/docs/x.txt она делает :

  • /dev - устройство
  • hd0p0: - физического устройства 0 раздел 0
  • / - абсолютный путь от корневого елемента
  • docs - искать директорий
  • x.txt - искать или создать (в зависимости от OP)
  • знает текущий рабочий каталог каждого процесса
  • и затем передаёт вниз.

Абстрактный слой ФС, это посредник. Он знает, какая конкретная файловая система на каком разделе, и вызывает подходящую реализацию. Здесь "прочитать файл X" становится конкретным вызовом tria_read_file , fat_read_file или ext_read_file.

Конкретная файловая система наконец понимает формат на диске. TriaFS, FAT и EXT2/3 реализованы полностью, чтение и запись. ISO9660 (диск с данными) и CDDA (аудио-CD), это намеренно только читающие каркасы, на CD не пишут, да и реализованы еще не до конца.

Ниже, раздел (MBR или GPT), затем

слой Storage, который мыслит только секторами,

и в самом низу драйверы IDE и AHCI, которые реально говорят с железом.

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

на удивление прост - ветвление по типу файловой системы раздела.

uint8_t* fs_read_file(uint8_t partition, const char* path, uint32_t base, uint32_t* out_size) {

uint8_t fs = partition_get_filesystem(partition);

if (fs == PARTITION_FILESYSTEM_FAT) return fat_read_file(partition, path, base, out_size);

if (fs == PARTITION_FILESYSTEM_TRIAFS) return triafs_read_file(path, base, out_size);

if (fs == PARTITION_FILESYSTEM_EXT) return ext_read_file(path, base, out_size);

/* ... ISO9660, CDDA ... */

}

Каждая конкретная файловая система предлагает одни и те же операции, чтение, запись, перечисление каталога, отличается лишь реализация за ними. Поэтому VFS сверху всегда видит один и тот же интерфейс, совершенно независимо от того, TRIAFS, FAT под ним или EXT.

Как раздел попадает в систему

Разделы физического диска - Триалогия
Разделы физического диска - Триалогия

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

if (header->magic == TRIAFS_MAGIC) { /* TriaFS */ }

if (bpb->signature == 0x55AA && bpb->bps) { /* FAT */ }

if (sb->ext_signature == EXT_SIGNATURE /*0xEF53, по смещению 1024*/) { /* EXT */ }

Каждый распознанный раздел попадает в реестр с носителем, стартовым сектором и типом файловой системы:

struct connected_partition_info_t {

uint8_t medium_type; // hd0, hd1, оптический привод ...

uint8_t filesystem; // FAT · LISTFS · EXT · ISO9660 · CDDA

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

uint32_t num_of_sectors;

};

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

Путь на примере одного read()

Путь одного read() для файла
Путь одного read() для файла

Конкретно, сверху до диска, на примере раздела FAT:

Приходит read(fd), VDS смотрит, на что указывает FD (поток VFS).

2. VFS через vfs_fread передаёт абстрактному слою.

3. Абстрактный слой диспетчеризует по типу ФС раздела к fat_read_file.

4. FAT превращает путь в цепочку кластеров, а кластеры в номера секторов.

5. Слой Storage получает секторы и выбирает драйвер:

StorageError thread_read_storage(StorageContext* ctx, uint32_t sector,

uint8_t num_of_sectors, void* memory);

6. IDE или AHCI читает секторы. И тут видна разница поколений: IDE перекидывает каждое слово через порты ввода-вывода (PIO), AHCI же даёт диску самому писать в память через DMA.

// IDE (старый, PIO): процессор читает каждое слово вручную

for (uint32_t j = 0; j < 256; j++) *mem++ = inw(base_port + 0); // 512 байт = 256 слов

// AHCI (современный, DMA) - кладём команду в command-list, железо делает передачу само

sata_read(port_base, command_list, sector, num_sectors, memory);

Пусть будет правда жизни

Эта иерархическая система росла долго, и это по ней видно. На границах слоёв находилось большинство моих проблем с блокировками, у каждого слоя своя (контекст VFS, блокировка раздела, блокировка Storage), и их порядок должен быть верным, иначе получаются ровно те deadlock'и из части про синхронизацию. Баг, стоивший мне часов, был простым printf под удерживаемой блокировкой Storage, который блокировал очередь консоли и замораживал всю систему на безобидном ls. А пути записи не устойчивы к сбоям - журнала нет, если питание пропадёт посреди записи, файловая система может остаться повреждённой. Буду доделывать :-)

Что дальше

Это был обзор. В следующих частях я опишу каждый слой по отдельности, каждый с настоящим кодом, проблемами, которые у меня были, и трюками, которые меня спасли. Сначала VDS (полностью, со всей механикой FD), затем VFS, абстрактный слой, разделы, слой Storage и, наконец, IDE и AHCI.

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

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