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

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

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

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

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

/*

 * int1.c

*/

#include <stdio.h>

#include <sys/neutrino.h>

#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 <stdio.h>

#include <sys/neutrino.h>

#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(). Этот метод позволяет прерываниям возникать с такой частотой, как им захочется» при этом позволяя вашему потоку выбирать значения из буфера с такой скоростью, с какой он сможет. Поскольку данные буферизуются на аппаратном уровне, никаких проблем со временем реакции на прерывание не будет.