Перейти к содержанию

Прерывания

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

Прерывание (interrupt) — это событие, которое заставляет процессор прервать текущее выполнение и передать управление специальному обработчику (interrupt handler). После обработки процессор возвращается к прерванному коду.

Прерывания делятся на три категории:

Внешние (аппаратные, asynchronous) — поступают от периферийных устройств в любой момент:

  • таймер (Timer interrupt, IRQ0) — основа вытесняющей многозадачности;
  • контроллер диска — сигнализирует о завершении операции ввода-вывода;
  • сетевая карта — получен пакет;
  • клавиатура, мышь — нажата клавиша.

Исключения процессора (synchronous faults/traps) — генерируются самим процессором при определённых условиях:

  • #DE (Divide Error, вектор 0) — деление на ноль;
  • #BP (Breakpoint, вектор 3) — инструкция int 3;
  • #GP (General Protection, вектор 13) — нарушение прав (привилегированная инструкция в user mode);
  • #PF (Page Fault, вектор 14) — обращение к невалидной или отсутствующей странице;
  • #UD (Undefined Opcode, вектор 6) — неизвестная инструкция.

Программные прерывания — вызываются инструкцией int n:

  • int 3 — точка останова (breakpoint);
  • int 0x80 — устаревший способ системных вызовов на x86-32.

Обработка прерывания: что делает процессор

