Введение

В программировании компьютерных систем, обработчик прерываний, также известный как подпрограмма обслуживания прерывания или ISR, является специальным блоком кода, связанным с конкретным условием прерывания. Обработчики прерываний инициируются аппаратными прерываниями, инструкциями программных прерываний или программными исключениями и используются для реализации драйверов устройств или переходов между защищенными режимами работы, такими как системные вызовы. Традиционной формой обработчика прерываний является аппаратный обработчик прерываний. Аппаратные прерывания возникают из-за электрических условий или протоколов низкого уровня, реализованных в цифровой логике, обычно обрабатываются посредством жестко закодированной таблицы векторов прерываний, асинхронно относительно нормального потока выполнения (в соответствии с уровнями маскирования прерываний), часто используют отдельный стек и автоматически переходят в другой контекст выполнения (уровень привилегий) на время выполнения обработчика прерывания. В целом, аппаратные прерывания и их обработчики используются для обработки условий высокого приоритета, требующих прерывания текущего кода, выполняемого процессором. Позже стало удобно, чтобы программное обеспечение могло запускать тот же механизм посредством программного прерывания (форма синхронного прерывания). Вместо использования жестко закодированной таблицы диспетчеризации прерываний на аппаратном уровне, программные прерывания часто реализуются на уровне операционной системы в виде функции обратного вызова. Обработчики прерываний выполняют множество функций, которые варьируются в зависимости от причины прерывания и скорости выполнения задачи обработчиком прерывания. Например, нажатие клавиши на компьютерной клавиатуре или перемещение мыши генерируют прерывания, которые вызывают обработчики, считывающие нажатую клавишу или положение мыши и копирующие соответствующую информацию в память компьютера. Обработчик прерываний является низкоуровневым аналогом обработчиков событий. Однако обработчики прерываний имеют необычный контекст выполнения, множество жестких ограничений по времени и объему памяти, а их по своей сути асинхронный характер делает их печально известными своей сложностью отладки стандартными методами (воспроизводимые тестовые примеры обычно отсутствуют), что требует специализированных навыков – важной части системного программирования – от инженеров-программистов, работающих на уровне аппаратных прерываний.

Флаги прерывания

В отличие от других обработчиков событий, от обработчиков прерываний ожидается установка флагов прерываний в соответствующие значения как часть их основной функциональности. Даже в процессоре, поддерживающем вложенные прерывания, обработчик часто вызывается при глобально заблокированных всех прерываниях аппаратной операцией процессора. В такой архитектуре обработчик прерываний обычно сохраняет минимально необходимый контекст, а затем как можно скорее сбрасывает флаг глобального запрета прерываний, чтобы разрешить прерывания более высокого приоритета прерывать текущий обработчик. Также важно, чтобы обработчик прерываний подавлял источник текущего прерывания каким-либо способом (часто переключением бита флага в регистре периферийного устройства), чтобы текущее прерывание не повторялось сразу после выхода из обработчика, что привело бы к бесконечному циклу. Выход из обработчика прерываний с системой прерываний в абсолютно правильном состоянии при любых обстоятельствах может быть сложной и кропотливой задачей, а ошибки в его реализации являются причиной многих серьезных сбоев, приводящих к полной остановке системы. Эти сбои иногда проявляются спорадически, когда редкий случай ошибки не возникает неделями или месяцами непрерывной работы. Формальная проверка обработчиков прерываний чрезвычайно трудна, а тестирование обычно выявляет только наиболее распространенные режимы отказа, поэтому незаметные, спорадические ошибки в обработчиках прерываний часто доходят до конечных пользователей.

Контекст исполнения

В современной операционной системе, при входе в обработчик аппаратного прерывания, контекст исполнения оказывается сложным и неоднозначным. В целях повышения производительности, обработчик обычно запускается в контексте памяти и исполнения текущего процесса, с которым он не имеет прямой связи (прерывание, по сути, временно захватывает текущий контекст – учет времени работы процесса часто приписывает время, затраченное на обработку прерывания, прерванному процессу). Однако, в отличие от прерванного процесса, прерывание, как правило, повышается аппаратным механизмом процессора до уровня привилегий, достаточного для непосредственного доступа к аппаратным ресурсам.

Расчет пространства стека

