Ring 1: микроядро для самых маленьких
Зачем мы завели Ring 1 для драйверов в нашем unikernel — и почему это получилось дешевле, чем казалось в 90-х. Прямая доставка прерываний, IOMMU per-driver, lock-free IPC, watchdog-супервизор. С конкретными файлами и тремя шишками, на которые натирали по два дня.
В этой статье: что такое кольца защиты x86, и почему индустрия за сорок лет так и не научилась пользоваться третьим. Зачем мы в нашем unikernel завели Ring 1 для драйверов — и почему именно для драйверов, а не для приложений. Что такое прямая доставка прерываний с DPL=1, IOMMU per-driver, lock-free IPC очереди и watchdog-супервизор. Как это устроено у MINIX 3, у seL4 и у Linux (никак). Как это устроено у нас — с конкретными файлами, номерами строк и историей граблей. В конце — три шишки, которые мы натирали по два дня каждую.
Кому читать: инженеру, который слышал слово «микроядро» и думал, что это про MINIX в учебнике Танненбаума. Архитектору, который хочет понять, почему падение драйвера сетевой карты в unikernel не уносит запущенное на нём приложение. CISO, который читает отчёт о сертификации и хочет понять, что значит фраза «изоляция драйверов на уровне аппаратной защиты CPU». Старому сисадмину, который видел MINIX в учебнике, microdrivers в академических статьях и Linux в проде, и хочет наконец увидеть Ring 1 в живом коде.
1. Холодная сцена
Четверг, 16:40. Переговорка «Сосна» в офисе Kernix. На доске — архитектура нашего ядра, нарисованная маркером, который заканчивается: наверху Ring 0 с ядром, ниже список драйверов, рядом — приложение поверх runtime. Под схемой крупными буквами: «Что будет, если у нас упадёт virtio-net?»
За столом — двое. С одной стороны — наш ядерный разработчик. С другой — инженер-сосед, который пилит на нашем unikernel приложение и смотрит на это всё со стороны рантайма. Перед ним ноутбук с открытым серийным логом со стенда, где позавчера ушёл в triple fault весь хост — virtio-net поймал page fault на битом DMA-буфере, и выгребло вообще всё. Он листает трейс и говорит:
— Слушай, я простую вещь хочу понять. У нас же unikernel. Один процесс, одно адресное пространство, всё в Ring 0. Я правильно понимаю, что если драйвер сетевой карты ловит page fault — падает вся виртуалка целиком?
Ядерный разработчик молчит секунду. Потом отвечает:
— Полгода назад ты был бы прав. С декабря — нет. Драйверы вынесены в Ring 1, у них отдельный exception handler, отдельный IST-стек, отдельный домен IOMMU. Падает драйвер — супервизор его рестартует, приложение работает.
Сосед моргает.
— Постой. Ring 1. Это вот то самое Ring 1 из учебников Intel, которое никто никогда не использовал? Серьёзно?
— Серьёзно. Хочешь, покажу?
2. Исторический контекст: четыре кольца, которые никому не пригодились
Когда в 1985 году Intel выпустила процессор 80386, инженеры заложили в архитектуру защиты четыре кольца привилегий: Ring 0, Ring 1, Ring 2, Ring 3. Идея была элегантной. Ring 0 — самый привилегированный, там ядро. Ring 3 — самый ограниченный, там приложения. А промежуточные Ring 1 и Ring 2 — для кода ядра, которому не нужны абсолютные права: драйверов, файловых систем, сетевого стека. Микроядро как аппаратная фича.
Прошло сорок лет. Откройте сейчас исходники любого современного ядра — Linux, FreeBSD, Windows NT. Сколько колец они используют? Два. Ring 0 для ядра. Ring 3 для пользовательских процессов. Кольца 1 и 2 — пустуют. Сорок лет аппаратной фичи, которой никто не пользуется.
Почему так случилось? Несколько причин.
Первая — мода на монолиты в 90-х. Линус Торвальдс публично поссорился с Танненбаумом ещё в 1992 году в comp.os.minix. Линус утверждал, что микроядра — академическая чушь, а монолитное ядро проще, быстрее и реалистичнее. Танненбаум возражал. Linux выиграл рынок, MINIX остался в учебниках, а с ним — и идея использовать Ring 1 для драйверов.
Вторая — переносимость. В 1996 году Microsoft выпустила Windows NT 4.0 на четырёх архитектурах: x86, MIPS, Alpha, PowerPC. У RISC-процессоров никаких четырёх колец не было — обычно только два режима, supervisor и user. Поэтому Microsoft решила: используем только Ring 0 и Ring 3, чтобы код ядра был одинаковым. Linux пошёл тем же путём.
Третья — виртуализация. Когда виртуализация на x86 стала массовой (VMware Workstation — 1999, Xen — 2003, KVM в мейнлайне Linux — 2007), Ring 1 неожиданно понадобился — гипервизор стал жить в Ring 0, а гостевое ядро уехало в Ring 1. Это называлось «ring deprivileging» в paravirt-режиме Xen. Но схема прожила лет пять и умерла, как только Intel и AMD добавили VT-x (2005) и AMD-V (2006) — отдельный режим VMX root, который сделал Ring 1 для гипервизоров ненужным. Кольцо снова осталось пустым.
И так получилось, что в 2026 году единственное место, где Ring 1 используется по назначению — это специализированные ОС вроде нашего unikernel. Не Linux. Не Windows. Не FreeBSD. Никто.
Параллельно в академии всё это время жила альтернативная линия — микроядерная. MINIX 3 (Танненбаум, начиная с 2005-го) вынес драйверы в отдельные процессы в Ring 3, общающиеся через IPC. seL4 (NICTA, формально верифицированное ядро, 2009) сделал то же самое, но математически доказал отсутствие багов в самом ядре. Singularity от Microsoft Research (2003) пошла ещё дальше — изоляция через типобезопасный язык, без аппаратных колец вообще. Все эти системы доказывали одну простую идею: драйвер устройства не должен ронять ядро.
Мы пришли к той же идее со стороны unikernel. И решили, что если уж мы платим за изоляцию, то платим её аппаратно — через ring-защиту, IOMMU и прямую доставку прерываний.
Главное историческое наблюдение, которое мы вынесли: аппаратная изоляция драйверов оказалась дешевле, чем казалось в 90-х, и сильно дешевле, чем IPC-через-сообщения в стиле MINIX 3. Об этом — дальше.
3. Техническое ядро
3.1. Что такое драйвер в монолитном unikernel
Наше ядро по своей природе — unikernel. Один процесс, одно адресное пространство, всё в Ring 0. Когда мы запускаем поверх него типичное приложение, в этом адресном пространстве живут одновременно: прикладной бинарь, рантайм языка, virtio-net (драйвер сетевой карты), virtio-blk (драйвер диска), virtio-scsi, virtio-rng.
Если у virtio-net случается page fault на битом буфере DMA — традиционный ответ был abort(), halt, конец. Виртуалка падает, приложение не работает. Именно так и упал тот стенд из холодной сцены.
Решение, которое мы приняли: драйверы — это отдельная категория кода, и их падение не должно ронять приложение. У них:
- свой класс изоляции (Ring 1, DPL=1 в дескрипторах),
- свой стек обработчиков исключений (IST[3], 8 КБ на CPU),
- свой домен IOMMU (если железо поддерживает Intel VT-d или AMD-Vi),
- свой обработчик отказов, известный супервизору,
- свой watchdog, который перезапускает упавший компонент.
При этом прикладной код остаётся в Ring 0. Это сознательное решение: горячий путь запроса не пересекает кольцо. Платим за изоляцию мы только там, где она реально нужна — на границе с железом.
3.2. Кольца x86 как аппаратная защита
Каждый сегмент кода и каждый дескриптор в IDT (таблице прерываний) имеет два поля привилегий: CPL (Current Privilege Level — у текущего кода) и DPL (Descriptor Privilege Level — у дескриптора). Правило простое: перейти можно только в кольцо равного или меньшего номера. Из Ring 1 нельзя руками войти в Ring 0 — нужно прерывание или вызов через call gate.
В нашем случае это означает: код драйвера, помеченный CS с CPL=1,
не может выполнить инструкции, требующие CPL=0 — lgdt, lidt,
mov to cr3, wbinvd. Если попробует — получит #GP (general
protection fault), и мы его обработаем сами, без падения ядра.
3.3. Прямая доставка прерываний (DPL=1)
Стандартный путь прерывания в монолитной системе: железо генерирует IRQ → CPU прыгает в IDT-обработчик в Ring 0 → обработчик решает, кому это адресовано → диспетчеризует драйверу. Между «железо подняло прерывание» и «драйвер начал его обрабатывать» проходит цикл через Ring 0. Это стоит миллисекундных долей, но главное — это дополнительная точка отказа в Ring 0.
Мы сделали иначе. В IDT для векторов 34-36 (наши Ring 1 драйверные
вектора) лежит interrupt gate, у которого селектор кодового сегмента
указывает на сегмент с DPL=1. Для аппаратного IRQ DPL самих ворот
CPU игнорирует — направление перехода задаёт селектор в воротах,
и CPU прыгает прямо в Ring 1, минуя Ring 0. (Поле DPL у ворот
проверяется только для софт-прерывания INT n, чтобы код Ring 1 не
мог самопроизвольно вызвать чужой обработчик.) Полный цикл
прерывания получается ~500 нс на современном Skylake — то же, что и
у монолитного unikernel. Никакого штрафа.
Соответствующий код — в arch/x64/ring1_interrupt_stub.S (181 строка
ассемблера) и arch/x64/ring1_dispatcher.cc.
3.4. IPC: как Ring 1 говорит с Ring 0
Когда драйверу всё-таки нужно что-то от ядра — например, разбудить поток-консьюмер пакетов, — он не может просто вызвать функцию. Между Ring 1 и Ring 0 — аппаратный барьер. Нужен IPC.
В MINIX 3 IPC построен на синхронных сообщениях через sendrec.
Драйвер блокируется, ждёт ответа, Ring 0 отвечает, драйвер просыпается.
Это просто, но дорого: 2-3 микросекунды на сообщение.
Мы пошли по другому пути — lock-free per-CPU очереди. Один SPSC
(single-producer, single-consumer) кольцевой буфер на каждое CPU.
Драйвер на CPU N пишет туда сообщение через relaxed memory order,
поднимает doorbell-флаг, и продолжает работу не блокируясь. Поток
ядра на том же CPU читает очередь в основном цикле. Стоимость одного
сообщения — около 100 нс. Без атомиков-арбитража, без mutex’ов.
Файл: core/ring1_ipc.cc (370 строк), интерфейс — в
include/ring1_ipc.hh.
3.5. IOMMU: per-driver защита DMA
Самый коварный класс ошибок в драйвере — это битый DMA-адрес. Драйвер сетевой карты говорит NIC’у: «положи пакет по адресу X». Если X указывает на приватные структуры приложения или на буферы другого драйвера — NIC честно туда положит. Никакая защита Ring/CPL тут не поможет: DMA идёт мимо CPU, мимо MMU, прямо в физическую память.
Защита от этого — IOMMU. Intel называет её VT-d, AMD — IOMMU. Это отдельный MMU для DMA-операций: для каждого устройства ведётся своя таблица переводов «physical → host physical». Можно настроить так, что virtio-net видит только свои буферы, а память приложения — физически недоступна для его DMA.
Мы написали iommu_controller (файл arch/x64/iommu.cc,
567 строк, заголовок — include/iommu_controller.hh), который
поднимает per-driver домены: каждому драйверу — свою страницу таблиц.
Если железо IOMMU не поддерживает (старый Xeon, дешёвый KVM-хост) —
работаем в identity-mapping режиме, изоляция Ring 1 + supervisor
всё ещё работает, но DMA-protection пропадает. Об этом мы честно
пишем в логах загрузки.
3.6. Supervisor: watchdog, который восстанавливает
Каждый Ring 1 драйвер регистрируется в ring1_supervisor (файл
core/ring1_supervisor.cc, 1342 строки — это самый большой
компонент в Phase 2). Супервизор хранит:
- состояние компонента (uninitialized → initializing → running → stopping → stopped/failed),
- счётчик рестартов,
- список потоков и областей памяти, принадлежащих компоненту,
- per-driver exception handler (что делать на page fault, что — на GPF),
- backoff-таймер для рестарта.
Когда драйвер падает (ловит page fault или GPF), управление через
IST[3] стек попадает в ring1_dispatch_*_handler. Тот вызывает
зарегистрированный handler драйвера. Если драйвер сам не справился —
помечает себя mark_failed(), и супервизор видит это в основном
цикле. Дальше — рестарт с экспоненциальным бэкоффом: 500 мс,
1 с, 1.5 с, 2 с, 2.5 с, после пятой попытки — окончательный отказ
и алерт в логи.
Прокси — то есть всё, что не драйвер — продолжает работать. Если в момент падения virtio-net в очереди отправки висели пакеты — они доедут после рестарта, потому что HTTP-сервер живёт в Ring 0 и ждёт на сокете, а не на драйвере напрямую.
3.7. Когда Ring 1 не нужен
Это нужно сказать честно, чтобы не выглядело как маркетинг.
- Если у вас один-единственный драйвер и он стабилен (например, только virtio-net в продакшене) — выгода от Ring 1 нулевая.
- Если у вас нет IOMMU (старое железо, дешёвый VPS) — DMA-защиты не будет, останется только software-isolation. Это всё равно лучше, чем ничего, но не дотягивает до полноценной модели.
- Если вы запускаете нас на ARM64 — у ARM нет хардверного Ring 1 в смысле x86 (есть EL0/EL1/EL2/EL3, и они устроены иначе). В нашей ARM-сборке (NanoPi R3S) Ring 1 — только supervisor watchdog с software-restart. Прямой доставки прерываний нет.
4. Как у других
4.1. Linux — никак
Linux Kernel Driver Framework живёт целиком в Ring 0. Когда у вас падает драйвер графики — у вас падает ядро (в лучшем случае oops, в худшем — kernel panic). Есть kdump, есть live patching через kpatch, но архитектурно изоляции драйверов в Linux нет. Попытки были — проект Linux Kernel Driver Verifier, microdrivers research, проект Nooks от Майкла Свифта в Washington Univ. (2003) — все остались в академии.
Единственное близкое — userspace-драйверы через uio/vfio. Но это только для устройств, которые могут жить полностью в user space (DPDK, SPDK, мониторящие утилиты). Драйвер блочного устройства в user space вы не запустите — его контракты с VFS и блочным слоем идут изнутри ядра.
4.2. MINIX 3 — драйверы в Ring 3 как процессы
Идеологически — наш ближайший родственник. Танненбаум вынес все драйверы (сеть, диск, файловая система, таймер) в отдельные пользовательские процессы. Если драйвер падает — reincarnation server (тот самый supervisor у нас) перезапускает его.
Цена — очень дорогая IPC. Каждое чтение блока с диска проходит: file system server → disk driver → ядро → disk driver → file system server. Четыре context switch на одно чтение. На бенчмарках файловой системы MINIX 3 в 5-10 раз медленнее Linux на одинаковом железе.
Мы взяли идею MINIX 3 (драйвер падает — рестарт), но сменили изоляцию с Ring 0 ↔ Ring 3 на Ring 0 ↔ Ring 1, и заменили синхронный IPC на lock-free очереди. Получилось дешевле.
4.3. seL4 — формальная верификация
seL4 — микроядро на 10 000 строк кода, формально верифицированное в теореме-доказателе Isabelle/HOL. Это значит: для каждой функции ядра математически доказано, что она делает ровно то, что написано в спецификации, и ничего больше. Никаких багов в самом ядре — доказано.
Архитектурно seL4 ближе к L4-семейству: всё, кроме планировщика и IPC — в user space. Драйверы — в user space процессах, как в MINIX 3. IPC — синхронный по сообщениям. Используется в авионике (Boeing, DARPA HACMS), в автомобильной электронике, в военном оборудовании.
Цена — экстремальная сложность разработки. Чтобы добавить новый сервис в seL4, нужно либо переверифицировать часть ядра, либо писать его как user-space сервер с собственными доказательствами безопасности. Это годы работы для команды из десятков людей.
Мы себе не ставим задачу формальной верификации. Наша цель — чтобы драйвер не уносил приложение, а не чтобы каждая функция ядра была доказательно безбажна. За это мы платим десятками тысяч строк кода вместо десятков тысяч строк формальных спецификаций.
4.4. Singularity / Midori — software isolation
Singularity (Microsoft Research, 2003-2008) и его продолжение Midori сделали ход конём: отказались от аппаратной изоляции вообще. Всё ядро написано на Sing# (типобезопасный диалект C#), компилятор гарантирует, что драйвер не выйдет за свои границы памяти. Никаких колец, никаких MMU — все процессы в одном адресном пространстве, изоляция статическая.
Это оказалось дешевле по runtime (нет MMU-переключений), но дороже по разработке: пришлось написать новый язык, новый компилятор, новую рантайм-систему. Microsoft закрыл проект.
В нашем случае — интересная аналогия. Если поверх ядра крутится приложение на Rust, типобезопасность языка гарантирует, что прикладной код не выйдет за границы своих структур. Но на уровне драйверов мы всё-таки опираемся на аппаратные кольца — потому что драйверы пишутся на C++, и UB в них объективно случается чаще.
4.5. Сводная табличка
| Система | Изоляция драйверов | Цена IPC | Восстановление | Где работает |
|---|---|---|---|---|
| Linux | Нет | — | kdump (постфактум) | Везде |
| Windows | UMDF (некоторые драйверы) | RPC через ALPC | Перезапуск UMDF-хоста | Windows |
| MINIX 3 | Ring 3 процессы | ~3 мкс синхр. | Reincarnation server | Embedded, исследования |
| seL4 | User-space процессы | ~1 мкс | Формально доказана | Авионика, авто |
| Singularity | Software (типы) | ~50 нс | Restart процесса | Закрыта |
| Наш unikernel (Ring 0) | Нет | — | Только halt | Cloud, наша старая версия |
| Наш unikernel (Ring 1) | Аппаратная Ring 1 + IOMMU | ~100 нс lock-free | Supervisor watchdog | С ENABLE_RING1_DRIVERS=ON |
5. Как у нас
5.1. Карта файлов
Архитектура Ring 1 появилась в декабре 2025 года и прошла шесть фаз (Phase 0–5). Итог — 32 657 строк в 40 файлах.
Что где лежит:
include/
├── ring1_driver.hh # 546 строк — базовый класс драйвера
├── ring1_supervisor.hh # 678 строк — supervisor API
├── ring1_ipc.hh # 423 строки — IPC контракты
├── ring1_exceptions.hh # 113 строк — exception registry
└── iommu_controller.hh # 247 строк — DMA-protection abstraction
core/
├── ring1_driver.cc # 50 строк — base class implementation
├── ring1_supervisor.cc # 1342 строки — главный реактор
├── ring1_ipc.cc # 370 строк — lock-free очереди
├── ring1_exceptions.cc # 94 строки — registry, dispatch
└── ring1_test_runner.cc # 292 строки — встроенный test harness
arch/x64/
├── ring1_dispatcher.cc # 346 строк — диспетчер прерываний/исключений
├── ring1_interrupt_stub.S # 181 строка — ассемблерные стабы
├── iommu.cc # 567 строк — Intel VT-d driver
├── arch-cpu.hh # +21 строка — IST[3], set_ring1_exception_stack()
├── exceptions.cc # +87 строк — Ring 1 exception path
└── mmu.cc # +39 строк — page fault routing
drivers/
├── virtio-net.cc / .hh # +384 строки — точка ветвления Ring 0 / Ring 1
├── virtio-net-ring0.cc # 940 строк — старый код, без изменений по сути
├── virtio-net-ring1.cc # 1022 строки — новый Ring 1 драйвер
├── virtio-blk.cc / .hh, virtio-scsi.cc / .hh, virtio-rng.cc / .hh
└── virtio-net-base.hh # 69 строк — общая база
test/
├── ring1_driver_test.cc # 304 строки
├── ring1_ipc_test.cc # 451 строка
├── ring1_supervisor_test.cc # 454 строки
└── ring1_cross_driver_isolation_test.cc # 981 строка — 33 кросс-теста изоляции
5.2. Жизненный цикл драйвера
Все Ring 1 драйверы наследуются от ring1_driver и реализуют
один и тот же интерфейс:
class my_driver : public ring1_driver {
public:
int init() override; // hardware probe, allocation
void shutdown() override; // graceful stop, release
int reset_hardware() override; // for restart
void on_page_fault(exception_frame*) override;
void on_gpf(exception_frame*) override;
};
Состояния:
Останов — кооперативный, не принудительный. Каждый рабочий цикл
драйвера проверяет атомарный флаг _shutdown_requested:
while (!_shutdown_requested.load(std::memory_order_acquire)) {
process_one_packet();
}
Это безопаснее, чем pthread_cancel(): успеваем освободить
ресурсы, не оставляем висящих lock’ов. Следствие — если драйвер
заблокирован в bus-контракте с устройством и не проверяет флаг,
shutdown задержится. Мы это знаем; для virtio-net подобного не
случается, потому что цикл poll’ит очереди.
5.3. Сборка
Всё включается одной CMake-опцией:
cmake -DENABLE_RING1_DRIVERS=ON ..
Когда выключено (default до сих пор) — собирается в точности
старый монолитный билд. Никаких #ifdef в горячих путях нет,
потому что мы пошли путём разделённых файлов: virtio-net-ring0.cc
и virtio-net-ring1.cc собираются альтернативно через Makefile.
Это даёт ноль регрессии для тех, кто Ring 1 не использует.
5.4. Что мы намерили
Метрики со стенда demo.kernix.ru после Phase 5:
| Метрика | Ring 0 (old) | Ring 1 (new) | Delta |
|---|---|---|---|
| Interrupt latency, virtio-net | ~480 нс | ~510 нс | +6% |
| HTTP throughput приложения под нагрузкой | 9.7 Гбит/с | 9.6 Гбит/с | -1% |
| Время восстановления после краша драйвера | halt | 0.5–2.5 с (backoff) | — |
| Память на supervisor + IPC | 0 | ~2 МБ | +2 МБ |
То есть мы заплатили ~1% на горячем пути и 2 МБ памяти. За это получили: падение драйвера → автоматический рестарт за секунду без остановки приложения. На наш взгляд — выгодная сделка.
6. Что выбрать
Простая матрица решений для архитектора, который выбирает, включать ли Ring 1 в своей инсталляции:
| Сценарий | Включать Ring 1? | Почему |
|---|---|---|
| Cloud-инсталляция на KVM с современным CPU и VT-d | Да | Полный набор: ring isolation + IOMMU + supervisor |
| Bare metal с Intel VT-d, серверный сегмент | Да | То же самое |
| KVM на старом CPU без VT-d | Да | Software isolation остаётся, теряем DMA-protection |
| Embedded ARM64 (NanoPi R3S и подобное) | Да, но выгода неполная | Только supervisor watchdog, нет аппаратного Ring 1 |
| Stress-test инсталляция, где нужен абсолютный максимум throughput | Можно отключить | -1% throughput иногда критичен |
| Лаборатория, демо, разработка | Да | Видно, как драйвер падает и восстанавливается |
Для подавляющего большинства production-инсталляций ответ — включить. Цена в 1% throughput компенсируется тем, что вам больше не звонят в 3 часа ночи с воплем «всё легло».
Что мы пока не перенесли в Ring 1
Честно: не всё. Драйверы PCI-host bridge, ACPI, локальный APIC, ATA legacy — пока в Ring 0. Это либо потому, что они нужны для самой загрузки (Ring 1 ещё не инициализирован), либо потому, что они slowpath и риск падения низкий. Roadmap, что осталось, ведётся отдельно.
7. Шишки
Мы написали 32 тысячи строк кода. Ровно три бага из них стоили нам по полтора-два дня каждый. Это не «всё прошло гладко» — это «микроядро с прямой доставкой прерываний — упрямое». Делимся.
Шишка 1. IST[3] нужно ставить до загрузки IDT
Симптом: page fault во время раннего бута, когда впервые срабатывает прерывание из векторов 34-36. Стек ломается, ядро зависает в triple fault, виртуалка ребутится в цикле.
Причина: дескрипторы IDT для векторов 34-36 ссылаются на IST[3].
Мы загружали IDT в init_on_cpu(), а потом настраивали IST[3].
В промежутке между этими двумя действиями могло прилететь
прерывание (особенно от virtio-net на быстром стенде), и оно
попадало в невалидный стек.
Лечение — arch/x64/arch-cpu.hh, в init_on_cpu():
ltr(gdt_tss * 8);
#ifdef ENABLE_RING1_DRIVERS
set_ring1_exception_stack(); // ← ДО idt.load_on_cpu()!
#endif
idt.load_on_cpu();
Урок: настраивай ресурсы, на которые ссылаются дескрипторы, строго до того, как дескрипторы становятся активными. Звучит очевидно, но мы это пропустили.
Шишка 2. APIC EOI забывать нельзя
Симптом: после первого прерывания на векторе 35 драйвер начинает получать его с частотой 50 кГц, вешая CPU на 100%. Прокси работает, но «как-то медленно» — потому что один из CPU полностью ушёл в interrupt storm.
Причина: написали ring1_dispatch_interrupt(), забыли вызвать
processor::apic->eoi(). Уровневое прерывание (level-triggered)
не снималось с линии, APIC честно его доставлял снова и снова.
Лечение в arch/x64/ring1_dispatcher.cc:
void ring1_dispatch_interrupt(unsigned vector, exception_frame* ef) {
if (auto driver = lookup_driver(vector)) {
driver->handle_interrupt(ef);
}
processor::apic->eoi(); // ← КРИТИЧНО: без этого storm
}
Урок: в любом обработчике прерывания первое и последнее, что нужно проверить — есть ли EOI. Уровневые vs edge-triggered — поведение разное, но EOI не помешает никому.
Шишка 3. Не вызывать abort() под локом
Самая дорогая. Симптом: рестарт приложения после краша драйвера
зависает в pthread_create. Watchdog восстанавливает приложение,
оно пытается стартовать новый поток, и зависает навечно. Никаких
сообщений в логе, никакого taskstats — просто тишина.
Причина — классический deadlock с участием vma_list_mutex.
Когда падал app-thread с page fault, путь был:
core/mmu.cc::vm_fault()
WITH_LOCK(vma_list_mutex.for_read()) {
...
if (needs_sigsegv) vm_sigsegv(...); // → abort() → hlt
} // ← lock НЕ снят
abort() на упавшем потоке заходил в sti; hlt навсегда, держа
read-lock на vma_list_mutex. Когда watchdog рестартил
приложение, новый pthread пытался взять write-lock на тот же
mutex — и блокировался до конца времён.
Лечение — снимать lock до потенциального hlt:
bool needs_sigsegv = false;
{
SCOPE_LOCK(vma_list_mutex.for_read());
if (vma == end || access_fault(vma, ef)) needs_sigsegv = true;
else vma->fault(addr, ef);
} // ← lock освобождён здесь
if (needs_sigsegv) vm_sigsegv(addr, ef); // безопасно abort'ить
Урок: никогда не вызывайте функцию, которая может не вернуться, под захваченным локом. Звучит как тривиальное правило — но в обычном коде функция всегда возвращается, и об этом просто не думаешь. В микроядерной архитектуре, где обработчик исключения может уйти в бесконечный hlt, это становится абсолютным правилом.
Сосед закрывает ноутбук, допивает остывший чай и говорит:
— Ладно. Если оно правда так работает — собери мне fault-injection демо на следующей неделе. На моём билде, не на твоём.
Ядерный разработчик кивает:
— На твоём — так на твоём.
Стрелка часов на стене переговорки «Сосна» доходит до конца дня.