Различия между версиями 10 и 11
Версия 10 от 2019-05-07 17:31:54
Размер: 41157
Редактор: FrBrGeorge
Комментарий:
Версия 11 от 2019-05-17 15:28:53
Размер: 41298
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 12: Строка 12:
 * Маскирование прерываний.
  
. Прерывания, в зависимости от возможности запрета, делятся на:
 * Маскирование прерываний. Прерывания, в зависимости от возможности запрета, делятся на:
Строка 36: Строка 35:
 Регистр '''Cause''':

Регистр '''Cause''':
Строка 40: Строка 39:

Строка 63: Строка 60:
 {{{  {{{#!highlight nasm
Строка 106: Строка 103:
{{{ {{{#!highlight nasm
Строка 117: Строка 114:
{{{ {{{#!highlight nasm
Строка 137: Строка 134:
{{{ {{{#!highlight nasm
Строка 156: Строка 153:
{{{ {{{#!highlight nasm
Строка 232: Строка 229:
 . Итак, главная проблема — синхронность побайтового ввода и обработки прерываний. И задача в том, чтобы обеспечить ''рассинхронизацию'' ввода и вывода: прерывание обрабатывается, когда оно происходит, а программа вводит свои байты, когда может. Итак, главная проблема — синхронность побайтового ввода и обработки прерываний. И задача в том, чтобы обеспечить ''рассинхронизацию'' ввода и вывода: прерывание обрабатывается, когда оно происходит, а программа вводит свои байты, когда может.
Строка 236: Строка 233:
 . '''Кольцевой буфер '''— массив (реже — список) данных, предназначенный для временного хранения асинхронно поступающих и асинхронно отбывающих данных. '''Кольцевой буфер '''— массив (реже — список) данных, предназначенный для временного хранения асинхронно поступающих и асинхронно отбывающих данных.
Строка 293: Строка 290:
 {{{  {{{#!highlight nasm
Строка 316: Строка 313:
  Есть подозрение, что регистров ядра '''$k''' два потому, что ''немедленно и безусловно'' надо сохранять как раз два пользовательских регистра — '''$at ''' и '''$sp.'''

 Стек удобно располагать от самой верхней доступной ячейки памяти. Оказалось, что MARS выделяет под память ядра 4 Мб, так что максимальный адрес получается 0x90400000

 При обработке событий важно сразу разделить исключения (для возврата из которых надо прибавлять 4 к содержимому $EPC) и [[https://moodle.cs.msu.ru/mod/lesson/view.php?id=1950|прерывания]] (для которых не надо):

 {{{
Есть подозрение, что регистров ядра '''$k''' два потому, что ''немедленно и безусловно'' надо сохранять как раз два пользовательских регистра — '''$at ''' и '''$sp.'''

Стек удобно располагать от самой верхней доступной ячейки памяти. Оказалось, что MARS выделяет под память ядра 4 Мб, так что максимальный адрес получается 0x90400000

При обработке событий важно сразу разделить исключения (для возврата из которых надо прибавлять 4 к содержимому $EPC) и [[https://moodle.cs.msu.ru/mod/lesson/view.php?id=1950|прерывания]] (для которых не надо):

{{{#!highlight nasm
Строка 343: Строка 340:
 Здесь реализована обработка переполнения с отбрасыванием предыдущего введённого.

 Других обработчиков '' прерываний'' нет, но есть обработчик исключения. Дело в том, что ввод'' ''символа из'' программы'' в нашем примере реализован нестандартным для Mars системным вызовом №22 (и это второй новый приём). Для начала обработаем все исключения, кроме исключения «ошибочный системный вызов», которое имеет №8 , но в регистре $13 (Cause) кодируется со сдвигом на 2 бита как 0x20 :

 {{{
Здесь реализована обработка переполнения с отбрасыванием предыдущего введённого.

Других обработчиков '' прерываний'' нет, но есть обработчик исключения. Дело в том, что ввод'' ''символа из'' программы'' в нашем примере реализован нестандартным для Mars системным вызовом №22 (и это второй новый приём). Для начала обработаем все исключения, кроме исключения «ошибочный системный вызов», которое имеет №8 , но в регистре $13 (Cause) кодируется со сдвигом на 2 бита как 0x20 :

{{{#!highlight nasm
Строка 366: Строка 363:
 Поскольку в случае необрабатываемого исключения всё равно программу надо завершить, использование регистров больше не ограничено.

 Константа IOSIZE — размер буфера. Размер — степень 2, так что вместо деления с остатком на этот размер при замыкании буфера в кольцо мы используем побитовое AND с маской IOSZB=(ШИРИНА-1,,2,,)

 Теперь обработаем исключение 8. Нас интересует только номер сервиса 22 (это будет буферизованный ввод):

 {{{
Поскольку в случае необрабатываемого исключения всё равно программу надо завершить, использование регистров больше не ограничено.

Константа IOSIZE — размер буфера. Размер — степень 2, так что вместо деления с остатком на этот размер при замыкании буфера в кольцо мы используем побитовое AND с маской IOSZB=(ШИРИНА-1,,2,,)

Теперь обработаем исключение 8. Нас интересует только номер сервиса 22 (это будет буферизованный ввод):

{{{#!highlight nasm
Строка 387: Строка 384:
 Здесь введена договорённость о неблокирующем вводе: если новых байтов ещё нет, возвращается -1

 Осталось только написать эпилог (не забудем прибавить 4 к $ESP в обработчиках исключений)

 {{{
Здесь введена договорённость о неблокирующем вводе: если новых байтов ещё нет, возвращается -1

Осталось только написать эпилог (не забудем прибавить 4 к $ESP в обработчиках исключений)

{{{#!highlight nasm
Строка 410: Строка 407:
 Наконец, вот код [[attachment:interruptBS.asm|основной программы]]. Для пущей асинхронности время в ней тратится в цикле случайной длины.

 {{{
Наконец, вот код [[attachment:interruptBS.asm|основной программы]]. Для пущей асинхронности время в ней тратится в цикле случайной длины.

{{{#!highlight nasm
Строка 452: Строка 449:
 . Многие устройства (например, жёсткие диски, сетевые карты и т. д.) обмениваются с компьютером ''большими ''объёмами данных.  * Многие устройства (например, жёсткие диски, сетевые карты и т. д.) обмениваются с компьютером ''большими ''объёмами данных.
Строка 459: Строка 456:
 ⇒ Пускай ''само устройство ''научится --(есть из тарел)--читать из оперативной памяти и записывать в неё
  * Не понадобится массовый MMIO (регистры управления удобнее оставить)
⇒ Пускай ''само устройство ''научится --(есть из тарел)--читать из оперативной памяти и записывать в неё
 * Не понадобится массовый MMIO (регистры управления удобнее оставить)

Прерывания

  • Прерывание — сигнал, сообщающий процессору о наступлении какого-либо внешнего события.
    • асинхронны: могут произойти в любое время в любом месте выполнения программы (ср. исключения: могут возникнуть только при выполнении конкретных инструкций)
    • позволяют освободить 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)

Программа, использующая прерывания, должна их включать: выставлять 1 в биты разрешения прерываний и во все нужные позиции маски прерываний, а также переводить используемые врешние устройства в режим работы по прерыванию:

  •    1         mfc0    $a0 $12                 # read from the status register
       2         ori     $a0 0xff11              # enable all interrupts
       3         mtc0    $a0 $12                 # write back to the status register
       4 
       5         li      $a0 2                   # enable keyboard interrupt
       6         sw      $a0 0xffff0000
    

Сам обработчик событий по адресу 0x800000180, таким образом, обычно состоит из следующих частей:

  • Запрет вызова обработчика (бит IE)
  • Сохранение всех регистров
  • Вычисление типа исключений (0 — прерывание)
    • Переход на обработчик соответствующего исключения или на обработчик прерываний
  • Обработчик прерываний:
    • Выяснение источника/ов прерывания (биты Pending)
    • Обработка всех случившихся прерываний (порядок определяется программно)
  • Обработчик исключения
    • Обработка исключения :)

    • Вычисление нового EPC
  • Восстановление всех регистров
  • Разрешение вызова обработчика
  • eret

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

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

Пример: консоль 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 тактов в секунду).

   1 loop:   lb      $t0 0xffff0000          # готовность ввода
   2         andi    $t0 $t0 1               # есть?
   3         beqz    $t0 loop                # нет — снова
   4         lb      $a0 0xffff0004          # считаем символ
   5         li      $v0 11                  # выведем его
   6         syscall
   7         b       loop

Чуть более сложный пример с выводом, в котором видна проблема переполнения. Вывод начинает работать (точнее, бит готовности выставляется в первый раз) только после нажатия «Reset» (подозреваю, что это — тоже пример из «реальной жизни»: при включении питания многие устройства находятся в неопределённом состоянии и требуют инициализации).

   1         li      $t1 0
   2 loop:   beqz    $t1 noout               # выводить не надо
   3 loopi:  lb      $t0 0xffff0008          # готовность вывода
   4         andi    $t0 $t0 1               # есть?
   5         beqz    $t0 loopi               # а надо! идём обратно
   6         sb      $t1 0xffff000c          # запишем байт
   7         li      $t1 0                   # обнулим данные
   8 noout:  lb      $t0 0xffff0000          # готовность ввода
   9         andi    $t0 $t0 1               # есть?
  10         beqz    $t0 loop                # нет — снова
  11         lb      $t1 0xffff0004          # считаем символ
  12         b       loop

Выставляя ползунок «Delay Length» в большое значение, мы заставляем консоль долго не давать готовности по выводы (в течение, скажем, 20 инструкций). Пока программа находится в половинке вывода (цикл loopi:), она не успевает за вводом.

Задание: пронаблюдать, что происходит с регистрами ввода, когда пользователь много нажимает на клавиатуре, а программа не успевает считать.

Главное свойство консоли она может инициировать прерывания в момент готовности ввода или вывода. Устанавливая в 1 первый бит в регистре RcC, мы разрешаем консоли возбуждать прерывание всякий раз, как пользователь нажал на клавишу. Устанавливая в 1 первый бит регистра TxC, мы разрешаем прерывание типа «окончание вывода». И в том, и в другом случае прерывание возникает одновременно с появлением бита готовности (нулевого) в соответствующем регистре. Таким образом, вместо постоянного опроса регистра мы получаем однократный вызов обработчика в подходящее время. Рассмотрим пример очень грязного обработчика прерывания от клавиатуры, который ничего не сохраняет, не проверяет причину события и номер прерывания. Зато по этому коду хорошо видна асинхронная природа работы прерывания. Рекомендуется выставить ползунок Mars «Run speed» в низкое значение (например, 5 раз в секунду).

   1         li      $a0 2                   # разрешим прерывания от клавиатуры
   2         sw      $a0 0xffff0000
   3         li      $a0 0
   4 loop:   beqz    $a0 loop                # вечный цикл
   5         beq     $a0 0x1b done           # ESC — конец
   6         li      $v0 11                  # выведем символ
   7         syscall
   8         li      $a0 0                   # затрём $a0
   9         j       loop
  10 done:   li      $v0 10
  11         syscall
  12 
  13 .ktext  0x80000180                      # очень грязный код обработчика
  14         lw      $a0 0xffff0004          # считаем символ
  15         eret

В примере ниже «полезные вычисления» делает подпрограмма sleep (на самом деле ничего полезного), время от времени проверяя содержимое ячейки 0 в глобальной области. Это лучше, чем модифицировать регистр или метку, определяемую пользовательской программой. Обработчик клавиатурного прерывания (для простоты — не проверяя, клавиатурное ли оно) записывает в эту ячейку код нажатой клавиши.

   1 .text
   2         .globl main
   3 main:
   4         mfc0    $a0 $12                 # read from the status register
   5         ori     $a0 0xff11              # enable all interrupts
   6         mtc0    $a0 $12                 # write back to the status register
   7 
   8         li      $a0 2                   # enable keyboard interrupt
   9         sw      $a0 0xffff0000
  10 
  11 here:
  12         jal     sleep
  13         lw      $a0 ($gp)               # print key stored in ($gp)
  14         beq     $a0 0x1b done           # ESC terminates
  15         li      $v0 1
  16         syscall
  17         j       here
  18 done:   li      $v0 10
  19         syscall
  20 
  21 .eqv    ZZZ     100000
  22 sleep:  li      $t0 ZZZ                 # Do nothing
  23 tormo0: subi    $t0 $t0 1
  24         blez    $t0 tormo1
  25         j       tormo0
  26 tormo1: jr      $ra
  27 
  28 .ktext  0x80000180                      # kernel code starts here
  29 
  30         mfc0    $k0 $12                 # !! disable interrupts
  31         andi    $k0 $k0 0xfffe          # !!
  32         mtc0    $k0 $12                 # !!
  33 
  34         move    $k1 $at                 # save $at. User programs are not supposed to touch $k0 and $k1
  35         sw      $v0 s1                  # We need to use these registers
  36         sw      $a0 s2                  # not using the stack
  37 
  38         mfc0    $k0 $13                 # Cause register
  39         srl     $a0 $k0 2               # Extract ExcCode Field
  40         andi    $a0 $a0 0x1f
  41         bne     $a0 $zero kexc          # Exception Code 0 is I/O. Only processing I/O here
  42 
  43         lw      $a0 0xffff0004          # get the input key
  44         sw      $a0 ($gp)               # store key
  45         li      $a0 '.'                 # Show that we handled the interrupt
  46         li      $v0 11
  47         syscall
  48         j       kdone
  49 
  50 kexc:   mfc0    $v0 $14                 # No exceptions in the program, but just in case of one
  51         addi    $v0 $v0 4               # Return to next instruction
  52         mtc0    $v0 $14
  53 kdone:
  54         lw      $v0 s1                  # Restore other registers
  55         lw      $a0 s2
  56         move    $at $k1                 # Restore $at
  57         mtc0    $zero $13
  58 
  59         mfc0    $k0 $12                 # Set Status register
  60         ori     $k0 0x01                # Interrupts enabled
  61         mtc0    $k0 $12                 # write back to status
  62 
  63         eret
  64 
  65 .kdata
  66 s1:     .word 10
  67 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): пока мы не вышли из прерывания, программа не может забрать данные из буфера, а пока программа не забрала данные из буфера, мы сидим в прерывании и ждём

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

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

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

  • Первый — это стек ядра. Чтобы не быть ограниченными в использовании регистров, вызове подпрограмм и т. п., в обработчике событий (в случае MARS — прерываний, исключений и ловушек) хочется использовать стек. Пользовательский стек использовать нельзя: а вдруг он испорчен, и мы обрабатываем исключение, с этим связанное?

    Таким образом, надо предусмотреть ещё одну область памяти, пользовательский стек куда-нибудь сохранять в прологе обработчика, записывать в $sp адрес этой области, а в эпилоге — восстанавливать. Вот как начнётся обработчик:

       1 .eqv    IOSIZE  64
       2 .eqv    IOSZB   63
       3 
       4 .kdata
       5 ksp:    .word   0x90400000              # Kernel SP
       6 kinhi:  .word   0
       7 kinlo:  .word   0
       8 kin:    .space  IOSIZE
       9 kmsgexc:.asciiz "Exception "
      10 kmsgat: .asciiz " at "
      11 
      12 .ktext  0x80000180
      13         mfc0    $k0 $12
      14         andi    $k0 $k0 0xfffe
      15         mtc0    $k0 $12                 # Запрет прерываний
      16 
      17         move    $k0 $at                 # спрячем $at
      18         move    $k1 $sp                 # спрячем $sp
      19         lw      $sp ksp                 # Новый $sp! можно сохранять ВСЁ
      20         push    $k0                     # спрятанный $at
      21         push    $k1                     # спрятанный $sp
    

Есть подозрение, что регистров ядра $k два потому, что немедленно и безусловно надо сохранять как раз два пользовательских регистра — $at и $sp.

Стек удобно располагать от самой верхней доступной ячейки памяти. Оказалось, что MARS выделяет под память ядра 4 Мб, так что максимальный адрес получается 0x90400000

При обработке событий важно сразу разделить исключения (для возврата из которых надо прибавлять 4 к содержимому $EPC) и прерывания (для которых не надо):

   1         mfc0    $k0 $13                 # Причина
   2         andi    $k1 $k0 0x7c            # Исключение или прерывание?
   3         bnez    $k1 exception           # …исключение
   4         andi    $k1 $k0 0x100           # Клавиатура?
   5         beqz    $k1 other               # …не клавиатура
   6         lw      $k0 0xffff0004          # считаем код
   7 
   8         lw      $k1 kinhi               # Свободное место в буфере
   9         sb      $k0 kin($k1)            # сложим туда байт
  10         addi    $k1 $k1 1               # следующее место
  11         andi    $k1 $k1 IOSZB           # кольцо
  12         lw      $k0 kinlo
  13         beq     $k0 $k1 final           # переполнение буфера
  14         sw      $k1 kinhi               # сохраним индекс в кольце
  15 
  16         j       final
  17 
  18 other:
  19         j final

Здесь реализована обработка переполнения с отбрасыванием предыдущего введённого.

Других обработчиков прерываний нет, но есть обработчик исключения. Дело в том, что ввод символа из программы в нашем примере реализован нестандартным для Mars системным вызовом №22 (и это второй новый приём). Для начала обработаем все исключения, кроме исключения «ошибочный системный вызов», которое имеет №8 , но в регистре $13 (Cause) кодируется со сдвигом на 2 бита как 0x20 :

   1 exception:
   2         beq     $k1 0x20 sysexc         # syscall
   3 fatalexc:                               # необрабатываемое исключение
   4         la      $a0 kmsgexc             # можно пользоваться любыми регистрами:
   5         li      $v0 4                   # всё равно аварийный останов
   6         syscall
   7         sra     $a0 $k1 2
   8         li      $v0 34
   9         syscall
  10         la      $a0 kmsgat
  11         li      $v0 4
  12         syscall
  13         mfc0    $a0 $14
  14         li      $v0 34
  15         syscall
  16         li      $v0 17
  17         syscall

Поскольку в случае необрабатываемого исключения всё равно программу надо завершить, использование регистров больше не ограничено.

Константа IOSIZE — размер буфера. Размер — степень 2, так что вместо деления с остатком на этот размер при замыкании буфера в кольцо мы используем побитовое AND с маской IOSZB=(ШИРИНА-12)

Теперь обработаем исключение 8. Нас интересует только номер сервиса 22 (это будет буферизованный ввод):

   1 sysexc: beq     $v0 22 sysin
   2         j       fatalexc
   3 sysin:
   4         lw      $k0 kinhi               # есть ли в буфере данные
   5         lw      $k1 kinlo               # hilo
   6         bne     $k0 $k1 sysinY          # есть
   7         li      $v0 -1                  # нету
   8         j       exfinal
   9 sysinY: lb      $v0 kin($t1)            # очередной символ
  10         addi    $t1 $t1 1               # буфер прокрутим
  11         andi    $t1 $t1 IOSZB
  12         sw      $t1 kinlo
  13         j       exfinal

Здесь введена договорённость о неблокирующем вводе: если новых байтов ещё нет, возвращается -1

Осталось только написать эпилог (не забудем прибавить 4 к $ESP в обработчиках исключений)

   1 exfinal:
   2         mfc0    $k0 $14                 # Завершаем исключение
   3         addiu   $k0 $k0 4
   4         mtc0    $k0 $14
   5 final:
   6         pop     $k1
   7         pop     $k0
   8         sw      $sp ksp
   9         move    $sp $k1
  10         move    $at $k0
  11 
  12         mtc0    $zero $13               # зануляем причину
  13         mfc0    $k0 $12                 #
  14         ori     $k0 0x01                #
  15         mtc0    $k0 $12                 # разрешаем
  16 
  17         eret

Наконец, вот код основной программы. Для пущей асинхронности время в ней тратится в цикле случайной длины.

   1 .text
   2         lw      $t0 0xffff0000
   3         ori     $t0 $t0 2               # Включили клавиатуру
   4         sw      $t0 0xffff0000
   5 
   6 loop:   li      $v0 22                  # НОВЫЙ syscall
   7         syscall
   8         bltz    $v0 noinput
   9         beq     $v0 0x1b end            # ESC — выход
  10         move    $a0 $v0
  11         li      $v0 1                   # выведем
  12         syscall
  13         li      $a0 0x20                # с пробелом
  14         li      $v0 11
  15         syscall
  16         j       loop                    # есть ещё ввод?
  17 noinput:
  18         jal     sleeprnd
  19         j       loop
  20 end:    li      $v0 10
  21         syscall
  22 
  23         # прождать случайное время
  24 sleeprnd:
  25         li      $a0 0
  26         li      $a1 100000
  27         li      $v0 42
  28         syscall
  29 sleepnext:
  30         subi    $a0 $a0 1
  31         blez    $a0 sleepdone
  32         mul.d   $f0 $f0 $f0
  33         j       sleepnext
  34 sleepdone:
  35         jr      $ra

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

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

  • Многие устройства (например, жёсткие диски, сетевые карты и т. д.) обмениваются с компьютером большими объёмами данных.

    • Пословное чтение/запись этих данных через MMIO-регистры крайне неэффективно
    • обмен малыми объёмами
    • всю работу делает процессор
  • Отображение части памяти устройства на оперативную память само по себе тоже неэффективно
    • либо это то же MMIO, только адреса разные
    • либо кто-то должен отвечать за заполнение этой области и актуальность находящихся там данных — опять процессор?

⇒ Пускай само устройство научится есть из тарелчитать из оперативной памяти и записывать в неё

  • Не понадобится массовый MMIO (регистры управления удобнее оставить)
  • Обмен теперь выглядит так:
    • Записываем адрес в регистр адреса
    • Записываем размер в регистр размера
    • Записываем команду (скажем, чтения) в регистр команд
    • Устройство начинает складывать данные прямо в память в указанное место
    • …складывает
    • …складывает
    • а программа тем временем работает
  • Когда устройство всё сложит, возникнет прерывание типа «Окончание операции В/В» Возможные проблемы:
    • Устройств несколько, а шина данных одна
    • Арбитраж (кому первому предоставить доступ к памяти)
    • Многоканальность
  • Устройство (например, сетевой интерфейс) готово прислать результаты операции чтения, а его забыли спросить (или не успели), потому что много и части

    • Bus mastering («захват шины»: устройство само может начать операцию В/В и инициировать прерывание, т. е. реализует часть ПДП контроллера)

  • Область обмена такая большая, что непрерывного куска в памяти прост не найти

    • Вместо одной области устройству передаётся список, и оно должно уметь раскладывать данные во все элементы списка

Д/З

TODO

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