В микроконтроллере низкого уровня чип может не иметь режимов защиты и блока управления памятью (MMU). В таких чипах контекст выполнения обработчика прерывания будет практически идентичен контексту прерванной программы, которая обычно выполняется на небольшом стеке фиксированного размера (ресурсы памяти традиционно были крайне ограничены в устройствах этого класса). Часто поддерживаются вложенные прерывания, что усугубляет использование стека. Основным ограничением для обработчика прерывания в данной задаче программирования является недопущение превышения доступного стека в наихудшем случае, что требует от программиста всестороннего анализа требований к стековому пространству каждого реализованного обработчика прерывания и задачи приложения. В случае превышения выделенного стекового пространства (состояние, известное как переполнение стека), это обычно не обнаруживается аппаратно в чипах этого класса. Если стек выходит за пределы другой записываемой области памяти, обработчик, как правило, будет работать корректно, но приложение может выйти из строя позже (иногда значительно позже) из-за побочного эффекта обработчика – повреждения памяти. Если стек выходит за пределы не записываемой (или защищенной) области памяти, сбой обычно происходит непосредственно в обработчике (как правило, это более простой случай для последующей отладки). В случае записи можно реализовать сторожевой стек – фиксированное значение, расположенное сразу за пределами допустимого стека, которое может быть перезаписано, но не будет, если система работает правильно. Обычно наблюдается повреждение сторожевого стека с помощью механизма сторожевого таймера. Это позволяет обнаружить большинство случаев переполнения стека в момент времени, близкий к моменту возникновения ошибки. В многозадачной системе каждая нить выполнения обычно имеет свой собственный стек. Если для прерываний не предусмотрен специальный системный стек, прерывания будут потреблять стековое пространство из стека прерванной нити выполнения. Эти системы обычно содержат MMU, и пользовательские стеки обычно конфигурируются таким образом, чтобы переполнение стека обнаруживалось MMU, либо как системная ошибка (для отладки), либо для переназначения памяти с целью расширения доступного пространства. Ресурсы памяти на этом уровне микроконтроллера, как правило, гораздо менее ограничены, что позволяет выделять стеки с достаточным запасом безопасности. В системах, поддерживающих большое количество нитей, предпочтительнее, чтобы аппаратный механизм прерывания переключал стек на специальный системный стек, чтобы ни один из стеков нитей не должен был учитывать использование вложенных прерываний в наихудшем случае. Даже небольшие процессоры, начиная с 8-битного Motorola 6809 1978 года, предоставляли отдельные указатели стека для системы и пользователя.

Ограничения по времени и совпадению

По многим причинам крайне желательно, чтобы обработчик прерывания выполнялся как можно быстрее, и настоятельно не рекомендуется (или запрещено) использовать аппаратное прерывание для вызова потенциально блокирующих системных вызовов. В системе с несколькими ядрами выполнения вопросы повторного входа также имеют первостепенное значение. Если система поддерживает аппаратный DMA, проблемы конкуренции могут возникнуть даже при наличии только одного ядра процессора. (Часто микроконтроллеры среднего уровня не имеют уровней защиты и MMU, но при этом оснащены DMA-контроллером с множеством каналов; в этом случае многие прерывания обычно генерируются самим DMA-контроллером, и от соответствующего обработчика прерываний ожидается особая осторожность.) Современная практика заключается в разделении обработчиков аппаратных прерываний на "первую половину" и "вторую половину". "Первая половина" (или первый уровень) принимает исходное прерывание в контексте выполняющегося процесса, выполняет минимально необходимую работу для перевода оборудования в менее критическое состояние (например, освобождает заполненный буфер приема) и затем планирует выполнение "второй половины" (или второго уровня) в ближайшем будущем с соответствующим приоритетом планирования; после запуска "вторая половина" работает в собственном контексте процесса с меньшими ограничениями и завершает логическую операцию обработчика (например, передает вновь полученные данные в очередь данных операционной системы).

Разделенные обработчики в современных операционных системах

В нескольких операционных системах, таких как Linux, Unix, macOS, Microsoft Windows, z/OS, DESQview и некоторые другие, использовавшиеся в прошлом, обработчики прерываний разделены на две части: обработчик прерываний первого уровня (FLIH) и обработчики прерываний второго уровня (SLIH). FLIH также известны как аппаратные или быстрые обработчики прерываний, а SLIH – как медленные/мягкие обработчики прерываний или отложенные вызовы процедур в Windows. FLIH реализует, как минимум, платформо-зависимую обработку прерываний, аналогичную подпрограммам прерываний. В ответ на прерывание происходит переключение контекста, и код для обработки прерывания загружается и выполняется. Задача FLIH – быстро обработать прерывание или записать критически важную информацию, специфичную для платформы, которая доступна только в момент прерывания, и запланировать выполнение SLIH для дальнейшей, более длительной обработки прерывания. FLIH вызывают джиттер при выполнении процессов. FLIH также маскируют прерывания. Уменьшение джиттера особенно важно для операционных систем реального времени, поскольку они должны гарантировать завершение выполнения конкретного кода в течение оговоренного времени. Чтобы уменьшить джиттер и потенциальную потерю данных из-за замаскированных прерываний, программисты стремятся минимизировать время выполнения FLIH, перенося как можно больше задач в SLIH. С учетом скорости современных компьютеров, FLIH могут реализовывать всю платформо- и аппаратно-зависимую обработку, а SLIH – для дальнейшей платформенно-независимой, длительной обработки. FLIH, обслуживающие аппаратное обеспечение, обычно маскируют связанное с ними прерывание (или оставляют его замаскированным, в зависимости от ситуации) до завершения выполнения. (Необычный) FLIH, который снимает маскировку со связанного с ним прерывания до завершения, называется реентрантным обработчиком прерываний. Реентрантные обработчики прерываний могут вызвать переполнение стека из-за множественных прерываний одним и тем же вектором прерывания, поэтому их обычно избегают. В системе приоритетных прерываний FLIH также (на короткое время) маскирует другие прерывания с равным или более низким приоритетом. SLIH выполняет задачи длительной обработки прерываний аналогично процессу. SLIH либо имеют выделенный поток ядра для каждого обработчика, либо выполняются пулом рабочих потоков ядра. Эти потоки находятся в очереди ожидания в операционной системе до тех пор, пока им не будет выделено процессорное время для обработки прерывания. SLIH могут иметь длительное время выполнения и, следовательно, обычно планируются аналогично потокам и процессам. В Linux FLIH называются верхней половиной, а SLIH – нижней половиной или bottom half. Это отличается от именования, используемого в других Unix-подобных системах, где оба являются частью bottom half.