21911
Комментарий:
|
29078
|
Удаления помечены так. | Добавления помечены так. |
Строка 2: | Строка 2: |
Строка 12: | Строка 11: |
* про каждое из них надо знать, что оно случиолось | * про каждое из них надо знать, что оно случилось |
Строка 22: | Строка 21: |
Регистр '''Status''': |
. Регистр '''Status''': |
Строка 29: | Строка 27: |
* Interrupt mask - Mаска прерываний. Бит равен 1, если соответствующее прерывание разрешено. * При возникновении прерывания будет установлен соответствующий бит регистра Cause (если прерывание маскировано — не произойдёт ничего) |
* Interrupt mask - Mаска прерываний. Бит равен 1, если соответствующее прерывание разрешено. * При возникновении прерывания будет установлен соответствующий бит регистра Cause (если прерывание маскировано — не произойдёт ничего) |
Строка 33: | Строка 32: |
* Interrupt enable - глобальное разрешение обработчика прерываний (0 - отключить)<<BR>> | * Interrupt enable - глобальное разрешение обработчика прерываний (0 - отключить) |
Строка 37: | Строка 36: |
Строка 39: | Строка 37: |
Строка 45: | Строка 42: |
1. Br - 1 если исключение произошло на инструкции в слоте задержки перехода. | 1. Br - 1 если исключение произошло на инструкции в слоте задержки перехода. |
Строка 47: | Строка 45: |
1. Exception code - код исключения. Прерывания, в отличие от исключений, могут возникать в произвольное время (например, прерывание ввода зависит от того, когда человек нажал на кнопку). Прерывания в Mars обрабатываются тем же кодом, что и исключения — специальным обработчиком по адресу 0x8000180. |
1. Exception code - код исключения. Прерывания, в отличие от исключений, могут возникать в произвольное время (например, прерывание ввода зависит от того, когда человек нажал на кнопку). Прерывания в Mars обрабатываются тем же кодом, что и исключения — специальным обработчиком по адресу 0x8000180. |
Строка 52: | Строка 47: |
* Нельзя пользоваться стеком (а вдруг он испорчен, или прерывание произошло во время его изменения?) | 1. Нельзя пользоваться стеком (а вдруг он испорчен, или прерывание произошло во время его изменения?) |
Строка 55: | Строка 50: |
* Нужно различать исключения (поле Exception code регистра Cause ненулевое) и прерывания (поле EXC нулевое) | 1. Нужно различать исключения (поле Exception code регистра Cause ненулевое) и прерывания (поле EXC нулевое) |
Строка 58: | Строка 53: |
* В Mars нужно как можно быстрее запретить обрабатывать исключения, чтобы исключение не случилось в ядре<<BR>> | 1. В Mars нужно как можно быстрее запретить обрабатывать исключения, чтобы исключение не случилось в ядре |
Строка 61: | Строка 56: |
* Перед выходом из обработчика | 1. Перед выходом из обработчика |
Строка 65: | Строка 60: |
Строка 67: | Строка 61: |
Строка 77: | Строка 70: |
Строка 79: | Строка 71: |
* Сохранение всех регистров * Вычисление типа исключений (0 — прерывание) |
1. Сохранение всех регистров 1. Вычисление типа исключений (0 — прерывание) |
Строка 82: | Строка 74: |
* Обработчик прерываний: | 1. Обработчик прерываний: |
Строка 85: | Строка 77: |
* Обработчик исключения | 1. Обработчик исключения |
Строка 88: | Строка 80: |
* Восстановление всех регистров * Разрешение вызова обработчика * '''eret''' '''Замечание''': как и во время обработки прерывания, во время системного вызова прерывания или вообще запрещены, или накапливаются (делаются «Pending») без вызова обработчика. Поэтому задача обработчика — как можно быстрее принять решение, что делать с прерыванием и вернуть управление пользовательской программе. |
1. Восстановление всех регистров 1. Разрешение вызова обработчика 1. '''eret''' '''Замечание''': как и во время обработки прерывания, во время системного вызова прерывания или вообще запрещены, или накапливаются (делаются «Pending») без вызова обработчика. Поэтому задача обработчика — как можно быстрее принять решение, что делать с прерыванием и вернуть управление пользовательской программе. |
Строка 95: | Строка 84: |
* Вся логика обработки данных остаётся в программе пользователя * При соблюдении некоторой дисциплины программирования можно вообще запретить пользовательской программе обращаться к внешним устройствам, оставив эти операции ядру, обрабатывающему прерывани |
1. Вся логика обработки данных остаётся в программе пользователя 1. При соблюдении некоторой дисциплины программирования можно вообще запретить пользовательской программе обращаться к внешним устройствам, оставив эти операции ядру, обрабатывающему прерывани |
Строка 104: | Строка 93: |
||0xffff0000||RcC||Управляющий регистр ввода||RW||0 бит — готовность, 1 бит — прерывание|| ||0xffff0004||RcD||Регистр данных ввода||R||введённый байт|| ||0xffff0008||TxC||Управляющий регистр вывода||RW||0 бит — готовность, 1 бит — прерывание|| ||0xffff000c||TxD||Регистр данных вывода||W||необязательные координаты курсора, байт для вывода|| Операции ввода и вывода в консоли возможно только если бит готовности равен 1. Если бит готовности нулевой в управляющем регистре ввода, значит, клавиша ещё не нажата, а если в управляющем регистре вывода — символ всё ещё выводится, следующий выводить нельзя (ну медленное устройство, в жизни так сплошь и рядом!). Как обычно, устройство заработает только после нажатия кнопки «Connect to MIPS». Простой пример чтения с клавиатуры при помощи поллинга. Удобно рассматривать с низкой скоростью работы эмулятора (3-5 тактов в секунду). {{{ |
||0xffff0000 ||RcC ||Управляющий регистр ввода ||RW ||0 бит — готовность, 1 бит — прерывание || ||0xffff0004 ||RcD ||Регистр данных ввода ||R ||введённый байт || ||0xffff0008 ||TxC ||Управляющий регистр вывода ||RW ||0 бит — готовность, 1 бит — прерывание || ||0xffff000c ||TxD ||Регистр данных вывода ||W ||необязательные координаты курсора, байт для вывода || Операции ввода и вывода в консоли возможно только если бит готовности равен 1. Если бит готовности нулевой в управляющем регистре ввода, значит, клавиша ещё не нажата, а если в управляющем регистре вывода — символ всё ещё выводится, следующий выводить нельзя (ну медленное устройство, в жизни так сплошь и рядом!). Как обычно, устройство заработает только после нажатия кнопки «Connect to MIPS». Простой пример чтения с клавиатуры при помощи поллинга. Удобно рассматривать с низкой скоростью работы эмулятора (3-5 тактов в секунду). {{{ |
Строка 126: | Строка 112: |
Чуть более сложный пример с выводом, в котором видна проблема переполнения. Вывод начинает работать (точнее, бит готовности выставляется в первый раз) только после нажатия «Reset» (подозреваю, что это — тоже пример из «реальной жизни»: при включении питания многие устройства находятся в неопределённом состоянии и требуют инициализации). {{{ |
Чуть более сложный пример с выводом, в котором видна проблема переполнения. Вывод начинает работать (точнее, бит готовности выставляется в первый раз) только после нажатия «Reset» (подозреваю, что это — тоже пример из «реальной жизни»: при включении питания многие устройства находятся в неопределённом состоянии и требуют инициализации). {{{ |
Строка 142: | Строка 128: |
Выставляя ползунок «Delay Length» в большое значение, мы заставляем консоль долго не давать готовности по выводы (в течение, скажем, 20 инструкций). Пока программа находится в половинке вывода (цикл ''loopi:''), она не успевает за вводом. '''Задание''': пронаблюдать, что происходит с регистрами ввода, когда пользователь много нажимает на клавиатуре, а программа не успевает считать. '''TODO — '''нижеследующее в отдельную страницу Главное свойство консоли она может инициировать прерывания в момент готовности ввода или вывода. Устанавливая в 1 первый бит в регистре RcC, мы разрешаем консоли возбуждать прерывание всякий раз, как пользователь нажал на клавишу. Устанавливая в 1 первый бит регистра TxC, мы разрешаем прерывание типа «окончание вывода». И в том, и в другом случае прерывание возникает одновременно с появлением бита готовности (нулевого) в соответствующем регистре. Таким образом, вместо постоянного опроса регистра мы получаем однократный вызов обработчика в подходящее время. Рассмотрим пример ''очень грязного'' обработчика прерывания от клавиатуры, который ничего не сохраняет, не проверяет причину события и номер прерывания. Зато по этому коду хорошо видна асинхронная природа работы прерывания. Рекомендуется выставить ползунок Mars «Run speed» в низкое значение (например, 5 раз в секунду). {{{ |
Выставляя ползунок «Delay Length» в большое значение, мы заставляем консоль долго не давать готовности по выводы (в течение, скажем, 20 инструкций). Пока программа находится в половинке вывода (цикл ''loopi:''), она не успевает за вводом. '''Задание''': пронаблюдать, что происходит с регистрами ввода, когда пользователь много нажимает на клавиатуре, а программа не успевает считать. Главное свойство консоли она может инициировать прерывания в момент готовности ввода или вывода. Устанавливая в 1 первый бит в регистре RcC, мы разрешаем консоли возбуждать прерывание всякий раз, как пользователь нажал на клавишу. Устанавливая в 1 первый бит регистра TxC, мы разрешаем прерывание типа «окончание вывода». И в том, и в другом случае прерывание возникает одновременно с появлением бита готовности (нулевого) в соответствующем регистре. Таким образом, вместо постоянного опроса регистра мы получаем однократный вызов обработчика в подходящее время. Рассмотрим пример ''очень грязного'' обработчика прерывания от клавиатуры, который ничего не сохраняет, не проверяет причину события и номер прерывания. Зато по этому коду хорошо видна асинхронная природа работы прерывания. Рекомендуется выставить ползунок Mars «Run speed» в низкое значение (например, 5 раз в секунду). {{{ |
Строка 171: | Строка 151: |
В примере ниже «полезные вычисления» делает подпрограмма '''sleep''' (на самом деле ничего полезного), время от времени проверяя содержимое ячейки 0 в глобальной области. Это лучше, чем модифицировать регистр или метку, определяемую пользовательской программой. Обработчик клавиатурного прерывания (для простоты — не проверяя, клавиатурное ли оно) записывает в эту ячейку код нажатой клавиши. '''TODO проверить таки источник прерывания и нарисовать что-то \|/-''' {{{ |
В примере ниже «полезные вычисления» делает подпрограмма '''sleep''' (на самом деле ничего полезного), время от времени проверяя содержимое ячейки 0 в глобальной области. Это лучше, чем модифицировать регистр или метку, определяемую пользовательской программой. Обработчик клавиатурного прерывания (для простоты — не проверяя, клавиатурное ли оно) записывает в эту ячейку код нажатой клавиши. {{{ |
Строка 242: | Строка 222: |
Символами "!!" отмечены инструкции, при выполнении которых в MARS может возникнуть фатальный повторный вход в обработчик события. | Символами "!!" отмечены инструкции, при выполнении которых в MARS может возникнуть фатальный повторный вход в обработчик события. === Кольцевой буфер === В разделе «Прерывания» мы уже встретились с основным свойством обработчика прерываний: обрабатывать надо ''быстро''. Поэтому вся ''логика'' обработки должна находится в пользовательской программе, а обработчику, скажем, клавиатурного прерывания остаётся только перекладывать из регистра ввода ''куда-нибудь''. С другой стороны, приведённый там пример обработчика мало чем отличается от традиционного поллинга — та же проверка, был ли ввод или не было, только вместо регистра готовности опрашиваем ячейку памяти. Если делать это слишком редко, легко пропустить очередной ввод, а если слишком часто — потратить время на опрос вместо полезных действий. . Итак, главная проблема — синхронность побайтового ввода и обработки прерываний. И задача в том, чтобы обеспечить ''рассинхронизацию'' ввода и вывода: прерывание обрабатывается, когда оно происходит, а программа вводит свои байты, когда может. Разумеется, для этого надо хранить не один обработанный, но ещё не запрошенный программой байт, а несколько. Эффективная структура, решающая такую задачу — '''кольцевой буфер.''' . '''Кольцевой буфер '''— массив (реже — список) данных, предназначенный для временного хранения асинхронно поступающих и асинхронно отбывающих данных. {{attachment:RB0.png}} * Буфер имеет фиксированный размер * «Поверхность» буфера (HI) — адрес первой свободной ячейки буфера * Когда поступят новые данные, они будут записаны в эту ячейку, а HI передвинется на следующую * «Дно» буфера (LO) — адрес последней занятой ячейки буфера * Когда понадобится забрать очередной элемент данных, его возьмут из этой ячейки, а LO передвинется на следующую Размер буфера не является ограничением: когда HI подберётся к концу, в начале уже образуются свободные места и HI при очередном сдвиге начнёт указывать в начало {{attachment:RB1.png}} Аналогично произойдёт и с LO. Вполне возможна ситуация, когда данных больше не прибыло, а программа требует ещё ввода. Эта ситуация называется ''исчерпанием буфера'' и распознаётся по тому, что HI и LO содержат один и тот же адрес: {{attachment:RB2.png}} Вариантов обработки исчерпания два: 1. Заданным образом уведомить программу, что данных ещё нет (например, вернуть специальное служебное значение) * Это называется ''неблокирующим вводом'' 1. Задержать операцию снятия данных до тех пор, пока они не появятся * Это называется ''блокирующим'' вводом 1. Продолжить скакать по уже введённым когда-то данным, как если бы исчерпания не было * это называется ''грубой ошибкой алгоритма!'' Заметим, что в момент создания кольцевой буфер находится в состоянии исчерпания (HI и LO указывают на нулевой элемент) Другая ситуация — ''переполнение буфера'': данные всё прибывают, а забирать их никто не хочет. Переполнение кольцевого буфера распознаётся по тому, что LO содержит адрес ячейки, следующей за HI (в кольце): {{attachment:RB3.png}} Вариантов обработки переполнения 2 (два других — плохие, негодные): 1. Продолжать вводить, но не увеличивать HI. При этом все прибывшие после переполнения данные, кроме последнего элемента, исчезают. * Если источник данных позволяет, можно ''заранее ''уведомить его о переполнении — может, он приостановит передачу на время. * Правда, для этого надо усложнить алгоритм работы, введя «верхний урез» поверхности буфера, при котором устройство уведомляется о приближающемся переполнении, и «нижний урез», при котором оно уведомляется о том, что переполнение больше не грозит. 1. Выбрасывать все вновь прибывающие данные, пока для них нет места в буфере. * См. примечания выше 1. Продолжать как ни в чём ни бывало вводить и увеличивать HI. * При этом внезапно™ возникает ситуация ''исчерпания ''и пропадает ''всё содержимое'' буфера 1. Задерживать ввод до тех пор, пока свободное место в буфере не появится. * Если обрабатывать таким образом прерывание ввода, то возникнет т. н. ''взаимоблокировка ''(deadlock): пока мы не вышли из прерывания, программа не может забрать данные из буфера, а пока программа не забрала данные из буфера, мы сидим в прерывании и ждём * ⇒ ''никаких ожиданий в обработчике прерываний!'' Нет, значит — нет |
Прерывания
- Прерывание — сигнал, сообщающий процессору о наступлении какого-либо внешнего события.
- асинхронны: могут произойти в любое время в любом месте выполнения программы (ср. исключения: могут возникнуть только при выполнении конкретных инструкций)
- позволяют освободить cpu от активного ожидания: программа спокойно вычисляет (не отвлекаясь на опрос готовности устройства), а когда устройство, наконец, себя проявит, результаты его активности быстро обрабатываются
обрабатываются так же, как и другие события, — обработчиком прерываний (как правило, частью ядра ОС)
- Проблемы возникающие при обработке прерываний:
- распознавание природы прерывания — что за устройство и что с ним случилось
- где-то должна появляться об этом информация (аппаратно!)
- прерывания нужно быстро обработать
- во одно и то же время может случиться несколько прерываний
- про каждое из них надо знать, что оно случилось
- распознавание природы прерывания — что за устройство и что с ним случилось
- Маскирование прерываний.
- Прерывания, в зависимости от возможности запрета, делятся на:
- маскируемые — прерывания, которые можно запрещать установкой соответствующих битов в регистре маскирования прерываний;
- немаскируемые — обрабатываются всегда, независимо от запретов на другие прерывания.
- Прерывания, в зависимости от возможности запрета, делятся на:
- Приоритеты обслуживания прерываний: если выясняется, что произошло несколько, какое обрабатывать первым? А если за время обработки произойдёт ещё несколько?
Обработчик прерываний
Прерывания, в отличие от исключений, могут возникать в произвольное время (например, прерывание ввода зависит от того, когда человек нажал на кнопку). Прерывания в Mars обрабатываются тем же кодом, что и исключения — специальным обработчиком по адресу 0x8000180. Обработка прерываний управляется сопроцессором 0 и его регистрами
Регистр Status:
bits |
31-16 |
15-8 |
7-5 |
4 |
3,2 |
1 |
0 |
target |
unused |
Int. mask |
unused |
K/U |
unused |
Exception level |
Int enable |
- Interrupt mask - Mаска прерываний. Бит равен 1, если соответствующее прерывание разрешено.
- При возникновении прерывания будет установлен соответствующий бит регистра Cause (если прерывание маскировано — не произойдёт ничего)
- Kernel/User - режим ядра/режим пользователя; не поддерживается Mars; всегда 1.
- Exception level - устанавливается автоматически при исключении; предотвращает повторный вход.
- Interrupt enable - глобальное разрешение обработчика прерываний (0 - отключить)
Выполнение кода и переход на обработчик выполняться не будет. в Mars обязательно отключать в начале обработчика прерываний
- Если за время, пока прерывания отключены, внешние устройства проявляют активность, и соответствующие прерывания не маскированы, после включения прерываний произойдёт вход в обработчик, и поле Pending interrupts регистра Cause будет содержать все соответствующие биты
- Так в «настоящих» MIPS. В Mars выставлять более одного бита умеет только устройство «консоль»
Регистр Cause:
31 |
30-16 |
15-8 |
7 |
6-2 |
1-0 |
Br |
unused |
Pending interrupts |
unused |
Exception code |
unused |
- Br - 1 если исключение произошло на инструкции в слоте задержки перехода.
- Pending interrupts - ожидающие (ещё не обработанные) прерывания.
- Exception code - код исключения. Прерывания, в отличие от исключений, могут возникать в произвольное время (например, прерывание ввода зависит от того, когда человек нажал на кнопку). Прерывания в Mars обрабатываются тем же кодом, что и исключения — специальным обработчиком по адресу 0x8000180.
Нужно сохранять все используемые регистры, кроме $k0 и $k1 (все, включая $at)
- Нельзя пользоваться стеком (а вдруг он испорчен, или прерывание произошло во время его изменения?)
Можно предусмотреть отдельный стек ядра (скажем, от 0xfffeeffc вниз) и пользоваться им (тогда $sp тоже сохраняется)
- Нужно различать исключения (поле Exception code регистра Cause ненулевое) и прерывания (поле EXC нулевое)
возврат из исключения по eret требует прибавить 4 к значению EPC (ошибочную инструкцию выполнять повторно обычно не надо)
возврат из прерывания по eret не требует увеличения EPC (инструкция по этому адресу ещё не выполнена)
- В Mars нужно как можно быстрее запретить обрабатывать исключения, чтобы исключение не случилось в ядре
Значит, в обработчике надо проводить как можно меньше времени — не все устройства Mars умеют выставлять бит в регистре, когда обработчик запрещён (бит IE регистра Status равен 0)
Если в поле Pending interrupts приехало несколько битов, значит, произошло несколько прерываний, и все надо обработать (или игнорировать)
- Перед выходом из обработчика
- Очистить регистр Cause (биты прерываний и тип исключения)
- Разрешить обработку прерываний
(бит EXL в Mars очистит инструкция eret)
mfc0 $a0 $12 # read from the status register ori $a0 0xff11 # enable all interrupts mtc0 $a0 $12 # write back to the status register li $a0 2 # enable keyboard interrupt sw $a0 0xffff0000
Сам обработчик событий по адресу 0x800000180, таким образом, обычно состоит из следующих частей:- Запрет вызова обработчика (бит IE)
- Сохранение всех регистров
- Вычисление типа исключений (0 — прерывание)
- Переход на обработчик соответствующего исключения или на обработчик прерываний
- Обработчик прерываний:
- Выяснение источника/ов прерывания (биты Pending)
- Обработка всех случившихся прерываний (порядок определяется программно)
- Обработчик исключения
Обработка исключения
- Вычисление нового EPC
- Восстановление всех регистров
- Разрешение вызова обработчика
eret Замечание: как и во время обработки прерывания, во время системного вызова прерывания или вообще запрещены, или накапливаются (делаются «Pending») без вызова обработчика. Поэтому задача обработчика — как можно быстрее принять решение, что делать с прерыванием и вернуть управление пользовательской программе.
- В обработчике выполняются только действия, необходимые для получения данных по прерыванию (чтение регистров, управление устройствами и т. п.)
- Вся логика обработки данных остаётся в программе пользователя
- При соблюдении некоторой дисциплины программирования можно вообще запретить пользовательской программе обращаться к внешним устройствам, оставив эти операции ядру, обрабатывающему прерывани
Пример: консоль Mars
Консоль Mars («Keyboard and Display MMIO Simulator») — воображаемое устройство, осуществляющее побайтовый ввод и вывод. Вернее окошко — «дисплей», куда выводятся байты, а нижнее — «клавиатура» (для удобства набираемый на клавиатуре текст отображается в этом окошке).
Консоль имеет следующие регистры ввода-вывода
0xffff0000 |
RcC |
Управляющий регистр ввода |
RW |
0 бит — готовность, 1 бит — прерывание |
0xffff0004 |
RcD |
Регистр данных ввода |
R |
введённый байт |
0xffff0008 |
TxC |
Управляющий регистр вывода |
RW |
0 бит — готовность, 1 бит — прерывание |
0xffff000c |
TxD |
Регистр данных вывода |
W |
необязательные координаты курсора, байт для вывода |
Операции ввода и вывода в консоли возможно только если бит готовности равен 1. Если бит готовности нулевой в управляющем регистре ввода, значит, клавиша ещё не нажата, а если в управляющем регистре вывода — символ всё ещё выводится, следующий выводить нельзя (ну медленное устройство, в жизни так сплошь и рядом!). Как обычно, устройство заработает только после нажатия кнопки «Connect to MIPS». Простой пример чтения с клавиатуры при помощи поллинга. Удобно рассматривать с низкой скоростью работы эмулятора (3-5 тактов в секунду).
loop: lb $t0 0xffff0000 # готовность ввода andi $t0 $t0 1 # есть? beqz $t0 loop # нет — снова lb $a0 0xffff0004 # считаем символ li $v0 11 # выведем его syscall b loop
Чуть более сложный пример с выводом, в котором видна проблема переполнения. Вывод начинает работать (точнее, бит готовности выставляется в первый раз) только после нажатия «Reset» (подозреваю, что это — тоже пример из «реальной жизни»: при включении питания многие устройства находятся в неопределённом состоянии и требуют инициализации).
li $t1 0 loop: beqz $t1 noout # выводить не надо loopi: lb $t0 0xffff0008 # готовность вывода andi $t0 $t0 1 # есть? beqz $t0 loopi # а надо! идём обратно sb $t1 0xffff000c # запишем байт li $t1 0 # обнулим данные noout: lb $t0 0xffff0000 # готовность ввода andi $t0 $t0 1 # есть? beqz $t0 loop # нет — снова lb $t1 0xffff0004 # считаем символ b loop
Выставляя ползунок «Delay Length» в большое значение, мы заставляем консоль долго не давать готовности по выводы (в течение, скажем, 20 инструкций). Пока программа находится в половинке вывода (цикл loopi:), она не успевает за вводом.
Задание: пронаблюдать, что происходит с регистрами ввода, когда пользователь много нажимает на клавиатуре, а программа не успевает считать.
Главное свойство консоли она может инициировать прерывания в момент готовности ввода или вывода. Устанавливая в 1 первый бит в регистре RcC, мы разрешаем консоли возбуждать прерывание всякий раз, как пользователь нажал на клавишу. Устанавливая в 1 первый бит регистра TxC, мы разрешаем прерывание типа «окончание вывода». И в том, и в другом случае прерывание возникает одновременно с появлением бита готовности (нулевого) в соответствующем регистре. Таким образом, вместо постоянного опроса регистра мы получаем однократный вызов обработчика в подходящее время. Рассмотрим пример очень грязного обработчика прерывания от клавиатуры, который ничего не сохраняет, не проверяет причину события и номер прерывания. Зато по этому коду хорошо видна асинхронная природа работы прерывания. Рекомендуется выставить ползунок Mars «Run speed» в низкое значение (например, 5 раз в секунду).
li $a0 2 # разрешим прерывания от клавиатуры sw $a0 0xffff0000 li $a0 0 loop: beqz $a0 loop # вечный цикл beq $a0 0x1b done # ESC — конец li $v0 11 # выведем символ syscall li $a0 0 # затрём $a0 j loop done: li $v0 10 syscall .ktext 0x80000180 # очень грязный код обработчика lw $a0 0xffff0004 # считаем символ eret
В примере ниже «полезные вычисления» делает подпрограмма sleep (на самом деле ничего полезного), время от времени проверяя содержимое ячейки 0 в глобальной области. Это лучше, чем модифицировать регистр или метку, определяемую пользовательской программой. Обработчик клавиатурного прерывания (для простоты — не проверяя, клавиатурное ли оно) записывает в эту ячейку код нажатой клавиши.
.text .globl main main: mfc0 $a0 $12 # read from the status register ori $a0 0xff11 # enable all interrupts mtc0 $a0 $12 # write back to the status register li $a0 2 # enable keyboard interrupt sw $a0 0xffff0000 here: jal sleep lw $a0 ($gp) # print key stored in ($gp) beq $a0 0x1b done # ESC terminates li $v0 1 syscall j here done: li $v0 10 syscall .eqv ZZZ 100000 sleep: li $t0 ZZZ # Do nothing tormo0: subi $t0 $t0 1 blez $t0 tormo1 j tormo0 tormo1: jr $ra .ktext 0x80000180 # kernel code starts here mfc0 $k0 $12 # !! disable interrupts andi $k0 $k0 0xfffe # !! mtc0 $k0 $12 # !! move $k1 $at # save $at. User programs are not supposed to touch $k0 and $k1 sw $v0 s1 # We need to use these registers sw $a0 s2 # not using the stack mfc0 $k0 $13 # Cause register srl $a0 $k0 2 # Extract ExcCode Field andi $a0 $a0 0x1f bne $a0 $zero kexc # Exception Code 0 is I/O. Only processing I/O here lw $a0 0xffff0004 # get the input key sw $a0 ($gp) # store key li $a0 '.' # Show that we handled the interrupt li $v0 11 syscall j kdone kexc: mfc0 $v0 $14 # No exceptions in the program, but just in case of one addi $v0 $v0 4 # Return to next instruction mtc0 $v0 $14 kdone: lw $v0 s1 # Restore other registers lw $a0 s2 move $at $k1 # Restore $at mtc0 $zero $13 mfc0 $k0 $12 # Set Status register ori $k0 0x01 # Interrupts enabled mtc0 $k0 $12 # write back to status eret .kdata s1: .word 10 s2: .word 11
Символами "!!" отмечены инструкции, при выполнении которых в MARS может возникнуть фатальный повторный вход в обработчик события.
Кольцевой буфер
В разделе «Прерывания» мы уже встретились с основным свойством обработчика прерываний: обрабатывать надо быстро. Поэтому вся логика обработки должна находится в пользовательской программе, а обработчику, скажем, клавиатурного прерывания остаётся только перекладывать из регистра ввода куда-нибудь.
С другой стороны, приведённый там пример обработчика мало чем отличается от традиционного поллинга — та же проверка, был ли ввод или не было, только вместо регистра готовности опрашиваем ячейку памяти. Если делать это слишком редко, легко пропустить очередной ввод, а если слишком часто — потратить время на опрос вместо полезных действий.
Итак, главная проблема — синхронность побайтового ввода и обработки прерываний. И задача в том, чтобы обеспечить рассинхронизацию ввода и вывода: прерывание обрабатывается, когда оно происходит, а программа вводит свои байты, когда может.
Разумеется, для этого надо хранить не один обработанный, но ещё не запрошенный программой байт, а несколько. Эффективная структура, решающая такую задачу — кольцевой буфер.
Кольцевой буфер — массив (реже — список) данных, предназначенный для временного хранения асинхронно поступающих и асинхронно отбывающих данных.
- Буфер имеет фиксированный размер
- «Поверхность» буфера (HI) — адрес первой свободной ячейки буфера
- Когда поступят новые данные, они будут записаны в эту ячейку, а HI передвинется на следующую
- «Дно» буфера (LO) — адрес последней занятой ячейки буфера
- Когда понадобится забрать очередной элемент данных, его возьмут из этой ячейки, а LO передвинется на следующую
Размер буфера не является ограничением: когда HI подберётся к концу, в начале уже образуются свободные места и HI при очередном сдвиге начнёт указывать в начало
Аналогично произойдёт и с LO.
Вполне возможна ситуация, когда данных больше не прибыло, а программа требует ещё ввода. Эта ситуация называется исчерпанием буфера и распознаётся по тому, что HI и LO содержат один и тот же адрес:
Вариантов обработки исчерпания два:
- Заданным образом уведомить программу, что данных ещё нет (например, вернуть специальное служебное значение)
Это называется неблокирующим вводом
- Задержать операцию снятия данных до тех пор, пока они не появятся
Это называется блокирующим вводом
- Продолжить скакать по уже введённым когда-то данным, как если бы исчерпания не было
это называется грубой ошибкой алгоритма!
Заметим, что в момент создания кольцевой буфер находится в состоянии исчерпания (HI и LO указывают на нулевой элемент)
Другая ситуация — переполнение буфера: данные всё прибывают, а забирать их никто не хочет. Переполнение кольцевого буфера распознаётся по тому, что LO содержит адрес ячейки, следующей за HI (в кольце):
Вариантов обработки переполнения 2 (два других — плохие, негодные):
- Продолжать вводить, но не увеличивать HI. При этом все прибывшие после переполнения данные, кроме последнего элемента, исчезают.
Если источник данных позволяет, можно заранее уведомить его о переполнении — может, он приостановит передачу на время.
- Правда, для этого надо усложнить алгоритм работы, введя «верхний урез» поверхности буфера, при котором устройство уведомляется о приближающемся переполнении, и «нижний урез», при котором оно уведомляется о том, что переполнение больше не грозит.
- Выбрасывать все вновь прибывающие данные, пока для них нет места в буфере.
- См. примечания выше
- Продолжать как ни в чём ни бывало вводить и увеличивать HI.
При этом внезапно™ возникает ситуация исчерпания и пропадает всё содержимое буфера
- Задерживать ввод до тех пор, пока свободное место в буфере не появится.
Если обрабатывать таким образом прерывание ввода, то возникнет т. н. взаимоблокировка (deadlock): пока мы не вышли из прерывания, программа не может забрать данные из буфера, а пока программа не забрала данные из буфера, мы сидим в прерывании и ждём
⇒ никаких ожиданий в обработчике прерываний! Нет, значит — нет