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

Пишем операционную систему Триалогия - Прерывания: IDT, IOAPIC и LAPIC

В двух прошлых частях постоянно всплывал "тик таймера", запускающий переключение контекста. Но как этот тик вообще доходит до процессора? А нажатие клавиши, дочитанный сектор диска, пришедший сетевой пакет? Через прерывания! Это нервная система ядра, механизм, которым внешний мир получает право прервать процессор. Прерывание, это сигнал: "Стоп, займись мной немедленно". Процессор прерывает то, что делал, прыгает к обработчику, делает нужное и потом возвращается ровно туда, где был. Источников три: Пара сокращений сразу: Вот дополненный список с полными английскими терминами и объяснениями на русском: IDT - Interrupt Descriptor Table Таблица дескрипторов прерываний. Специальная структура данных процессора x86, содержащая адреса обработчиков исключений и прерываний. Когда возникает прерывание или исключение, процессор по номеру вектора находит соответствующий обработчик в IDT. IRQ Interrupt Request Запрос на прерывание. Сигнал от аппаратного устройства (клавиатуры, таймера, сетевой карт
Оглавление
Триалогия - Пишем операционную систему
Триалогия - Пишем операционную систему

В двух прошлых частях постоянно всплывал "тик таймера", запускающий переключение контекста. Но как этот тик вообще доходит до процессора? А нажатие клавиши, дочитанный сектор диска, пришедший сетевой пакет? Через прерывания! Это нервная система ядра, механизм, которым внешний мир получает право прервать процессор.

Что такое прерывание

Прерывание, это сигнал: "Стоп, займись мной немедленно". Процессор прерывает то, что делал, прыгает к обработчику, делает нужное и потом возвращается ровно туда, где был. Источников три:

  • Железо (устройства): нажата клавиша, диск закончил, пришёл пакет.
  • сам процессор (исключения): доступ к несуществующей памяти, деление на ноль, нарушение защиты.
  • другие процессоры (IPI): "перепланируйся", "очисти свой кэш адресов".

Пара сокращений сразу:

Вот дополненный список с полными английскими терминами и объяснениями на русском:

IDT - Interrupt Descriptor Table Таблица дескрипторов прерываний. Специальная структура данных процессора x86, содержащая адреса обработчиков исключений и прерываний. Когда возникает прерывание или исключение, процессор по номеру вектора находит соответствующий обработчик в IDT.

IRQ Interrupt Request Запрос на прерывание. Сигнал от аппаратного устройства (клавиатуры, таймера, сетевой карты и т.д.), сообщающий процессору о необходимости обработки события.

GSI Global System Interrupt Глобальное системное прерывание. Уникальный номер прерывания в системе APIC. Используется для сопоставления входов IOAPIC с векторами прерываний процессоров.

EOI End Of Interrupt Конец обработки прерывания. Сигнал контроллеру прерываний (обычно LAPIC), что текущий обработчик завершил работу и контроллер может принимать и выдавать новые прерывания того же типа.

IPI Inter-Processor Interrupt Межпроцессорное прерывание. Прерывание, которое одно ядро процессора отправляет другому ядру. Используется в многопроцессорных системах для синхронизации, планирования задач и управления состоянием ядер.

IOAPIC Input/Output Advanced Programmable Interrupt Controller Контроллер прерываний ввода-вывода. Получает аппаратные IRQ от устройств и перенаправляет их в виде прерываний на нужные процессоры через систему APIC. Заменяет старый контроллер PIC в современных системах.

LAPIC Local Advanced Programmable Interrupt Controller Локальный контроллер прерываний процессора. Находится в каждом ядре CPU. Принимает прерывания от IOAPIC, генерирует локальные прерывания и отправляет IPI другим ядрам. Также содержит локальный таймер APIC.

ISR - Interrupt Service Routine обработчик прерывания. Это ещё один важный термин, который обычно встречается рядом с перечисленными выше.

Связь между ними

Упрощённая схема обработки аппаратного прерывания:

Триалогия - схема обработки аппаратного прерывания
Триалогия - схема обработки аппаратного прерывания

IDT: телефонная книга процессора

Триалогия - таблица IDT из 256 записей
Триалогия - таблица IDT из 256 записей

В центре стоит таблица из 256 записей, IDT. Каждая запись (gate) относится к вектору (от 0 до 255) и говорит - когда придёт прерывание с этим номером, прыгай по этому адресу обработчика. Gate занимает всего восемь байт и несёт смещение обработчика, селектор кода ядра и тип.

void set_int_handler(uint8_t index, void* handler, uint8_t type) {

idt[index].selector = 0x08; // сегмент кода ядра

idt[index].address_0_15 = (uint32_t)handler & 0xFFFF;

idt[index].address_16_31 = (uint32_t)handler >> 16;

idt[index].type = type; // 0x8E interrupt gate · 0x8F trap gate · 0xEE syscall (DPL 3)

}

// lidt загружает таблицу — на КАЖДОМ CPU, ведь IDT общая для всех

Следует иметь ввиду тип прерывания (idt[index].type = type) - interrupt gate 0x8E на время обработчика блокирует дальнейшие прерывания, trap gate 0x8F нет, а gate 0xEE с DPL 3 можно вызвать даже из пространства пользователя, это запись syscall на векторе 0x80.

IOAPIC: посредник для устройств

Устройства не подключены к процессору напрямую. Все они висят на центральном посреднике, IOAPIC. Он получает электрический сигнал устройства (как GSI), переводит его в вектор IDT и решает, на какой CPU он пойдёт, по очереди, чтобы не одно ядро забирало все прерывания, пока остальные отдыхают. Он умеет глушить (маскировать) отдельные линии и различает, линия уровневая (Level-triggered) или фронтовая (Edge-triggered), устройства PCI уровневые. Какая линия устройства какому вектору соответствует, ядро читает при старте из таблиц ACPI.

Edge-triggered (фронтовое прерывание)

Прерывание возникает по изменению сигнала (обычно по фронту: 0 -> 1).

Контроллер реагирует только на момент изменения уровня.

Особенности:

  • если прерывание потеряно, устройство может не получить обслуживание;
  • несколько устройств плохо делят одну линию;
  • использовалось в старом PIC (8259A).

Level-triggered (уровневое прерывание)

Прерывание активно, пока устройство удерживает линию в определённом состоянии (обычно в низком уровне).

Пока сигнал активен, контроллер считает прерывание ожидающим обработки.

Последовательность:

  1. Устройство выставляет IRQ.
  2. CPU получает прерывание.
  3. Обработчик читает регистр устройства и устраняет причину прерывания.
  4. Устройство снимает сигнал IRQ.
  5. Отправляется EOI.

Если устройство не сняло IRQ, прерывание будет приходить снова.

Почему PCI использует level-triggered

У PCI линии INTA#, INTB#, INTC#, INTD# являются уровневыми и active-low.

Это позволяет:

  • безопасно разделять одну линию прерывания между несколькими устройствами;
  • избежать потери прерываний;
  • проще работать в многопроцессорных системах через IOAPIC.

Если сработали одновременно сетевая карта и SATA-контроллер, линия остаётся активной, пока оба устройства не сбросят свои запросы.

В контексте IOAPIC

Для каждого входа IOAPIC в Redirection Table задаются:

  • Trigger Mode - Edge, Level
  • Polarity - Active High, Active Low

Для классического PCI обычно:

Trigger Mode = Level
Polarity = Active Low

Поэтому в ACPI-таблицах для PCI-устройств чаще всего увидишь:

Level - Triggered
Active - Low
а для старых ISA IRQ (таймер, клавиатура и т.п.) обычно:

Edge-Triggered
Active-High

LAPIC: «ухо» каждого процессора

У каждого ядра свой local APIC. Это точка входа для всего, что достаёт процессор снаружи: прерываний устройств, разведённых IOAPIC, IPI от других ядер и собственного таймера LAPIC, который, между прочим, и поставляет тот самый тик планировщика из прошлых частей.

Одно правило здесь высечено в камне: после каждого прерывания обработчик обязан послать LAPIC EOI - "готово". Только тогда LAPIC пропускает следующее прерывание.

void apic_send_eoi(void) {

lapic_write(APIC_EOI, 0); // "готово" — только теперь проходит следующий IRQ

}

Забытый EOI, это классическая неприятная ошибка- всё вроде бы работает, но после первого же такого прерывания процессор глохнет именно на этой линии, потому что LAPIC вечно ждёт "готово".

Исключения процессора: когда он сам бьёт тревогу

Векторы с 0 по 31 зарезервированы под исключения, прерывания, которые процессор поднимает сам, когда что-то идёт не так. Самое известное, это page fault (вектор 14) #PF - доступ к неотображённой памяти, ровно то, что страница-страж из статьи про стеки нарочно провоцирует, чтобы поймать переполнение стека. Рядом general protection fault (13) #GPF при нарушении защиты и деление на ноль (0). Часть этих исключений поправима (при page fault ядро подгружает недостающую страницу и продолжает если это возможно), другие это конец - double fault (8) возникает, когда срывается уже сама обработка исключения, и обычно ведёт к полной остановке.

IPI: когда процессоры говорят друг с другом

На нескольких ядрах одному CPU иногда надо подтолкнуть другого. "Ты только что разбудил поток поважнее, перепланируйся", это такой случай (IPI reschedule на векторе 0xF1, вытаскивающий другой CPU из его цикла ожидания). Или "Я изменил страничную память, очисти свой кэш адресов" (TLB-shootdown из статьи про память). Такие вызовы идут через LAPIC, один CPU пишет вектор в LAPIC целевого CPU, и там происходит прерывание.

Весь путь на примере клавиатуры

  1. Ты жмёшь клавишу, устройство шлёт сигнал в IOAPIC.
  2. IOAPIC переводит линию устройства в вектор 0x21, выбирает CPU и шлёт его в LAPIC этого CPU.
  3. LAPIC прерывает процессор.
  4. Процессор смотрит вектор 0x21 в IDT и прыгает к заглушке (Stub) обработчика (маленький кусок ассемблера).
  5. Заглушка (Stub) сохраняет все регистры, это ровно тот снимок (state) регистров из статьи про многозадачность, и вызывает C-диспетчер (handler).
  6. Диспетчер (handler) вызывает цепочку обработчиков, зарегистрированных на эту линию, здесь драйвер клавиатуры.
  7. Обработчик читает клавишу и докладывает "обработано.
  8. EOI уходит в LAPIC.
  9. Заглушка (asm Stub) восстанавливает регистры, iret, и процессор продолжает ровно там, где его прервали.

Шаг 5, кстати, и есть причина, почему именно прерывание таймера может запустить переключение контекста - заглушка (Stub) и так уже построила полный снимок (snapshot) регистров, и планировщику остаётся лишь приписать его текущему потоку и загрузить следующий.

Общий слой IRQ

Драйвер ничего этого знать не хочет, ни векторов IDT, ни GSI, ни EOI. Для этого есть абстракция. Он лишь говорит: "Зови эту функцию, когда придёт этот IRQ".

Драйвер регистрирует свой обработчик IRQ
Драйвер регистрирует свой обработчик IRQ

Флаги покрывают особые случаи - общие линии (несколько устройств на одном IRQ), PCI-INTx (уровневое) и MSI, где современные устройства уже не используют свою линию, а поднимают прерывание записью в память.

А старый PIC?

Для полноты, раньше, во времена одного CPU, это посредничество делал простой чип 8259 (PIC). Но он умеет обслуживать лишь одно ядро. Я его выключил, его код остался только чтобы заглушить его при старте, вся маршрутизация идёт через IOAPIC и LAPIC.

Сегодня только правда жизни

Прерывания, это места, где параллельность бьёт всего жёстче, ведь IRQ может ворваться в середину любой инструкции. Обработчик, который слишком долог или ставит неверную блокировку, замораживает всю систему (dead lock, self lock). Забытый EOI глушит линию. А double fault, случай, когда срывается уже сама обработка ошибки, оставляет только панику, его я отдельно подстраховал собственным task gate, чтобы он хотя бы успел выдать сообщение, а не отправлял систему молча в reset. Это был один из уроков SMP- падений из обзора.

На этом все про прерывания...

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

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