Прерывание — событие изменяющее последовательность инструкций, исполняемых процессором. Они бывают синхронные —вызваны текущим контекстом исполнения и асинхронные — вызваны из вне. Согласно терминологии Intel синхронные — исключения, асинхронные - прерывание.
Прерывания генерируются довольно многими источниками: таймеры, устройства IO. Исключения вызываются обычно либо ошибками в коде, либо аномальными ситуациями, как отсутствие страницы в памяти.
Когда происходит прерывание процессор должен сохранить текущий счётчик команд eip в стеке режима ядра и занести в eip адрес, соответсвующий обработчику прерывания.
Обработка прервания должна удовлетворять некоторым условиям:
- Ядро должно, как можно быстрее обработать прерывание. Потому они делятся на неотложные и те, обработка которых может подождать, достаточно просто отметить на будущее, что нужно сделать.
- Ядро должно суметь обработать вложенные прерывания.
- Количество критических секций, где прерывания должны быть отключены, должно быть минимально.
Маскируемые прерывания — запросы на прерывания (IRQ запросы) могут быть “замаскированы” и не вызвать прерывания исполнения.
Немаскируемые прерывания NMI (Non Mascable Interrupt) — всегда должны быть обработаны, достаточно небольшое количество прерываний являются таковыми.
Каждое прерывание и исключение в x86 идентифицируется 8 битным беззнаковым числом — вектором прерывания.
Ошибки — синхронные с повторением прерывания. Если ошибку можно обработать, то в стэке ядра сохраняется регистр eip, указывающий на инструкцию, которая вызвала ошибку и будет переисполнена.
Ловушки — синхронные без повторения прерывания. Вызываются определёнными инструкциями, после выполнения которых управление вернётся процессу, и eip будет указывать на следующую инструкцию. Ловушки применяются в основном для отладки.
Программные исключения — возникают по запросу из программы: int. Используются, например, для системных вызовов.
Кратко опишем принцип работы системного вызова.
В x86 системный вызов может быть организован несколькими путями. Первый — через прерывание int 0x80, которое переключало контекст пользователя на ядреный. Второй — выполнить ассемблерную инструкцию — sysenter. Выйти из системного вызова можно с помощью iret или sysexit соответсвенно. При этом номер системного вызова, который нужно выполнить, сохраняется в регистре eax.
Теперь вернёмся к общей теме прерываний и исключений.
У каждого аппаратного устройства на плате может 1 и более линия IRQ. Все они подключены к контролеру прерываний, например к APIC, который принимает запросы на прерывание для процессора. Если IRQ выставлена, то контролер преобразует принятый запрос в вектор, сохраняет в порте ввода-вывода и посылает сигнал INTR процессору, после чего ждёт подтверждение от процессора о приёме прерывания и т.д.
IRQ линии могут быть выборочно отключены, но прерывание при этом не потеряется, оно будет доставлено процессору после включения соответствующего прерывания. Но это не маскировка сигналов. Сигналы маскируются с помощью флага IF в регистре флагов EFLAGS. Если он сброшен, то маскируемые прерывания игнорируются.
Как было сказано ранее обработчик прерываний должен быть быстрым, но иногда ему нужно выполнять очень большое количество работы. Потому прерывания делиться на TOP HALF — которые нужно немедленно обработать, например аппаратные ошибки, и BOTTOM HALF — обработку которых можно отложить. Прерывание может быть сгенерировано несколькими способами:
Level: Прерывание регистрируется при изменении сигнала с 0 на 1. После чего необходимо подтверждение об обработке, чтобы сигнал снова опустился в 0 и мы могли принять следующее прерывание. Как видно, это не очень эффективно, потому что мы игнорируем все другие прерывания, пока обрабатываем текущее.
Edge: Прерывание регистрируется изменением напряжения на входе с 0 на 1 или с 1 на 0. Что позволяет обрабатывать прерывания во время прерывания. NAPI (new API): подход к генерации прерываний для сетевых драйверов Linux. Идея в том, чтобы обрабатывать большой(критический) пул сетевых пакетов сразу. Как только пакетов становиться больше определённого значения генерируется прерывание и происходит обработка всего пула сразу.
Существуют несколько контекстов работы процесса:
-
task — обычный контекст режима ядра. В этом контексте макрос current() вернёт task_struct текущего процесса. В этом контексте процесс может быть свободно усыплён, прерван, так же разрешены переходы в другие контексты, например в ядерный.
-
IRQ — контекст прерывания. В этом контексте current() вернёт task_struct прерванного процесса, прерывание не ассоциировано с каким бы то ни было процессом. Другие прерывание в этом контексте обычно выключены. В этом контексте нельзя уснуть. Переключиться на другой процесс тоже нельзя. В этом контексте мы обрабатываем прерывания типа TOP HALF. Обработчики должны быть быстрыми и простыми.
-
SOFT IRQ — отличается от предыдущего тем, что другие прерывания в этом контексте разрешены. В этом контексте обрабатываются BOTTOM HALF прерывания, обработка которых может подождать и в общем не столько критична, как из верхней половины. В этом режиме соответсвенно возможны вложенные прерывания.
-
_atomic — в нём в основном работаю ядерные функции. Макрос current() имеет смысл, он возвращает task_struct текущего процесса. Прерывания разрешены, но переключить на исполнение другого процесса нас не могут. В таком режиме исполняются атомарные операции.
-
User — обычные пользовательские процессы.
- user -> task (exception/syscall)
- user -> irq (interrupt)
- task -> irq (interrupt)
- task -> atomic (запрет переключения)
- task -> user (iret)
- task -> task (многозадачность, вызов schedule)
- irq -> soft irq - раазрешить прерывания
- irq -> task
- soft -> irq
- soft -> task
- atomic -> task
- atomic -> irq
/* (/include/linux/preempt.h)
* Are we doing bottom half or hardware interrupt processing?
* Are we in a softirq context? Interrupt context?
* in_softirq - Are we currently processing softirq or have bh disabled?
* in_serving_softirq - Are we currently processing softirq?
*/
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
/*
* Are we in NMI context?
*/
#define in_nmi() (preempt_count() & NMI_MASK)
#define in_atomic() (preempt_count() != 0)
- Добровольно — процесс может позвать schedule(). Однако тут есть забавный факт — планировщик может снова вызвать этот же процесс, что можно обойти предварительно изменив состояние процесса макросом — set_current_state(). Как мы разбирали раньше такое может быть проделано только в Task контексте.
- Принудительно — прерывание по таймеру.
schedule() (/kernel/sched/core.c):
{
...
preempt_disable(); // запрещаем вытеснение - schedule
__schedule(false);
sched_preempt_enable_no_resched(); // разрешаем вытеснение - schedule
...
}
__schedule() (/kernel/sched/core.c):
{
...
local_irq_disable(); // запрещаем прерывания
...
next = pick_next_task(rq, prev, cookie); // получаем следующий процесс
...
rq = context_switch(rq, prev, next, cookie);
/* переключаем процессы: меняем местами gs(current), push регистры, меняем rsp, rip менять не нужно. pop регистры, ret */
}
(!TODO)