Прерывания

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

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

bits

31-16

15-8

7-5

4

3,2

1

0

target

unused

Int. mask

unused

K/U

unused

Exception level

Int enable

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 при очередном сдвиге начнёт указывать в начало

RB1.png

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

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

RB2.png

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

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

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

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

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

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

RB3.png

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

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

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

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

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

Ввод с помощью кольцевого буфера

В этом примере, помимо кольцевого буфера ввода, присутствует два новых приёма.

Прямой доступ к памяти

Базовая статья на Хабре

Д/З

TODO