Различия между версиями 5 и 6
Версия 5 от 2019-04-19 08:15:12
Размер: 21911
Редактор: FrBrGeorge
Комментарий:
Версия 6 от 2019-04-19 08:39:43
Размер: 29078
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 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

  1. Br - 1 если исключение произошло на инструкции в слоте задержки перехода.
  2. Pending interrupts - ожидающие (ещё не обработанные) прерывания.
  3. Exception code - код исключения. Прерывания, в отличие от исключений, могут возникать в произвольное время (например, прерывание ввода зависит от того, когда человек нажал на кнопку). Прерывания в Mars обрабатываются тем же кодом, что и исключения — специальным обработчиком по адресу 0x8000180.
    • Нужно сохранять все используемые регистры, кроме $k0 и $k1 (все, включая $at)

  4. Нельзя пользоваться стеком (а вдруг он испорчен, или прерывание произошло во время его изменения?)
    • Можно предусмотреть отдельный стек ядра (скажем, от 0xfffeeffc вниз) и пользоваться им (тогда $sp тоже сохраняется)

  5. Нужно различать исключения (поле Exception code регистра Cause ненулевое) и прерывания (поле EXC нулевое)
    • возврат из исключения по eret требует прибавить 4 к значению EPC (ошибочную инструкцию выполнять повторно обычно не надо)

    • возврат из прерывания по eret не требует увеличения EPC (инструкция по этому адресу ещё не выполнена)

  6. В Mars нужно как можно быстрее запретить обрабатывать исключения, чтобы исключение не случилось в ядре
    • Значит, в обработчике надо проводить как можно меньше времени — не все устройства Mars умеют выставлять бит в регистре, когда обработчик запрещён (бит IE регистра Status равен 0)

    • Если в поле Pending interrupts приехало несколько битов, значит, произошло несколько прерываний, и все надо обработать (или игнорировать)

  7. Перед выходом из обработчика
    • Очистить регистр Cause (биты прерываний и тип исключения)
    • Разрешить обработку прерываний
    • (бит EXL в Mars очистит инструкция eret)

    Программа, использующая прерывания, должна их включать: выставлять 1 в биты разрешения прерываний и во все нужные позиции маски прерываний, а также переводить используемые врешние устройства в режим работы по прерыванию:
            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)
  8. Сохранение всех регистров
  9. Вычисление типа исключений (0 — прерывание)
    • Переход на обработчик соответствующего исключения или на обработчик прерываний
  10. Обработчик прерываний:
    • Выяснение источника/ов прерывания (биты Pending)
    • Обработка всех случившихся прерываний (порядок определяется программно)
  11. Обработчик исключения
    • Обработка исключения :)

    • Вычисление нового EPC
  12. Восстановление всех регистров
  13. Разрешение вызова обработчика
  14. eret Замечание: как и во время обработки прерывания, во время системного вызова прерывания или вообще запрещены, или накапливаются (делаются «Pending») без вызова обработчика. Поэтому задача обработчика — как можно быстрее принять решение, что делать с прерыванием и вернуть управление пользовательской программе.

    • В обработчике выполняются только действия, необходимые для получения данных по прерыванию (чтение регистров, управление устройствами и т. п.)
  15. Вся логика обработки данных остаётся в программе пользователя
  16. При соблюдении некоторой дисциплины программирования можно вообще запретить пользовательской программе обращаться к внешним устройствам, оставив эти операции ядру, обрабатывающему прерывани

Пример: консоль Mars

Консоль Mars («Keyboard and Display MMIO Simulator») — воображаемое устройство, осуществляющее побайтовый ввод и вывод. Вернее окошко — «дисплей», куда выводятся байты, а нижнее — «клавиатура» (для удобства набираемый на клавиатуре текст отображается в этом окошке).

Console.png

Консоль имеет следующие регистры ввода-вывода

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 может возникнуть фатальный повторный вход в обработчик события.

Кольцевой буфер

В разделе «Прерывания» мы уже встретились с основным свойством обработчика прерываний: обрабатывать надо быстро. Поэтому вся логика обработки должна находится в пользовательской программе, а обработчику, скажем, клавиатурного прерывания остаётся только перекладывать из регистра ввода куда-нибудь.

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

  • Итак, главная проблема — синхронность побайтового ввода и обработки прерываний. И задача в том, чтобы обеспечить рассинхронизацию ввода и вывода: прерывание обрабатывается, когда оно происходит, а программа вводит свои байты, когда может.

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

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

RB0.png

  • Буфер имеет фиксированный размер
  • «Поверхность» буфера (HI) — адрес первой свободной ячейки буфера
    • Когда поступят новые данные, они будут записаны в эту ячейку, а HI передвинется на следующую
  • «Дно» буфера (LO) — адрес последней занятой ячейки буфера
    • Когда понадобится забрать очередной элемент данных, его возьмут из этой ячейки, а LO передвинется на следующую

Размер буфера не является ограничением: когда HI подберётся к концу, в начале уже образуются свободные места и HI при очередном сдвиге начнёт указывать в начало

RB1.png

Аналогично произойдёт и с LO.

Вполне возможна ситуация, когда данных больше не прибыло, а программа требует ещё ввода. Эта ситуация называется исчерпанием буфера и распознаётся по тому, что HI и LO содержат один и тот же адрес: RB2.png

Вариантов обработки исчерпания два:

  1. Заданным образом уведомить программу, что данных ещё нет (например, вернуть специальное служебное значение)
    • Это называется неблокирующим вводом

  2. Задержать операцию снятия данных до тех пор, пока они не появятся
    • Это называется блокирующим вводом

  3. Продолжить скакать по уже введённым когда-то данным, как если бы исчерпания не было
    • это называется грубой ошибкой алгоритма!

Заметим, что в момент создания кольцевой буфер находится в состоянии исчерпания (HI и LO указывают на нулевой элемент)

Другая ситуация — переполнение буфера: данные всё прибывают, а забирать их никто не хочет. Переполнение кольцевого буфера распознаётся по тому, что LO содержит адрес ячейки, следующей за HI (в кольце):

RB3.png

Вариантов обработки переполнения 2 (два других — плохие, негодные):

  1. Продолжать вводить, но не увеличивать HI. При этом все прибывшие после переполнения данные, кроме последнего элемента, исчезают.
    • Если источник данных позволяет, можно заранее уведомить его о переполнении — может, он приостановит передачу на время.

    • Правда, для этого надо усложнить алгоритм работы, введя «верхний урез» поверхности буфера, при котором устройство уведомляется о приближающемся переполнении, и «нижний урез», при котором оно уведомляется о том, что переполнение больше не грозит.
  2. Выбрасывать все вновь прибывающие данные, пока для них нет места в буфере.
    • См. примечания выше
  3. Продолжать как ни в чём ни бывало вводить и увеличивать HI.
    • При этом внезапно™ возникает ситуация исчерпания и пропадает всё содержимое буфера

  4. Задерживать ввод до тех пор, пока свободное место в буфере не появится.
    • Если обрабатывать таким образом прерывание ввода, то возникнет т. н. взаимоблокировка (deadlock): пока мы не вышли из прерывания, программа не может забрать данные из буфера, а пока программа не забрала данные из буфера, мы сидим в прерывании и ждём

    • никаких ожиданий в обработчике прерываний! Нет, значит — нет

LecturesCMC/ArchitectureAssembler2019/09_Interrupts (последним исправлял пользователь FrBrGeorge 2019-05-17 15:28:53)