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

Пишем операционную систему Триалогия - Device Manifest - как помнить железо в файле TOML

В прошлой части UDM поженил каждое найденное устройство с драйвером. Это работает, но у этого есть слепое пятно - вся эта работа делается заново при каждой загрузке и как только пропадает питание, всё забывается. При следующем старте UDM снова сканирует, снова сопоставляет и снова ничего не помнит. Для маленькой системы () это нормально. Но как только устройства приходят и уходят, как только хочется знать, было ли устройство тут когда-то или сейчас отсутствует, нужна память, переживающая перезапуск. Именно это и есть Device Manifest - описание всех когда-либо виденных устройств, сохранённое как файл TOML. Он лежит в файловой системе, загружается при старте и сверяется со сканом. То, что UDM размещает и обрабатывает в памяти, манифест пишет на диск. Сначала посмотрим, как этот файл выглядит. Каждое устройство, это одна запись, и каждая запись хранит ровно те факты, которые понадобятся мне в следующий раз. В основе запись выглядит так: [[device]] id = "0000:02:00.0" bus = 2
Оглавление
Триалогия - Пишем операционную систему
Триалогия - Пишем операционную систему

Device manifest, или как моя система помнит своё железо

В прошлой части UDM поженил каждое найденное устройство с драйвером. Это работает, но у этого есть слепое пятно - вся эта работа делается заново при каждой загрузке и как только пропадает питание, всё забывается. При следующем старте UDM снова сканирует, снова сопоставляет и снова ничего не помнит. Для маленькой системы () это нормально. Но как только устройства приходят и уходят, как только хочется знать, было ли устройство тут когда-то или сейчас отсутствует, нужна память, переживающая перезапуск.

Именно это и есть Device Manifest - описание всех когда-либо виденных устройств, сохранённое как файл TOML. Он лежит в файловой системе, загружается при старте и сверяется со сканом. То, что UDM размещает и обрабатывает в памяти, манифест пишет на диск.

Файл, который помнит устройства

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

Формат TOML реестра устройств - Триалогия
Формат TOML реестра устройств - Триалогия

В основе запись выглядит так:

[[device]]

id = "0000:02:00.0"

bus = 2

vendor = "8086"

device = "10fb"

class = "02"

driver = "e1000e"

present = true

last_seen = "2025-11-14T13:42:00Z"

Первые поля, это информация уже знакомая нам из части про PCI: адрес на шине, производитель, модель, класс. Интересное поле - driver, здесь записано решение которое принял UDM при поиске драйвера в базе данных известных ему драйверов. А затем есть два поля, которые и превращают манифест в память: present говорит, воткнуто (подключено) ли устройство прямо сейчас, а last_seen говорит, когда я видел его в последний раз.

Отличие в разделении "уже видели" и "есть сейчас" думаю обьяснять подробно не надо. Устройство, которое я вынимаю, не исчезает из файла. Оно остаётся, только с present = false. Так система по-прежнему знает о нём, даже когда оно сейчас не подключено.

Кто получает какой драйвер - из if получается таблица

У UDM сопоставление было эвристикой в коде, цепочкой условий, что возвращала нужный драйвер для известных ID производителей. Это честно и просто, но есть минус - каждое новое правило значит трогать центральную логику. В манифесте (Device Manifest) я решил это иначе. Вместо цепочки if есть таблица из данных и каждый драйвер приносит свою часть этой таблицы сам.

Система матчера с таблицами-провайдерами и шаблонами
Система матчера с таблицами-провайдерами и шаблонами

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

typedef struct {

uint16_t vendor_id; // или PCI_ANY_16

uint16_t device_id; // или PCI_ANY_16

uint8_t class_code; // или PCI_ANY_8

const char* driver;

} pci_match_t;

Правило { 0x8086, 0x10fb, PCI_ANY_8, "e1000e" } попадает ровно в одну модель. Правило { 0x8086, PCI_ANY_16, 0x02, "..." } попадает в любую сетевую карту Intel. А { PCI_ANY_16, PCI_ANY_16, 0x01, "ahci" } попадает в любой контроллер хранения этого класса, от кого бы он ни был. Чем больше шаблонов, тем шире правило.

Каждый драйвер регистрирует свою маленькую таблицу как провайдер.

static const pci_match_t ahci_matches[] = {

{ PCI_ANY_16, PCI_ANY_16, 0x01, "ahci" },

};

pci_register_match_provider(ahci_matches, 1);

Сам поиск, это тогда один вызов, который проходит все зарегистрированные провайдеры и возвращает лучшее совпадение.

const char* driver = pci_match_driver(&dev);

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

Через перезагрузки - первый старт против следующих

С файлом хранения и матчером вместе вырисовывается ясная схема, в зависимости от того, знает система устройство или нет.

Ход через перезагрузки с флагом present
Ход через перезагрузки с флагом present

При самом первом старте файла devices.toml ещё нет. Поэтому система делает полную работу: сканирует PCI, определяет драйвер для каждого устройства, пишет всё в файл, каждое с present = true и текущим временем. После этого файл есть и система знает железо.

При каждом следующем старте, файл это быстрый путь (fast path). Он загружается, потом всё равно идёт скан, но уже только для сверки - что ещё за железо осталось, что новое, чего нет. Для известных устройств никому не надо искать сопоставление, привязка уже в файле. Появившиеся новые устройства проходят через матчер и добавляются. Отсутствующие получают present = false, но остаются в файле.

Этот флаг present, это сердце памяти. Железо, что было тут однажды, не забывается, а помечается как отсутствующее. Воткни (или подключи) его обратно и запись вместе с драйвером сразу снова на месте. Тот же принцип несёт и hotplug - устройство, что появляется во время работы, сканируется, сопоставляется и добавляется, а то что исчезает, ставится в отсутствующие. Файл devices.toml всегда остаётся текущим состоянием системы.

Правда жизни

В этом подсистеме я нашёл баг который стоил мне нервов и он поучителен тем, насколько был неприметен. Мой writer для TOML мог записать пустое значение, например строку driver = вовсе без кавычек, когда у устройства не было драйвера. А мой парсер TOML в свою очередь сдавался ровно на таком пустом значении и объявлял весь остаточный от этого места файл неверным. Итог: файл devices.toml не загружался никогда полностью, и быстрый путь на деле не работал никогда как должен. Система каждый раз сканировала заново полностью весь PCI.

Коварство было в том, что ничто не выглядело сломанным. Устройство ведь всё равно получало свой драйвер, потому что скан ставил present и сопостовлял драйвер заново каждый раз. Флаг present маскировал баг. Только когда я спросил себя, почему сохранённое значение после перезагрузки снова пропадало, я добрался до загрузки и парсера. Парсер теперь принимает пустые значения и возвращает пустую строку вместо ошибки, а writer аккуратно записывает пустые строки в кавычки.

Что дальше

На этом путешествие по распознаванию устройств заканчивается: от скана PCI через таблицы ACPI, назначение драйверов в UDM, и до этой постоянной памяти в DeviceManifest. Система теперь знает, какое у неё железо и как им управлять. В следующем большом разделе я покидаю машинное отделение ядра и поднимаюсь наверх в мир userspace. Как события ядра становятся сообщениями для программ и как приложения над ядром вообще живут. Начнём с системы событий которая связывает обе стороны.

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

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

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