Опишите порядок выполнения аппаратного прерывания при нажатии клавиши?
Система прерываний любого компьютера является его важнейшей частью, позволяющей быстро реагировать на события, обработка которых должна выполнятся немедленно: сигналы от машинных таймеров, нажатия клавиш клавиатуры или мыши, сбои памяти и пр. Рассмотрим в общих чертах компоненты этой системы.
Сигналы аппаратных прерываний, возникающие в устройствах, входящих в состав компьютера или подключенных к нему, поступают в процессор не непосредственно, а через два контроллера прерываний, один из которых называется ведущим, а второй - ведомым
Два контроллера используются для увеличения допустимого количества внешних устройств. Дело в том, что каждый контроллер прерываний может обслуживать сигналы лишь от 8 устройств. Для обслуживания большего количества устройств контроллеры можно объединять, образуя из них веерообразную структуру. В современных машинах устанавливают два контроллера, увеличивая тем самым возможное число входных устройств до 15 (7 у ведущего и 8 у ведомого контроллеров).
К входным выводам IRQ1...IRQ7 и IRQ8...IRQ15 (IRQ - это сокращение от Interrupt Request, запрос прерывания) подключаются выводы устройств, на которых возникают сигналы прерываний. Выход ведущего контроллера подключается к входу INT микропроцессора, а выход ведомого - к входу IRQ2 ведущего. Основная функция контроллеров - передача сигналов запросов прерываний от внешних устройств на единственный вход прерываний микропроцессора. При этом, кроме сигнала INT, контроллеры передают в микропроцессор по линиям данных номер вектора, который образуется в контроллере путем сложения базового номера, записанного в одном из его регистров, с номером входной линии, по которой поступил запрос прерывания. Номера базовых векторов заносятся в контроллеры автоматически в процессе начальной загрузки компьютера. Для ведущего контроллера базовый вектор всегда равен 8, для ведомого - 70h. Таким образом, номера векторов, закрепленных за аппаратными прерываниями, лежат в диапазонах 8h...Fh и 70h...77h. Очевидно, что номера векторов аппаратных прерываний однозначно связаны с номерами линий, или уровнями IRQ, а через них - с конкретными устройствами компьютера. Процессор, получив сигнал прерывания, выполняет последовательность стандартных действий, обычно называемых процедурой прерывания. Подчеркнем, что здесь идет речь лишь о реакции самого процессора на сигналы прерываний, а не об алгоритмах обработки прерываний, предусматриваемых пользователем в программах обработки прерываний.
Функции обработчика прерываний зависят от решаемой задачи и назначения того устройства, от которого поступают сигналы прерываний. В случае прерываний от клавиатуры задача обработчика прерываний - принять и сохранить код нажатой клавиши. Прерывания от мыши свидетельствуют о ее перемещении, что требует обновления положения курсора на экране. Если обслуживаемым устройством является физическая установка, то сигнал прерывания может говорить о том, что в установке накоплен определенный объем данных, которые надо перенести из памяти установки в память компьютера. В любом случае обработчик прерываний должен быть программой несложной, для выполнения которой не требуется много машинного времени.
Рассмотрим структуру программы с обработкой аппаратных прерываний. Наиболее удобным аппаратным прерыванием, которое можно использовать в исследовательских целях, является прерывание от системного таймера, которое генерируется 18.2 раза в секунду и служит источником сигналов для хода системных часов, отсчитывающих время, истекшее после включения машины. Замена системного обработчика на прикладной не приводит к каким-либо неприятностям, кроме, возможно, остановки на некоторое время системных часов.
Будем считать, что наш программный комплекс представляет собой программу типа .ЕХЕ и что обработчик прерываний входит в общий с основной программой программный сегмент. Для определенности будем использовать вектор 08h, хотя, разумеется, для любого другого аппаратного вектора структура программы останется той же. Поначалу приведем текст программы с некоторыми купюрами.
В приведенном примере обработчик прерываний расположен в конце программы, после главной процедуры main. Взаимное расположение процедур не имеет ни малейшего значения; с таким же успехом обработчик можно было поместить в начале программы. Не имеет также значения, выделен ли обработчик в отдельную процедуру или просто начинается с метки.
Для того, чтобы прикладной обработчик получал управление в результате прерываний, его адрес следует поместить в соответствующий вектор прерывания. При этом исходное содержимое вектора будет затерто, и если прерывания будут поступать и после завершения программы, возникнет весьма неприятная ситуация, когда управление будет передаваться по адресу, по которому в памяти может располагаться что угодно. Поэтому стандартной методикой является сохранение в памяти исходного содержимого вектора и восстановление этого содержимого перед завершением программы.
Хотя и чтение, и заполнение вектора прерываний можно выполнить с помощью простых команд mov, однако предпочтительнее использовать специально предусмотренные для этого функции DOS. Для чтения вектора используется функция с номером 35h. В регистр AL помещается номер вектора. Функция возвращает исходное содержимое вектора в парс регистров ES:BX (легко догадаться, что в ES сегментный адрес, а в ВХ смещение). Для хранения исходного содержимого вектора в сегменте данных предусмотрена двухсловная ячейка old_08. В младшем слове этой ячейки (с фактическим адресом old_08) будет хранится смещение, в старшем (с фактическим адресом old_08+2) - сегментный адрес. Для того, чтобы обратиться к словам, составляющим эту ячейку, приходится использовать описатель word ptr, который как бы заставляет транслятор на время забыть о начальном объявлении ячейки и позволяет рассматривать ее, как два отдельных слова. Если бы мы отвели для исходного вектора две 16-битовые ячейки, например
то к ним можно было бы обращаться без всяких описателей.
Сохранив исходный вектор, можно установить в нем адрес нашего обработчика. Для установки вектора в DOS предусмотрена функция 25h. Она требует указания номера устанавливаемого вектора в регистре AL, a его полного адреса - в парс регистров DS:DX. Здесь нас подстерегает неприятность. Занести в регистр DX смещение нашего обработчика new_08 не составляет труда, это делается командой
mov DX,offset new_08 ;Смещение нашего обработчика
Однако регистр DS у нас занят - в нем хранится сегментный адрес сегмента данных. Придется на какое-то время сохранить этот адрес, для чего удобнее всего воспользоваться стеком. Откуда взять сегментный адрес обработчика? Между прочим, в языке ассемблера существует специальная конструкция, позволяющая определить сегментный адрес любого поля. В нашем случае она выглядела бы таким образом:
В примере 3-3 использован другой прием - содержимое CS отправляется в стек и тут же извлекается оттуда в регистр DS:
push CS pop DS
После возврата из DOS надо не забыть восстановить исходное содержимое DS, сохраненное в стеке. Инициализация обработчика прерываний закончена. Начиная с этого момента, каждый сигнал таймера будет приводить к прерыванию продолжающейся основной программы и передаче управления на процедуру new_08.
Перед завершением программы необходимо поместить в вектор 8 адрес исходного, системного обработчика, который был сохранен в двухсловном поле old_08. Перед вызовом функции 25h установки вектора в регистры DS:DX надо занести содержимое этого двухсловного поля. Эту операцию можно выполнить одной командой Ids, если указать в качестве ее первого операнда регистр DX, а в качестве второго - адрес двухсловной ячейки, в нашем случае old_08. Именно имея в виду использование этой команды, мы и объявили поле для хранения вектора двухсловным, отчего возникли некоторые трудности при его заполнении командами mov. Если бы мы использовали второй предложенный выше вариант и отвели для хранения вектора две однословные ячейки (old_08_offs и old_08_seg), то команду Ids пришлось бы снабдить описателем изменения размера ячейки:
Ids DX,dword ptr old_08_offs
Между прочим, здесь так же разрушается содержимое DS, но поскольку сразу же вслед за функцией 25h вызывается функция 4Ch завершения программы, это не имеет значения.
Последнее, что нам осталось рассмотреть - это стандартные действия по завершению самого обработчика прерываний. Выше уже говорилось, что последней командой обработчика должна быть команда iret, возвращающая управление в прерванную программу. Однако перед ней необходимо выполнить еще одно обязательное действие - послать в контроллер
прерываний команду конца прерываний. Дело в том, что контроллер прерываний, передав в процессор сигнал прерывания INT, блокирует внутри себя линии прерываний, начиная с той, которая вызвала данное прерывание, и до последней в порядке возрастания номеров IRQ. Таким образом, прерывание, пришедшее, например, по линии IRQ 6 (гибкий диск) заблокирует дальнейшую обработку прерываний по линиям 6 и 7, а прерывание от таймера (IRQ0) блокирует вообще все прерывания (IRQ0...IRQ7, а также и IRQ8...IRQ15, поскольку все они являются разветвлением уровня IRQ2, см, гл. 1, рис. 1.11). Любой обработчик аппаратного прерывания обязан перед своим завершением снять блокировку в контроллере прерываний, иначе вся система прерываний выйдет из строя. Снятие блокировки осуществляется посылкой команды с кодом 20h в один из двух портов, закрепленных за контроллером прерываний. Для ведущего контроллера эта команда посылается в порт 20h, для ведомого - в порт A0h. Таким образом, если бы мы обрабатывали прерывания от часов реального времени (линия прерываний IRQ8, вектор 70h, ведомый контроллер), то команда конца прерывания выглядела бы так:
Mov AL,20h ;команда конца прерывания
Out A0h,AL ;пошлем ее в порт ведомого контроллера
Указанную последовательность команд иногда называют приказом, или командой EOI (от end of interrupt, конец прерывания).
Разобравшись в этих общих вопросах, рассмотрим пример реальной программы, включающей обработчик прерываний от таймера. Для того, чтобы приведенную выше фрагментарную программу преобразовать в действующую, надо написать содержательную часть самого обработчика, а также придумать, что будет делать основная программа после инициализации прерываний. Все это сделать очень просто.
Пусть наш обработчик в ответ на каждое прерывание от таймера выводит на экран какой-нибудь символ. Для этого можно воспользоваться функцией 0Eh прерывания BIOS 10h. Это прерывание обслуживает большое количество различных функций, обеспечивающих управление экраном. Сюда входят функции вывода символов и строк, настройки режимов видеосистемы, загрузки нестандартных таблиц символов и многие другие. Функция 0Eh предназначена для вывода на экран отдельных символов. Она требует указания в регистре AL кода выводимого символа. Процедура new_08 будет выглядеть в этом случае следующим образом:
Что же касается основной программы, то самое простое включить в нее (после завершения действий по инициализации обработчика прерываний) функцию DOS 01H ожидания ввода с клавиатуры:
mov AH,01h
int 21h
В результате программа, дойдя до этих строк, остановится (фактически будет выполняться цикл опроса клавиатуры в ожидании нажатия клавиши, включенный в состав программы реализации функции 01h DOS), а на экран непрерывной чередой будут выводиться символы коммерческого at (рис. 3.5). После нажатия на любую клавишу программа завершится.
Рис. 3.5. Вывод программы 3-3.
Приведенный пример позволяет обсудить чрезвычайно важный вопрос о взаимодействии обработчиков аппаратных прерываний с прерываемой программой и операционной системой. Особенностью аппаратных прерываний является то, что они могут возникнуть в любой момент времени и, соответственно, прервать выполняемую программу в любой точке. Текущая программа, разумеется, использует регистры, как общего назначения, так и сегментные. Если в обработчике прерывания мы разрушим содержимое хотя бы одного из регистров процессора, прерванная программа по меньшей мере продолжит свое выполнение неправильным образом, а скорее всего произойдет зависание системы. Поэтому в любом обработчике аппаратных прерываний необходимо в самом его начале сохранить все регистры, которые будут использоваться в программе обработчика, а перед завершением обработчика (перед командой iret) восстановить их. Регистры, которые обработчиком не используются, сохранять не обязательно.
В нашем простом обработчике используется только один регистр АХ. Его мы и сохраняем в стеке первой же командой push AX, восстанавливая в самом конце обработчика командой pop AX.
Вторая неприятность может возникнуть из-за того, что в обработчике аппаратного прерывания мы воспользовались для вывода на экран функцией BIOS. Вообще говоря, считается, что в обработчиках аппаратных прерываний нельзя использовать никакие системные средства. Причина такого запрета заключается в том, что аппаратное прерывание может придти в любой момент, в частности тогда, когда прерываемая программы сама выполняет какую-либо функцию DOS или BIOS. Однако, если мы прервем выполнение системной функции на полпути, и начнем выполнять ту же самую или даже другую функцию с начала, произойдет разрушение системы, которая не предусматривает такое "вложенное" выполнение своих программ. В настоящее время разработаны программные приемы, позволяющие эффективно обойти указанный запрет, однако использование их в программе драматически увеличивает ее сложность и объем, и рассматривать эти приемы мы здесь не будем.
В нашем случае дело усугубляется тем, что прерывания от таймера не только могут придти в тот момент, когда выполняется функция DOS, но и неминуемо приходят только в такие моменты, так как мы остановили программу с помощью вызова функции 01h прерывания 2Hi и, следовательно, все время, пока наша программа ждет нажатия клавиши, в действительности выполняются внутренние программы DOS. Именно поэтому мы отказались от использования в обработчике прерывания функции DOS и выводим на экран символы с помощью прерывания BIOS. Выполнение функции BIOS "на фоне" DOS не так опасно. Надо только следить за тем, чтобы наше прерывание BIOS в обработчике не оказалось вложенным в такое же прерывание BIOS в прерываемой программе.
Рассмотренный пример имеет существенный недостаток. Записав в вектор прерываний 8 адрес нашего обработчика, мы затерли исходное содержимое вектора и тем самым ликвидировали (в логическом плане) исходный, системный обработчик. Практически это приведет к тому, что на время работы нашей программы остановятся системные часы, и если в системе есть какие-то другие программы, использующие прерывания от таймера, они перестанут работать должным образом. Ликвидировать указанный недостаток очень просто: надо "сцепить" наш обработчик с системным так, чтобы в ответ на каждый сигнал прерывания активизировались последовательно оба обработчика. Рассмотрим методику сцепления обработчиков.
При инициализации прикладного обработчика, сцепляемого с системным, следует точно так же, как и раньше, сохранить в программе адрес системного обработчика и поместить в вектор прерывания адрес прикладного обработчика. Изменениям подвергнется только программа самого обработчика, начало которой должно выглядеть следующим образом:
Как будет работать такая программа? После того, как процессор выполнит процедуру прерывания, в стеке прерванного процесса оказываются три слова: слово флагов, а также двухсловный адрес возврата в прерванную программу (рис.3.6, нижние три слова стека).
Рис. 3.6. Стек прерванной программы в процессе выполнения прикладного обработчика прерываний.
CS1 - сегментный адрес прерванного процесса;
IP1 - смещение точки возврата в прерванную программу;
CS2 - сегментный адрес прикладного обработчика;
IP2 - смещение точки возврата в прикладной обработчик.
Именно такая структура данных должна быть на верху стека, чтобы команда iret, которой завершается любой обработчик прерываний, могла вернуть управление в прерванную программу.
Первая команда нашего обработчика pushf засылает в стек еще раз слово флагов, а команда дальнего вызова процедуры call cs:old_08 (где ячейка old_08 объявлена с помощью оператора dd двойным словом) в процессе передачи упражнения системному обработчику помещает в стек двухсловный адрес возврата на следующую команду прикладного обработчика. В результате в стеке формируется трехсловная структура, которая нужна команде iret.
Системный обработчик, закончив обработку данного прерывания, завершается командой iret. Эта команда забирает из стека три верхние слова и осуществляет переход по адресу CS2:IP2, т.е. на продолжение прикладного обработчика.
Завершающая команда нашего обработчика iret снимает со стека три верхних слова и передает упражнение по адресу CS1:IP1.
Описанная методика сцепления прикладного обработчика с системным используется чрезвычайно широко и носит специальное название перехвата прерывания.
Обработчики программных прерываний
Программные прерывания вызываются командой int, операндом которой служит номер вектора с адресом обработчика данного прерывания. Команда int используется прежде всего, как стандартный механизм вызова системных средств. Так, команда int 21H позволяет обратиться к многочисленным функциям DOS, а команды int 10h, int 13h или int 16h - к группам функций BIOS, отвечающим за управление теми или иными аппаратными средствами компьютера. В этих случаях обработчики прерываний представляют собой готовые системные программы, и в задачу программиста входит только вызов требуемого программного средства с помощью команды int с подходящим номером.
В некоторых специальных случаях, однако, программисту приходится писать собственный обработчик прерывания, которое уже обслуживается системой. Таким образом, например, осуществляется управление резидентными программами, которые для связи с внешним миром обычно используют прерывание 2Fh. В каждой резидентной программе имеется собственный обработчик этого прерывания, который, выполнив свою долю действий, передает управление "предыдущему", адрес которого находился ранее в векторе 2Fh, и был сохранен обработчиком в своих полях данных. Другой пример - перехват прерываний BIOS в обработчиках аппаратных прерываний с целью обнаружения моментов времени, когда ни одна из наличных программ не использует данное прерывание и, следовательно, сам обработчик может им воспользоваться.
Наконец, прикладной программист может воспользоваться одним из свободных векторов, написать собственный обработчик соответствующего прерывания и оставить его резидентным в памяти. После этого любые программы могут с помощью команды int вызывать этот обработчик, который, таким образом, становится резидентной программой общего пользования.