Глава 4 Прерывания

QNX/Neutrino и прерывания

В данной главе мы рассмотрим прерывания, как с ними работать в QNX/Neutrino, их воздействие на диспетчеризацию и режим реального времени, а также некоторые стратегии их использования.

Первый вопрос, который приходит на ум: «А что такое прерывание?»

Прерывание — это в точности то, что определяется этим словом — прерывание того, что происходит в данный момент, и переход к выполнению другой задачи.

Например, предположим, что вы сидите за своим рабочим столом и выполняете задание «А». Вдруг звонит телефон — Чрезвычайно Уважаемый Клиент (ЧУК) нуждается в вашем незамедлительном ответе на некий важный вопрос. После того как вы ответите на этот вопрос, вы сможете возвратиться к заданию «А»; впрочем, возможно, что этот ЧУК изменит ваши приоритеты, и вам придется отложить задание «А» и немедленно приступить к заданию «Б».

Давайте теперь рассмотрим это в проекции на QNX/Neutrino.

В любой момент времени процессор занят обработкой готового к выполнению потока с наивысшим приоритетом (этот поток будет находиться в состоянии RUNNING («выполняется»). Чтобы вызвать прерывание, подключенная к шине компьютера аппаратура выставляет сигнал на линии прерывания (в нашей аналогии это был телефонный звонок).

Как только сигнал прерывания выставлен, ядро переключается на участок кода, который настраивает окружение для выполнения подпрограммы обработки прерывания (Interrupt Service Routine — ISR) — кода, который определяет, что должно происходить при обнаружении прерывания.

Интервал времени от момента установки аппаратурой сигнала прерывания до выполнения первой инструкции обработчика прерываний называют временем реакции на прерывание. Время реакции на прерывание измеряется в микросекундах. Различные процессоры характеризуются различными временами реакции прерывание; это зависит от быстродействия процессора, архитектуры кэша, быстродействия памяти, и, конечно, от эффективности операционной системы.

В нашей аналогии, если вы, например, слушаете музыку в наушниках и не слышите телефонного звонка, вам потребуется больше времени, чтобы обратить внимание на это «прерывание». В QNX/Neutrino может происходить то же самое, поскольку существует инструкция процессора, которая блокирует прерывания (для процессоров x86 это инструкция

cli
). Процессор не будет обращать внимание на какие бы то ни было прерывания до тех пор, пока они не будут разблокированы (инструкция
sti
для семейства x86).

Чтобы избежать процессорно-зависимых вызовов на ассемблере, QNX/Neutrino обеспечивает четыре функции: InterruptEnable() и InterruptDisable(), и InterruptLock() и InterruptUnlock(). Эти функции принимают на себя все заботы о низкоуровневых деталях всех поддерживаемых платформ.

Обработчик прерывания (ISR) обычно выполняет минимально возможный объем работы и завершается (в нашей аналогии это был бы краткий разговор по телефону с ЧУКом — не заставлять же заказчика ждать на линии несколько часов, пока мы сделаем работу! Достаточно сказать: «Не беспокойтесь, все будет сделано!»). Когда обработчик прерывания (ISR) завершается, он может либо сообщить ядру, что ничего больше делать не надо (это означает, что обработчик прерываний полностью завершил обработку события), либо что ядро должно выполнить некоторое действие, вследствие которого некий поток может переключиться в состояние READY («готов»).

В нашей аналогии сообщение ядру о том, что прерывание полностью обработано, подобно сообщению клиенту ответа на поставленный вопрос — после этого можно спокойно вернуться к тому, что мы делали раньше, зная, что вопрос клиента отработан.

Сообщение ядру о том, что требуется выполнить некоторое действие, подобно убеждению заказчика, что вы работаете над его проблемой и дополнительно сообщите, когда она будет решена. Трубка теперь повешена, но телефон может зазвонить опять.

Подпрограмма обработки прерывания

Обработчик прерывания (ISR) представляет собой фрагмент кода, ответственный за очистку источника прерывания.

Это ключевой момент, особенно с учетом того, что прерывание имеет приоритет выше, чем приоритет любой программы. Это означает, что время, затрачиваемое на выполнение обработчика прерывания, может оказать серьезное воздействие на диспетчеризацию потоков. Время выполнения ISR должно быть минимальным. Давайте исследуем этот вопрос несколько подробнее.

Очистка источника прерываний

Аппаратное устройство, которое сгенерировало прерывание, будет удерживать сигнал прерывания до тех пор, пока не удостоверится в том, что прерывание успешно обработано. Поскольку аппаратура не умеет читать мысли, программа должна сообщить ей, что отреагировала на вызвавшую прерывание причину. Обычно это выполняется путем чтения регистра состояния из определенного порта ввода/вывода или блока данных из определенного адресного пространства памяти.

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

Поскольку прерывание выполняется с более высоким приоритетом, чем любой программный поток, мы должны потратить как можно меньше времени на непосредственное выполнение обработчика прерывания, чтобы свести воздействие на диспетчеризацию к минимуму. Если очистка источника прерывания выполняется простым считыванием регистра и возможно, записью полученного значения в глобальную переменную, тогда наша задача проста.

Обработка подобного рода выполняется обработчиком прерываний (ISR) последовательного порта. Аппаратура последовательного порта генерирует прерывание по приему символа. Обработчик считывает регистр, содержащий символ, записывает этот символ в кольцевой буфер. Сделано. Общее время на обработку: единицы микросекунд. Ну, собственно, так и должно быть. Представьте, что произошло бы, если бы вы принимали символы со скоростью 115 Кбод (примерно по символу каждые 100 микросекунд); если бы вы затрачивали на обработку прерывания что-то около 100 микросекунд, у вас бы больше ни на что не осталось времени!

Не поймите меня неправильно. ISR последовательного порта может выполняться несколько дольше, потому что в нем еще предусмотрен опрос устройства на предмет наличия дополнительных символов в очереди.

Понятно, что минимизацию времени, затрачиваемого на обработку прерывания, можно трактовать как «повышение качества обслуживания клиента». В нашей аналогии это минимизация времени занятости телефонной линии, чтобы другие клиенты не услышали сигнал «занято».

А что если обработка слишком сложна? Есть два варианта развития событий:

• Затраты времени на очистку источника прерывания невелики, но надо много чего сделать с оборудованием (клиент задал нам короткий вопрос, но на подготовку ответа требуется значительное время).

• Затраты времени на очистку источника прерывания достаточно велики (клиент долго и запутанно объясняет свою проблему).

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

Второй случай достаточно уродливый. Если ISR не очистит источник прерывания на момент своего завершения, ядро немедленно будет повторно прервано программируемым контроллером прерываний (Programmable Interrupt Controller — PIC; в процессорах серии x86 серии это микросхема Intel 8259 или ей эквивалентная).

Специально для любителей контроллеров прерываний — мы вскоре кратко рассмотрим прерывания, активные как по уровню, так и по фронту.

Таким образом, ISR так и будет работать все время и не даст активизироваться никакому потоку, который мог бы выполнить обработку.

И какой же ущербный кусок железа может требовать продолжительного времени на очистку источника прерывания? Базовый контроллер дисковода PC удерживает сигнал прерывания на шине до тех пор, пока вы не прочитаете ряд его регистров состояния. К сожалению, данные в этих регистрах не всегда бывают доступны немедленно, и приходится опрашивать регистры на предмет поступления данных. Это может занять порядка миллисекунды — для компьютера это очень много!

Чтобы решить эту проблему, надо временно маскировать прерывания — явно приказать контроллеру PIC игнорировать прерывания от определенного источника прерываний, пока вы не прикажете ему сделать обратное. В этом случае, даже при активном сигнале прерывания, контроллер PIC будет игнорировать его и ничего не скажет процессору. Это позволит вашему ISR запланировать поток, чтобы вынести работу с аппаратурой за пределы обработчика прерываний. Когда ваш поток закончит передачу данных от аппаратных средств, он может приказать контроллеру PIC демаскировать это прерывание. Это позволяет снова распознавать прерывания от данного аппаратного модуля. В нашей аналогии это подобно переводу звонка ЧУКа на вашего помощника.

Передача работы потоку

Как сделать так, чтобы ISR приказал ядру запланировать поток для выполнение некоторой работы? (Или, наоборот, как сказать ядру, что ему не следует так поступать?)

Ниже приведен псевдокод типового ISR:

FUNCTION ISR

BEGIN

 определить источник прерывания

 очистить источник прерывания

 IF надо передать работу потоку THEN

  RETURN (событие);

 ELSE

  RETURN (NULL);

 END IF

END

Трюк здесь заключается в том, что вместо пустого указателя (NULL) можно возвратить некое событие (типа

struct sigevent
, мы говорили об этой структуре в главе «Часы, таймеры и периодические уведомления»).

Отметим, что событие, которое вы возвращаете, должно продолжать существовать даже после того, как будет освобожден стек ISR (потому что локальные переменные хранятся в стеке — прим. ред.). Это означает, что событие должно либо быть описано вне ISR, либо передаваться из области устойчивых данных при помощи параметра ISR area, либо быть быть описано в пределах ISR как статическое. Это ваш выбор. Если вы возвращаете событие, ядро доставляет его потоку при возврате из вашего ISR. Поскольку событие «предупреждает» поток (путем передачи ему импульса, как мы говорили в главе «Обмен сообщениями», или сигнала), это может заставить ядро выполнить перепланирование потоков, желающих получить процессор. Если ваш ISR возвращает NULL, это оповещает ядро, что в дополнительных действиях на уровне потоков нет необходимости, и перепланирования не произойдет — будет продолжать выполняться поток, вытесненный вашим ISR.

Активность прерываний по уровню и по фронту

Недостает еще одного фрагмента мозаики. Большинство контроллеров прерываний могут быть запрограммированы на чувствительность либо к уровню сигнала прерывания, либо к его фронту.

В режиме чувствительности по уровню считается, что сигнал прерывания выставлен, когда соответствующая линия шины находится в активном состоянии (это соответствует отметке «1» на рисунке ниже).

Выставление сигнала прерывания в режиме чувствительности по уровню.

Из рисунка видно, что работа с прерыванием контроллером дисковода в таком режиме привела бы к вышеупомянутой проблеме. Каждый раз при завершении ISR ядро сообщает контроллеру прерываний: «Порядок, это прерывание обработано. Сообщи мне, когда оно возникнет снова.» (отметка «2» на рисунке). Говоря техническим языком, ядро посылает контроллеру сигнал EOI (End Of Interrupt — «конец прерывания»). Контроллер PIC анализирует линию прерывания и если она все еще активна, он немедленно прерывает ядро заново (отметка «3»).

Мы могли бы обойти эту проблему, перепрограммировав контроллер прерываний в режим чувствительности по фронту.

В этом режиме прерывания распознаются контроллером только по переднему фронту сигнала.

Выставление сигнала прерывания в режиме чувствительности по фронту.

Здесь, даже если обработчик прерываний не очищает источник прерывания, после передачи ядром контроллеру сигнала EOI контроллер не может заново прервать ядро, потому что другого переднего фронта на линии прерывания после передачи EOI не будет. Для распознавания следующего прерывания на данной линии ее сначала будет необходимо деактивировать (отметка «4»), а затем активировать вновь (отметка «1»).

Похоже, что все наши проблемы решены! Будем использовать режим чувствительности по фронту и жить счастливо. Но, к сожалению, у режима чувствительности по фронту тоже есть свои проблемы.

Предположим, что ваш ISR не очистил источник прерывания. Когда ядро выдаст контроллеру сигнал EOI, аппаратные средства будут по-прежнему удерживать сигнал прерывания в активном состоянии. Однако, поскольку контроллер работает в режиме чувствительности по фронту, все последующие прерывания от этого устройства он не увидит.

Что же это за добрый парень, который так пишет ISR, чтобы тот забыл очистить источник прерывания? К сожалению, готовых рецептов здесь нет. Представьте себе ситуацию, когда два устройства (например адаптер SCSI и адаптер Ethernet), разделяющих одну и ту же линию прерывания на позволяющей это шине. (Сейчас вы скажете: «Да ну, какой придурок будет так делать?!» Ну, это иногда случается, особенно когда не хватает свободных прерываний...)

В этом случае одному и тому же вектору прерывания соответствовали бы два ISR (это, кстати, допустимо), и ядро при получении прерывания по этой линии вызывало бы их каждый раз поочередно.

Разделяемые прерывания без перекрытия.

В этом случае, поскольку только одно из аппаратных устройств (плата SCSI) было активно, когда отработал связанный с ним обработчик корректно очистил источник прерывания (этап 2.) Отметьте, что ядро вызывает ISR для платы Ethernet (этап 3) независимо ни от чего — оно просто не знает, какое конкретное устройство требовало обслуживания, поэтому всегда отрабатывает всю цепочку.

А теперь представьте себе другой случай:

Разделяемые прерывания с перекрытием.

Это как раз та самая проблемная ситуация.

Устройство Ethernet запрашивает прерывание первым. Это приводит к тому, что выставляется сигнал прерывания (передний фронт импульса распознается контроллером прерываний), и ядро вызывает первый в очереди обработчик прерывания (драйвер SCSI; этап 1). Обработчик драйвера SCSI смотрит на свои аппаратные средства и говорит: «Не, это не мое. Ладно, забудь» (этап 2). Затем ядро вызывает следующий обработчик прерывания в очереди, соответствующий плате Ethernet (этап 3). Обработчик драйвера Ethernet смотрит на свои аппаратные средства и восклицает: «О! Мои аппаратные средства запросили прерывание! Надо очистить источник». И тут, как назло, как раз в процессе очистки устройство SCSI генерирует прерывание (этап 4).

Когда ISR платы Ethernet завершит очистку источника прерываний (этап 5), сигнал прерывания по-прежнему останется выставлен вследствие возникшего прерывания от устройства SCSI. Однако, контроллер прерываний, запрограммированный на режим чувствительности по фронту, реагирует на переход группового сигнала прерывания из неактивного состояния в активное. А этого не будет, потому что ядро уже вызвало оба обработчика прерываний и теперь ждет другое прерывание от контроллера.

В этом случае подходящим решением был бы режима чувствительности по уровню, потому что когда ISR Ethernet завершится, и ядро выдаст контроллеру сигнал EOI, контроллер сможет распознать, что сигнал прерывания все еще активен, и прервет ядро заново. Тогда ядро снова прошло бы по всей цепочке ISR, и на этот раз обработкой занялся бы ISR драйвера SCSI.

Выбор режима чувствительности зависит от типа аппаратных средств и стартового кода. Некоторые аппаратные средства поддерживают только либо один, либо другой режим. Аппаратные средства, которые поддерживают оба режима, могут быть запрограммированы на тот или иной режим стартовым кодом. За окончательным ответом обращайтесь к документации по BSP (Board Support Package — пакет поддержки платы), поставляемого с вашей системой.

Написание обработчиков прерываний

Давайте посмотрим, как настроить обработчики прерываний — вызовы, характеристики и кое-какие стратегии реализации.

Подключение обработчиков прерываний

Для подключения к источнику прерывания воспользуйтесь функцией InterruptAttach() или InterruptAttachEvent().

#include 


int InterruptAttachEvent(int intr,

 const struct sigevent *event, unsigned flags);


int InterruptAttach(int intr,

 const struct sigevent* (*handler)(void *area, int id),

 const void *area, int size, unsigned flags);

Параметр intr определяет, к какому прерыванию вы хотите подключить обработчик.

Передаваемые значения определяются стартовым кодом, который перед запуском QNX/Neutrino, среди прочего, инициализирует контроллер обработки прерываний. (В документации по QNX/ Neutrino приводится подробная информация о стартовом коде; см. Справочник по утилитам, главы

startup-*
, например,
startup-p5064
.)

Уже здесь функции InterruptAttach() и InterruptAttachEvent() различаются. Рассмотрим сначала функцию InterruptAttachEvent(), как более простую. Затем вернемся к рассмотрению функции InterruptAttach().

Подключение с помощью функции InterruptAttachEvent()

Функция InterruptAttachEvent() принимает два дополнительных аргумента: event, который является указателем на структуру

struct sigevent
, описывающую генерируемое событие, а также flags. Функция InterruptAttachEvent() сообщает ядру, что при обнаружении прерывания должно быть сгенерировано событие event, после чего данный уровень прерываний должен быть демаскирован. Отметьте, что за то, как интерпретировать событие и какой поток перевести в состояние READY, отвечает ядро, при обнаружении прерывания.

Подключение с помощью функции InterruptAttach()

При использовании функции InterruptAttach() мы определяем другой набор параметров. Параметр handler — это адрес функции, которую надо вызвать. Как видно из прототипа, функция handler() возвращает структуру

struct sigevent
(указывающую на тип события, которое следует сгенерировать) и принимает два параметра. Первый передаваемый параметр — area, тот самый, который передается функции InterruptAttach(). Второй параметр, id, — идентификатор прерывания, его также возвращает InterruptAttach(). Он применяется для идентификации прерывания, а также для маскирования, демаскирования, блокировки и деблокировки прерывания. Четвертый параметр InterruptAttach(), size, указывает размер (в байтах) области данных, которая передается в параметре area. И, наконец, пятый параметр — flags — тот же самый параметр flags, что и у InterruptAttachEvent(); мы скоро к нему вернемся.

Теперь, когда вы подключились к прерыванию

Допустим, что вы уже вызвали функцию InterruptAttachEvent() или InterruptAttach().

Поскольку подключение к прерываниям — не та вещь, которую вы хотели бы позволять кому попало, QNX/Neutrino позволяет делать это только потокам, у которых есть право «привилегированного ввода/вывода» («I/O privity») (см. функцию ThreadCtl() в справочном руководстве по Си-библиотеке QNX/Neutrino). Получить это право могут только потоки, которые выполняются под идентификатором пользователя root или которые установили свой идентификатор пользователя в root при помощи setuid(). Следовательно, реально эти права есть только у root. (Что, кстати, вполне логично — зачем несуперпользовательскому процессу привилегированный ввод/вывод? — прим. ред.)

Ниже приведен фрагмент программы, в котором реализовано подключение ISR к вектору аппаратного прерывания, идентифицируемого константой HW_SERIAL_IRQ:

#include 


int InterruptID;


const struct sigevent* intHandler(void *arg, int id) {

 ...

}


int main (int argc, char **argv) {

 interruptID =

  InterruptAttach(HW_SERIAL_IRQ, intHandler, sevent,

   sizeof(event), 0);

 if (interruptID == -1 {

  fprintf(stderr, "%s: ошибка подключения к IRQ %d\n",

  progname, W_SERIAL_IRQ);

  perror(NULL);

  exit(EXIT_FAILURE);

 }

 ...

 return (EXIT_SUCCESS);

}

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

Отключение обработчика прерывания

Когда вы закончили с обработчиком прерывания, вы можете пожелать уничтожить связь между ним и вектором:

int InterruptDetach(int id);

Я сказал «можете», потому что обрабатывающие прерывания потоки, как правило, используются в серверах, а серверы обычно не завершаются. Это часто ведет к предрассудку, что хорошо организованному серверу никогда не понадобится самостоятельно вызывать InterruptDetach(). К тому же, при смерти потока или процесса ОС автоматически отключит все связанные с ним обработчики прерываний. Таким образом, если программа просто дойдет до конца main(), вызовет exit() или завершится по SIGSEGV, все ее ISR будут автоматически отключены от соответствующих векторов прерываний. (Впрочем, вы, вероятно, пожелаете сделать это несколько изящнее, запретив соответствующему устройству генерацию прерываний. Если же прерывание разделяемое, и его используют другие устройства, то здесь двух вариантов быть не может вообще — вы просто обязаны «убрать за собой», иначе у вас либо больше не будет прерываний (в режиме чувствительности по фронту), либо пойдет постоянный поток запросов на прерывание (в режиме чувствительности по уровню).

Продолжая вышеприведенный пример, если бы мы захотели отключиться от прерывания, то мы использовали бы следующий код:

void terminateInterrupts(void) {

 InterruptDetach(interruptID);

}

Если это последний ISR, связанный с данным вектором прерывания, то ядро автоматически произведет маскирование источника прерывания на уровне контроллера, чтобы таких прерываний больше не возникало.

Параметр flags

Последний параметр, flags, управляет различными дополнительными опциями:

_NTO_INTR_FLAGS_END

Указывает, что данный обработчик должен сработать после всех других обработчиков данного прерывания (если они есть).

_NTO_INTR_FLAGS_PROCESS

Указывает на то, что данный обработчик связан с процессом, а не с потоком. Что из этого вытекает, так это условие автоматического отключения обработчика. Если вы определяете этот флаг, обработчик будет автоматически отключен от источника прерывания при завершении процесса. Если этот флаг не определен, обработчик прерывания будет отключен от источника, когда завершится поток, подключивший его.

_NTO_INTR_FLAGS_TRK_MSK

Указывает, что ядро должно отследить, сколько раз данное прерывание было маскировано. Это приводит к несколько большей загрузке ядра, но это необходимо для корректного демаскирования источника прерываний при завершении потока или процесса.

Обработчик прерывания

Давайте рассмотрим собственно обработчик прерывания. В первом примере применим InterruptAttach(), а затем рассмотрим аналогичный случай, только с применением функции InterruptAttachEvent().

Применение функции InterruptAttach()

В продолжение примера приведем функцию intHandler() — наш обработчик прерывания. Она отвечает за микросхему последовательного порта 8250 (допустим, что она генерирует прерывание HW_SERIAL_IRQ).

/*

 * int1.c

*/

#include 

#include 

#define REG_RX  0

#define REG_II  2

#define REG_LS  5

#define REG_MS  6

#define IIR_MASK 0x07

#define IIR_MSR  0x00

#define IIR_THE  0x02

#define IIR_RX  0x04

#define IIR_LSR  0x06

#define IIR_MASK 0x07


volatile int serial_msr; // Сохраненное значение

             // регистра состояния модема

volatile int serial_rx; // Сохраненное значение

             // регистра приема

volatile int serial_lsr; // Сохраненное значение

              // регистра состояния линии


static int base_reg = 0x2f8;

const struct sigevent* intHandler(void *arg, int id) {

 int iir;

 struct sigevent *event = (struct sigevent*)arg;


 /*

  * Определить (и очистить) источник прерывания

  * чтением регистра идентификации прерывания

 */

 iir = in8(base_reg + REG_II) & IIR_MASK;

 /* Нет прерывания? */

 if (iir & 1) {

  /* Значит, нет и события */

  return (NULL);

 }


 /*

  * Выяснить, что вызвало прерывание, и определить, надо ли

  * потоку что-нибудь с этим делать.

  * (Константы основаны на строении регистра

  * идентификации прерывания 8250.)

 */

 switch (iir) {

 case IIR_MSR:

  serial_msr = in8(base_reg + REG_MS);

  /* Разбудить поток */

  return (event);

  break;

 case IIR_THE:

  /* Ничего не делать */

  break;

 case IIR_RX:

  /* Считать символ */

  serial_rx = in8(base_reg + REG_RX);

  break;

 case IIR_LSR:

  /* Сохранить регистр состояния линии */

  serial_lsr = in8(base_reg + REG_LS);

  break;

 default:

  break;

 }

 /* Никого не беспокоить */

 return (NULL);

}

Первое, что бросается в глаза, — что все переменные, к которым обращается ISR, должны быть объявлены как

volatile
. В с единственным процессором это делается не для блага обработчиков прерываний, а для облегчения жизни потокам, которые могут быть прерваны обработчиком прерывания в любой момент. Конечно, в многопроцессорной ЭВМ обработчики прерываний вполне могли бы выполняться одновременно с кодом потоков, и в таких случаях надо быть предельно осторожными с вещами подобного рода.

С помощью ключевого слова

volatile
мы указываем компилятору не кэшировать значения этих переменных, поскольку они могут быть изменены в любой точке выполнения программы.

Следующее, на что мы обращаем внимание — это прототип самого обработчика прерывания. Он обозначен как

const struct sigevent*
. Это говорит о том, что подпрограмма intHandler() возвращает указатель на
struct sigevent
. Это стандарт для всех подпрограмм обработки прерываний.

Наконец, обратите внимание на то, что решение, передавать или не передавать событие потоку, принимает сам обработчик. Здесь мы генерируем событие только в случае прерывания по изменению регистра состояния модема (MSR) (событие определяется переменной event, которая передается обработчику прерывания в момент его подключения). Во всех других случаях мы игнорируем прерывание (и обновляем кое-какие глобальные переменные); однако, источник прерывания мы очищаем во всех случаях. Это выполняется считыванием порта ввода/вывода с помощью вызова in8().

Применение функции InterruptAttachEvent()

Если бы мы должны были переписать вышеприведенную программу с применением функции InterruptAttachEvent(), это бы выглядело так:

/*

 * Фрагмент int2.c

*/

#include 

#include 

#define HW_SERIAL_IRQ 3

#define REG_RX  0

#define REG_II  2

#define REG_LS  5

#define REG_MS  6

#define IIR_MASK 0x07

#define IIR_MSR  0x00

#define IIR_THE  0x02

#define IIR_RX  0x04

#define IIR_LSR  0x06

#define IIR_MASK 0x07


static int base_reg = 0x2f8;


int main(int arcgc, char **argv) {

 int intId; // Идентификатор прерывания

 int iir; // Регистр идентификации

      // прерывания

 int serial_msr; // Сохраненное значение

          // регистра состояния модема

 int serial_rx; // Сохраненное значение регистра

          // приема

 int serial_lsr; // Сохраненное значение

          // регистра состояния линии

 struct sigevent event;

 // Обычная настройка main()...

 // Настроить событие

 intId = InterruptAttachEvent(HW_SERIAL_IRQ, &event, 0);

 for (;;) {

  // Ждать события от прерывания

  // (можно было использовать MsgReceive)

  InterruptWait(0, NULL);

  /*

  * Определить (и очистить) источник прерывания

  * чтением регистра идентификации прерывания

  */

  iir = in8(base_reg + REG_II) & IIR_MASK;

  // Демаскировать прерывание, чтобы оно

  // могло сработать снова

  InterruptUnmask(HW_SERIAL_IRQ, intId);

  /* Нет прерывания? */

 if (iir & 1) {

  /* Ждать нового */

  continue;

  }

  /*

  * Выяснить, что вызвало прерывание,

  * и надо ли что-то с этим делать

 */

  switch (iir) {

 case IIR_MSR:

  serial_msr = in8(base_reg + REG_MS);

  /*

   * Выполнить какую-нибудь обработку...

  */

  break;

  case IIR_THE:

  /* He делать ничего */

  break;

  case IIR_RX:

  /* Считать символ */

  serial_rx = in8(base_reg + REG_RX);

  break;

  case IIR_LSR:

  /* Запомнить регистр состояния линии */

  serial_lsr = in8(base_reg + REG_LS);

  break;

  }

 }

 /* Сюда мы не доберемся */

 return (0);

}

Обратите внимание, что функция InterruptAttachEvent() возвращает идентификатор прерывания (небольшое целое число). Мы сохранили это значение в переменной intId, чтобы впоследствии смогли с его помощью демаскировать прерывание. После того как мы подключились к прерыванию, мы должны ждать его возникновения. Поскольку в данном случае мы применили функцию InterruptAttachEvent(), при каждом возникновении прерывания мы будем получать соответствуют предварительно созданное событие. Сравните это с предыдущим случаем, где применялась InterruptAttach() — там решение о том, посылать событие или нет, принимал сам обработчик. При использовании функции InterruptAttachEvent() ядро понятия не имеет, было ли аппаратное прерывание «существенным» для нас или нет — оно просто каждый раз генерирует событие, затем маскирует соответствующее прерывание и предоставляет нам самим возможность решать, насколько это важно и что с этим делать.

В примере с InterruptAttach() мы принимали решение путем возврата либо

struct sigevent
, чтобы указать, что что-то должно произойти, либо константы NULL. Обратите внимание на изменения в программе в варианте с InterruptAttachEvent():

• «Работа ISR» теперь выполняется потоком — в функции main().

• Теперь мы должны всегда демаскировать источник прерывания после получения нашего события (потому что ядро маскирует его для нас).

• Если прерывание для нас является несущественным, мы не будем ничего предпринимать и просто пойдем дальше по программному циклу в ожидании другого прерывания.

• Если прерывание существенно для нас, мы непосредственно обрабатываем его (см. блок операторов

case IIR_MSR
).

Где в программе очищать источник прерывания — это зависит от ваших аппаратных средств и от выбранной схемы уведомления. При использовании SIGEV_INTR в сочетании с InterruptWait() ядро не ставит в очередь более одного уведомления; при использовании же SIGEV_PULSE в сочетании с MsgReceive() поставлены в очередь будут все. Если вы используете сигналы (например, в сочетании с SIGEV_SIGNAL), вы сами определяете, ставить их в очередь или нет. Одни аппаратные средства требуют очистки источника прерываний до начала чтения данных, другие — нет, и можно читать данные из них при выставленном сигнале прерывания.

Такой сценарий как ISR, возвращающий SIGEV_THREAD, повергает меня в дикий ужас. Настоятельно рекомендую по возможности избегать этого «приема».

В вышеприведенном примере с программированием последовательного порта мы приняли решение использовать функцию InterruptWait(), которая ставит в очередь только одно событие. Аппаратура последовательного порта может выставить еще одно прерывание сразу после чтения нами регистра идентификации прерываний (IIR), но это нормально, потому что в очередь будет поставлен максимум один SIGEV_INTR. Мы подберем это уведомление на следующей итерации цикла

for
.

Различия между InterruptAttach() и InterruptAttachEvent()

Напрашивается естественный вопрос: «Когда и какую функцию выбирать?»

Наиболее очевидное преимущество InterruptAttachEvent() состоит в том, что применять ее значительно проще, чем InterruptAttach() — поскольку нет никакого ISR, его и отлаживать не надо. Другое преимущество состоит в том, что поскольку в пространстве ядра ничего не выполняется (a ISR делал бы именно так), нет никакой опасности разрушить систему — если вы столкнетесь с ошибкой программирования, то пострадает конкретный процесс, а не система в целом. Однако, применение этой функции будет более или менее эффективным по сравнению с InterruptAttach() в зависимости от того, чего вы пытаетесь достичь. Этот вопрос достаточно сложен, и сводить его к нескольким словам (типа «быстрее» или «лучше») было бы неправильно. Давайте рассмотрим несколько возможных сценариев.

Вот что происходит, когда мы используем InterruptAttach():

Поток управления при использовании InterruptAttach().

Выполняющийся поток («Поток 1») прерывается, и мы переключаемся в ядро. Ядро сохраняет контекст «Потока 1». Затем ядро смотрит, кто ответственен за обработку данного прерывания и решает, что это «ISR». Ядро настраивает контекст для «ISR» и передает ему управление. «ISR» опрашивает аппаратуру и решает возвратить

struct sigevent
. Ядро отмечает возвращаемое событие, выясняет, кто должен его обработать, и переводит их в состояние READY. Это может привести к планированию ядром другого потока, «Потока 2».

Теперь давайте сопоставим это с тем, что будет происходить при использовании InterruptAttachEvent():

Поток управления при использовании InterruptAttachEvent().

В этом случае путь обслуживания прерываний намного короче. Мы выполнили одно переключение контекста от выполнявшегося потока («Поток 1») в ядро. Вместо второго переключения контекста в ISR ядро просто «притворилось», что получило от ISR

struct sigevent
и среагировало на него, запланировав «Поток 2».

Теперь вы думаете: «Великолепно! Забудем про InterruptAttach() и будем использовать простую функцию InterruptAttachEvent()

Это не такая хорошая идея, как кажется на первый взгляд, потому что вы можете и не захотеть просыпаться от каждого прерывания, генерируемого аппаратурой! Вернитесь к примеру, который приведен выше — событие там возвращалось только тогда, когда изменялся регистр состояния модема, а не по приему символа, изменению регистра состояния линии или опустошению буфера передачи.

В этом случае, особенно если бы последовательный порт принимал символы (которые вы хотели бы проигнорировать), вы бы потратили много времени впустую на перепланирование своего потока — и только ради того, чтобы он проанализировал состояние последовательного порта и принял решение, что ничего делать не надо. В данном случае все бы выглядело примерно так:

Поток управления при использовании InterruptAttachEvent() с излишним перепланированием.

Происходящее по сути заключается в том, что вы вызываете переключение контекста для перехода к «Потоку 2», он опрашивает аппаратуру и понимает, что делать ничего не требуется, и это влечет за собой еще одно лишнее переключение контекста обратно в «Поток 1».

Вот что произошло бы, если бы вы применили функцию InterruptAttach(), но не пожелали планировать другой поток (т.е. просто вернулись обратно):

Поток управления при использовании InterruptAttach() без перепланирования потоков.

Ядро знает, что выполнялся «Поток 1», и что ISR не сказал ему что-либо сделать, поэтому после прерывания оно может смело вернуть управление «Потоку 1».

Для справки: вот что делает функция InterruptAttachEvent() (это не реальный исходный текст, поскольку функция InterruptAttachEvent() в действительности связывает с ядром структуру данных — она не реализована как отдельная вызываемая функция!):

// «Внутренний» обработчик

static const struct sigevent*

internalHandler(void *arg, int id) {

 struct sigevent *event = arg;

 InterruptMask(intr, id);

 return (arg);

}


int InterruptAttachEvent(int intr,

const struct sigevent *event, unsigned flags) {

 static struct sigevent static_event;

 memcpy(&static_event, event, sizeof(static_event));

 return

  (InterruptAttach(intr, internalHandler, &static_event,

   sizeof(*event), flags));

}

Что выбрать?

Так какую функцию применять? От редко возникающих прерываний почти всегда можно отмахнуться применением InterruptAttachEvent(). Поскольку прерывания будут происходить редко, даже лишние перепланирования потоков значительного воздействия на общую производительность системы не окажут. Единственный момент, когда это проявится — это если на данном прерывании будут «сидеть» еще и другие устройства; в этом случае, поскольку функция InterruptAttachEvent() маскирует источник прерывания, то это прерывание останется заблокированным до тех пор, пока источник не будет демаскирован обратно. Если при этом первое устройство требует много времени на обслуживание, остальным придется все это время ждать демаскирования. По большому счету, это проблема аппаратной организации системы — не следует размещать медленные устройства на одной линии прерывания с быстрыми.

При выборе функции для более часто возникающих прерываний требуется учесть множество факторов:

• Ненужные прерывания — если их число будет существенным, лучше применять InterruptAttach() и отфильтровывать их прямо в ISR, Возьмем, например, тот же случай с последовательным устройством. Поток может выдать команду: «Дай мне 64 байта». Если ISR запрограммирован с учетом того, что пока не будут приняты все 64 байта, ничего полезного не произойдет, все возникающие в процессе приема прерывания будут отфильтрованы. ISR возвратит событие только после окончания приема всех 64 байт.

• Время реакции — если ваши аппаратные средства чувствительны к интервалу времени от момента выставления запроса на прерывание до отработки ISR, вам следует использовать InterruptAttach(), чтобы свести это время к минимуму. Это сработает, потому что диспетчеризация ISR в ядре выполняется очень быстро.

• Буферизация — если ваша аппаратура имеет встроенные средства буферизации, вы можете обойтись функцией InterruptAttachEvent() и очередью из единственного события, как в случае с комбинацией SIGEV_INTR и InterruptWait(). Этот метод позволяет прерываниям возникать с такой частотой, как им захочется» при этом позволяя вашему потоку выбирать значения из буфера с такой скоростью, с какой он сможет. Поскольку данные буферизуются на аппаратном уровне, никаких проблем со временем реакции на прерывание не будет.

Функции, которые может вызывать ISR

Следующий вопрос, за который следует взяться, — это список функций, которые может вызывать ISR.

Небольшое отступление. Исторически, причина основных затруднений при написании обработчиков прерываний заключалась (и в большинстве других операционных систем до сих пор заключается) в том, что ISR работают в особом окружении.

Одна из конкретных причин, усложняющих написание ISR, состоит в том, что с точки зрения ядра ISR на самом деле не является «полноправным» потоком. С позиции ядра это, если хотите, такой таинственный «аппаратный» поток. Это означает, что ISR не имеет права делать никаких манипуляций «на уровне потока» — таких как, например, обмен сообщениями, синхронизация, системные вызовы, дисковый ввод/вывод, и т.д.

Не усложняет ли это написание ISR? Конечно. И поэтому решение заключается в том, чтобы в самом теле обработчика выполнять минимум работы, а все остальное делать уже на уровне потока, где есть доступ ко всем сервисам.

Ваши цели при написании ISR должны заключаться в следующем:

• считать переменчивую (в оригинале было «transient» — прим. ред.) информацию;

• очистить источник прерывания;

• возможно, запланировать поток, который сделает реальную работу.

Такая «архитектура « держится на том, что QNX/Neutrino обеспечивает очень быстрые времена переключения контекста. Вы знаете, что сможете быстро переключиться в ваш обработчик для выполнения работы, критичной по времени. Вы также знаете, что когда обработчик возвратит событие для запуска потока, то поток тоже активизируется очень быстро. И именно эта философия «ничего не делайте в теле ISR» делает обработчики прерываний в QNX/Neutrino столь простыми!

Итак, какие же вызовы можно использовать в теле ISR? Вот официальный список:

• функции семейства atomic_*() (например, atomic_set());

• функции семейства mem*() (типа memcpy());

• большинство функций семейства str*() (типа strcmp()). Остерегайтесь, однако, потому что не все эти функции являются безопасными — например, strdup() вызывает malloc(), в которой используется мутекс, а это запрещено.

Вообще, что касательно строковых функций, перед их использованием надо индивидуально смотреть их описание в руководстве по Си-библиотеке;

InterruptMask();

InterruptUnmask();

InterruptLock();

InterruptUnlock();

InterruptDisable();

InterruptEnable();

in*() и out*().

Основное эмпирическое правило формулируется примерно так: «Не используйте ничего, что требует большого объема стека или больших затрат времени, и не используйте ничего, что делает системные вызовы». Требование по стековому пространству проистекает из того факта, что ISR имеют очень ограниченный объем стека.

Список функций, безопасных для применения в ISR, имеет реальный смысл — например, если вам потребуется скопировать область памяти, хорошим выбором будет применение функций типа mem*() и str*(). Скорее всего, вам потребуется читать регистры аппаратных средств (например, чтобы сохранить какие-либо значения или очистить источник прерывания), тогда вам пригодятся функции ввода/вывода из семейств in*() и out*().

А как насчет ошарашивающего выбора функций семейства Interrupt*()? Давайте рассмотрим их попарно.

InterruptMask() и InterruptUnmask()

Эти функции ответственны за маскирование источника прерывания на уровне контроллера; это предохраняет прерывания от передачи процессору. Обычно эти функции применяются, когда вы хотите доделать работу в потоке, но не можете очистить источник прерывании непосредственно в теле ISR. В этом случае ISR должен вызвать InterruptMask(), а поток, после завершения работы, — InterruptUnmask().

Имейте в виду, что число вызовов InterruptUnmask() должно соответствовать числу вызовов InterruptMask() — чтобы прерывание продолжало работать, вы обязаны демаскировать его ровно столько раз, сколько раз оно было маскировано.

Заметьте, между прочим, что функция InterruptAttachEvent() выполняет InterruptMask() автоматически (в ядре), поэтому ваш обрабатывающий прерывание поток должен вызывать InterruptUnmask().

InterruptLock() и InterruptUnlock()

Эти функции используются для блокировки (InterruptLock()) и деблокировки (InterruptUnlock()) прерываний в одно- или многопроцессорной системе. Вам может понадобиться заблокировать прерывания, например, чтобы защитить поток от ISR (или, дополнительно, в SMP-системе — защитить ISR от потока). Когда вы сделаете нужные манипуляции с критическими данными, вы сможете деблокировать прерывания обратно. Отметьте, что данные функции рекомендованы к применению вместо известных вам функций InterruptDisable() и InterruptEnable(), потому что корректно работают в SMP-системах. По сравнению со «старыми» функциями, проверка на многопроцессорность вносит дополнительные издержки, но в однопроцессорной системе ими можно пренебречь, поэтому я рекомендую вам всегда использовать InterruptLock() и InterruptUnlock().

InterruptDisable() и InterruptEnable()

Не используйте эти функции в новых проектах. Исторически, эти функции применялись для вызова инструкций

cli
и
sti
в процессорах серии x86, когда QNX/Neutrino еще не была многоплатформенной ОС.

С тех пор функции были модернизированы для работы со всеми типами процессоров, но чтобы не огорчать SMP-системы, используйте лучше функции InterruptLock() и InterruptUnlock().

Еще одна вещь, которую не вредно будет повторить, заключается в том, что в SMP-системе возможно одновременное выполнение ISR и другого потока.

Резюме

При работе с прерываниями принимайте во внимание следующие положения:

• Не оставайтесь в обработчике прерывания слишком долго — выполняйте в нем минимальный объем работы. Это поможет сократить время реакции на прерывание и упростить отладку.

• Применяйте функцию InterruptAttach() только тогда, когда нужно обращаться к аппаратным средствам непосредственно после прерывания, в противном случае избегайте ее.

• Применяйте функцию InterruptAttachEvent() во всех других случаях. Ядро запланирует поток (на основе события, которое вы передадите) для обработки возникшего прерывания.

• Защищайте переменные, используемые как в обработчиках прерываний (при использовании InterruptAttach()), так и в потоках, путем вызова InterruptLock() и InterruptUnlock().

• Объявляйте переменные, используемые в качестве посредников между потоками и обработчиками прерывании, как

volatile
, чтобы компилятор не кэшировал их «просроченные» значения, уже измененные обработчиком прерывания.

Загрузка...