Обработка подобного рода выполняется обработчиком прерываний (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) независимо ни от чего — оно просто не знает, какое конкретное устройство требовало обслуживания, поэтому всегда отрабатывает всю цепочку.