Когда аппаратура или исключение сигнализируют прерывание, процессор выполняет строго определённую последовательность:

 User space                          Kernel space
 ──────────────────                  ──────────────────────────────────────────────
 Процесс выполняется
        │  ← аппаратный сигнал (IRQ) или исключение (#PF, #GP, ...)
 CPU сохраняет на kernel stack:
 ┌──────────────────────────────┐
 │   SS        (сегм. стека)    │  ← если смена кольца (ring 3 → ring 0)
 │   RSP       (указатель стека)│
 │   RFLAGS    (регистр флагов) │
 │   CS        (сегм. кода)     │
 │   RIP       (адрес возврата) │
 │   Error code (если есть)     │  ← #DF, #TS, #NP, #SS, #GP, #PF, #AC (и #VC, #CP на новых CPU)
 └──────────────────────────────┘
        │  CPU читает IDTR → находит IDT в памяти
        │  берёт дескриптор IDT[вектор]
        │  из дескриптора извлекает адрес обработчика
 ┌─────────────────────┐
 │   interrupt_handler │  ← ядро обрабатывает прерывание
 │   (ring 0)          │     (обслуживает устройство, обрабатывает fault...)
 └─────────────────────┘
        │  iret — восстанавливает RIP, CS, RFLAGS, RSP, SS
 Процесс продолжает выполнение (или другой процесс, если был context switch)

Таблица дескрипторов прерываний (IDT)

IDT (Interrupt Descriptor Table) — таблица в памяти ядра, содержащая 256 записей (дескрипторов). Каждый дескриптор указывает на обработчик конкретного прерывания или исключения.

IDTR (регистр процессора)
┌──────────────────────────────────────────┐
│  base address IDT   │   limit (size-1)   │
└────────────────┬─────────────────────────┘
IDT в памяти ядра (256 × 16 байт = 4096 байт)
┌─────┬──────────────────────────────────────────────────────────┐
│  0  │ Gate descriptor: offset handler, segment sel, type, DPL  │  #DE divide error
├─────┼──────────────────────────────────────────────────────────┤
│  1  │ Gate descriptor                                          │  #DB debug
├─────┼──────────────────────────────────────────────────────────┤
│  2  │ Gate descriptor                                          │  NMI
├─────┼──────────────────────────────────────────────────────────┤
│  3  │ Gate descriptor  (DPL=3 — доступен из user mode)         │  #BP int 3
├─────┼──────────────────────────────────────────────────────────┤
│ ... │ ...                                                      │
├─────┼──────────────────────────────────────────────────────────┤
│ 13  │ Gate descriptor                                          │  #GP general protection
├─────┼──────────────────────────────────────────────────────────┤
│ 14  │ Gate descriptor                                          │  #PF page fault
├─────┼──────────────────────────────────────────────────────────┤
│ ... │ ...                                                      │
├─────┼──────────────────────────────────────────────────────────┤
│ 32  │ Gate descriptor                                          │  IRQ0  (timer)
├─────┼──────────────────────────────────────────────────────────┤
│ 33  │ Gate descriptor                                          │  IRQ1  (keyboard)
├─────┼──────────────────────────────────────────────────────────┤
│ ... │ ...                                                      │
├─────┼──────────────────────────────────────────────────────────┤
│255  │ Gate descriptor                                          │
└─────┴──────────────────────────────────────────────────────────┘

Структура одного gate descriptor (16 байт, Interrupt Gate, x86-64):
┌─────────────────────────────────────────────────────────────┐
│ байты 0..1: offset[15:0]      байты 2..3: segment selector  │
├─────────────────────────────────────────────────────────────┤
│ байт 4:  IST (биты 0..2) | 0 (биты 3..7)                    │
├─────────────────────────────────────────────────────────────┤
│ байт 5:  type (биты 0..3) | 0 | DPL (биты 5..6) | P (бит 7) │
├─────────────────────────────────────────────────────────────┤
│ байты 6..7:   offset[31:16]                                 │
├─────────────────────────────────────────────────────────────┤
│ байты 8..11:  offset[63:32]                                 │
├─────────────────────────────────────────────────────────────┤
│ байты 12..15: reserved (должны быть нули)                   │
└─────────────────────────────────────────────────────────────┘
  offset — полный 64-битный адрес обработчика
  type   — Interrupt Gate (IF=0 при входе) или Trap Gate (IF не меняется)
  DPL    — минимальный уровень привилегий для программного вызова через int n
  P      — present bit (1 — дескриптор валиден)

Структура каждой записи IDT:

  • адрес обработчика;
  • уровень привилегий (ring level), с которого разрешён вызов;
  • тип (Interrupt Gate, Trap Gate, Task Gate).

Адрес IDT хранится в специальном регистре IDTR, который загружается привилегированной инструкцией:

lidt idtr_value      /* загрузить адрес и размер IDT (только в ring 0) */

Стандартные векторы:

Вектор Тип Описание
0 Fault Divide by zero (#DE)
1 Trap Debug
2 Interrupt NMI (Non-Maskable Interrupt)
3 Trap Breakpoint (#BP, int 3)
6 Fault Invalid Opcode (#UD)
13 Fault General Protection (#GP)
14 Fault Page Fault (#PF)
32–47 Interrupt IRQ0–IRQ15 (аппаратные прерывания)
128 Trap int 0x80 (системный вызов, x86-32)

Прерывание vs. системный вызов

Аспект Прерывание Системный вызов
Источник Аппаратура или процессор Программа (инструкция syscall/int)
Асинхронность Асинхронное (в любой момент) Синхронное (контролируется программой)
Сохранение контекста Ядро сохраняет все регистры rcx, r11 перезаписываются
Задержка Непредсказуемая Предсказуемая
Примеры Timer interrupt, Page Fault read(), write(), fork()

Оба механизма переключают процессор в kernel mode и используют похожую инфраструктуру (IDT), но системный вызов — это управляемый переход, тогда как прерывание — внешнее событие.

Генерация прерываний из кода

/* int 3 — breakpoint; доставит SIGTRAP в user mode */
void trigger_breakpoint(void) {
    asm volatile("int $3");
}

/* Системный вызов write через int 0x80 (x86-32 legacy) */
void write_int80(const char *msg, int len) {
    asm volatile(
        "movl $4,  %%eax\n\t"    /* sys_write = 4 */
        "movl $1,  %%ebx\n\t"    /* fd = stdout */
        "movl %0,  %%ecx\n\t"    /* buffer */
        "movl %1,  %%edx\n\t"    /* length */
        "int  $0x80"
        : : "r"(msg), "r"(len)
        : "eax", "ebx", "ecx", "edx"
    );
}

В x86-64 для системных вызовов используется инструкция syscall, а не int 0x80.

Как ОС управляет процессами через прерывания

Основа вытесняющей многозадачности — таймерное прерывание:

  1. Аппаратный таймер (PIT/HPET/APIC timer) генерирует IRQ0 каждые ~10 мс (100 Гц) или чаще.
  2. Процессор прерывает текущий процесс и вызывает обработчик из IDT[32].
  3. Ядро сохраняет полный контекст прерванного процесса (все регистры, rip, rflags, rsp) в kernel stack.
  4. Вызывается планировщик (scheduler) — выбирает следующий процесс.
  5. Ядро восстанавливает контекст следующего процесса.
  6. Инструкция iret возвращает управление — но уже другому процессу.
Процесс A работает
     |
     | (через 10 мс)
     v
Timer IRQ0 -> IDT[32] -> timer_handler()
     |
     | Сохранить контекст A
     | Выбрать процесс B
     | Восстановить контекст B
     v
iret -> Процесс B продолжает

Упрощённый обработчик таймера в ядре:

void timer_interrupt_handler(struct pt_regs *regs) {
    /* regs содержит сохранённый контекст прерванного процесса */
    update_process_times();
    struct task_struct *next = schedule();
    if (next != current)
        context_switch(current, next, regs);
    /* iret восстановит контекст нового процесса */
}

Аналогично работают и другие прерывания: Page Fault загружает отсутствующую страницу из swap; прерывание диска разблокирует процесс, ожидавший I/O; сетевое прерывание добавляет пакет в очередь приёма.

Прерывания — механизм, которым ОС отбирает контроль у процессов и обеспечивает многозадачность.

Связанные темы

Источники

  • Intel SDM Vol. 3A: Chapter 6 — Interrupt and Exception Handling
  • man 7 signal — сигналы как высокоуровневый интерфейс к прерываниям
  • Linux kernel documentation: interrupts — https://www.kernel.org/doc/html/latest/
  • OSDev Wiki: Interrupts — https://wiki.osdev.org/Interrupts