Device manifest, или как моя система помнит своё железо
В прошлой части UDM поженил каждое найденное устройство с драйвером. Это работает, но у этого есть слепое пятно - вся эта работа делается заново при каждой загрузке и как только пропадает питание, всё забывается. При следующем старте UDM снова сканирует, снова сопоставляет и снова ничего не помнит. Для маленькой системы () это нормально. Но как только устройства приходят и уходят, как только хочется знать, было ли устройство тут когда-то или сейчас отсутствует, нужна память, переживающая перезапуск.
Именно это и есть Device Manifest - описание всех когда-либо виденных устройств, сохранённое как файл TOML. Он лежит в файловой системе, загружается при старте и сверяется со сканом. То, что UDM размещает и обрабатывает в памяти, манифест пишет на диск.
Файл, который помнит устройства
Сначала посмотрим, как этот файл выглядит. Каждое устройство, это одна запись, и каждая запись хранит ровно те факты, которые понадобятся мне в следующий раз.
В основе запись выглядит так:
[[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. Вместо того чтобы зашивать всё в одном центральном месте, каждый драйвер приносит свой вклад сам. Новый драйвер теперь значит - добавить таблицу правил и зарегистрировать её. Центральную логику сопоставления для этого трогать уже не надо.
Через перезагрузки - первый старт против следующих
С файлом хранения и матчером вместе вырисовывается ясная схема, в зависимости от того, знает система устройство или нет.
При самом первом старте файла 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. Как события ядра становятся сообщениями для программ и как приложения над ядром вообще живут. Начнём с системы событий которая связывает обе стороны.
Было бы интересно увидеть ваши комментарии и улучшить статьи.
◀ Предыдущая статья Содержание Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением