Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

4. МУЛЬТИЗАДАЧНОСТЬ В ПРОЦЕССОРЕ I80286

4.1. Задача и сегмент состояния задачи

4.2. Переключение задач

4.3. Синхронизация задач и семафоры

4.4. Пример мультизадачного монитора

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

Подтверждением этому служит наличие огромного количества резидентных программ - от простейших часов и калькуляторов до сложных резидентных интегрированных сред, аналогичных Borland SideKick.

Резидентные программы, за редким исключением, не реализуют настоящую мультизадачность. Обычно с помощью резидентных программ вы можете только переключаться от одной запущенной программы к другой. Типичный пример "мультизадачной" резидентной программы - часы, которые работают параллельно с другими программами и постоянно показывают время в заранее определённом месте экрана. Другой пример - резидентная программа фоновой печати PRINT, входящая в состав MS-DOS.

Использование резидентных программ - не самый лучший способ организации переключения задач. Это связано с возникновением конфликтов между различными резидентными программами - например, по клавишам их активизации или по используемым прерыванием.

Учитывая необходимость реализации переключения программ, фирма Microsoft в операционной системе MS-DOS версии 5.0 реализовала переключатель программ, встроенный в диалоговую оболочку DOSSHELL. Эта оболочка позволяет запустить на выполнение несколько программ и переключаться от одной к другой. Но активна только одна задача - та, на которую переключился пользователь. Остальные находятся в "замороженном" состоянии.

Однако часто бывает необходимо, чтобы программы работали в режиме разделения времени процессора. В этих случаях нужно использовать операционную систему, работающую в мультизадачном режиме - OS/2, UNIX, XENIX, WINDOWS, DeskView.

Мультизадачность позволяет не только задействовать все ресурсы современных персональных компьютеров, но и существенно повышает производительность труда. Например, вы с помощью модема принимаете файл размером 1-2 мегабайта. Скорость передачи данных по телефонным линиям редко превышает 2400 бит в секунду, поэтому в худшем случае процесс получения файла может растянуться на часы. Без использования мультизадачности ваш компьютер всё это время будет занят только приёмом файла. Более того, в основном он будет находиться в состоянии ожидания, так как передача данных производится очень медленно.

Без мультизадачности вы обречены ждать завершения процесса и не сможете выполнять на компьютере никакую другую работу, что само по себе весьма печально. И это несмотря на то, что компьютер практически ничем не загружен!

Как же можно организовать мультизадачность? Самый простой способ заключается в использовании таймера.

Напомним, что таймер вырабатывает прерывание IRQ0 примерно 18,2 раза в секунду. Операционная система может использовать это прерывание для переключения с одной выполняющейся программы на другую, предоставляя каждой программе квант времени. При этом у пользователя компьютера возникнет иллюзия параллельной работы нескольких программ.

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

Первый момент связан с загрузкой программ. В мультизадачной среде одновременно работают несколько программ. Они должны либо все одновременно находиться в оперативной памяти, либо загружаться туда по мере необходимости с магнитного диска.

Если программы загружены в память одновременно, то для каждой из них, разумеется, должна быть выделена своя область памяти. Как для программ, так и для областей данных, принадлежащих этим программам.

Для обеспечения надёжной работы программ было бы также неплохо изолировать эти области памяти друг от друга для предотвращения случайного или преднамеренного доступа за пределы выделенной области памяти.

Второй момент - процесс переключения от выполнения одной программы к выполнению другой. Инициатором этого процесса обычно является таймер, генерирующий периодические прерывания.

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

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

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

Ситуация напоминает железнодорожный переезд - движение разрешено либо поездам, либо автомобилям, но не одновременно и тем и другим!

Решение проблемы также аналогичное - установка семафора. Только в случае с железной дорогой это настоящий семафор, а в случае мультизадачной операционной системы - это ячейка памяти, отражающая текущее состояние ресурса - свободен или занят. Если ресурс компьютера занят какой либо программой, другие программы должны ждать, пока он не освободится (пока не проедет поезд).

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

Поэтому все мультизадачные операционные системы используют защищённый режим работы процессора. Адресация памяти в защищённом режиме полностью удовлетворяет требованиям изолирования параллельно работающих программ.

Например, для каждой программы можно создать свою локальную таблицу дескрипторов LDT. В этом случае программа принципиально будет иметь доступ только к своей области памяти, выделенной ей операционной системой. Для организации межзадачного взаимодействия можно вызывать модули операционной системой через вентили вызова. Кроме того, операционная система может создать области памяти "общего пользования", поместив соответствующие дескрипторы в глобальную таблицу дескрипторов GDT. Таблица GDT одна на все программы и доступна всем программам.

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

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

4.1. Задача и сегмент состояния задачи

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

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

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

Для хранения контекста неактивной в настоящей момент задачи процессор i80286 использует специальную область памяти, называемую сегментом состояния задачи TSS (Task State Segment). Формат TSS представлен на рис. 14.

Рис. 14. Формат сегмента состояния задачи TSS.

Сегмент TSS адресуется процессором при помощи 16-битного регистра TR (Task Register), содержащего селектор дескриптора TSS, находящегося в глобальной таблице дескрипторов GDT (рис. 15).

Рис. 15. Дескриптор сегмента состояния задачи TSS.

Поле доступа содержит бит B - бит занятости. Если задача активна, этот бит устанавливается процессором в 1.

Операционная система для каждой задачи создаёт свой TSS. Перед тем как переключиться на выполнение новой задачи, процессор сохраняет контекст старой задачи в её сегменте TSS.

Что же конкретно записывается в TSS при переключении задачи?

Записывается содержимое регистров общего назначения AX, BX, CX, DX, регистров SP, BP, SI, DI, сегментных регистров ES, CS, SS, DS, содержимое указателя команд IP и регистра флажков FLAGS. Кроме того, сохраняется содержимое регистра LDTR, определяющего локальное адресное пространство задачи.

Дополнительно при переключении задачи в область TSS со смещением 44 операционная система может записать любую информацию, которая относится к данной задаче. Эта область процессором не считывается и никак не модифицируется.

Поле Link представляет собой поле обратной связи и используется для организации вложенных вызовов задач. Это поле мы рассмотрим в следующем разделе.

Поля Stack 0, Stack 1, Stack 2 хранят логические адреса (селектор:смещение) отдельных для каждого кольца защиты стеков. Эти поля используются при межсегментных вызовах через вентили вызова.

Для обеспечения защиты данных процессор назначает отдельные стеки для каждого кольца защиты. Когда задача вызывает подпрограмму из другого кольца через вентиль вызова, процессор вначале загружает указатель стека SS:SP адресом нового стека, взятого из соответствующего поля TSS.

Затем в новый стек копируется содержимое регистров SS:SP задачи (т.е. адрес вершины старого стека задачи). После этого в новый стек копируются параметры, количество которых задано в вентиле вызова и адрес возврата.

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

Включение адресов стеков в TSS позволяет разделить стеки задач и обеспечивает их автоматическое переключение при переключении задач.

4.2. Переключение задач

Для переключения задач имеются следующие возможности:

  • переключение по команде JMP;
  • переключение по команде CALL;
  • переключение по прерыванию.

В первом и втором случаях для переключения задачи используются обычные команды JMP и CALL, но в качестве операнда в этих командах указывается адрес сегмента TSS задачи, на которую необходимо переключиться.

Если произошло переключение с первой задачи на вторую при помощи команды JMP, то для возврата к выполнению первой задачи необходимо вновь использовать JMP, указав в качестве операнда адрес TSS первой задачи.

Команда CALL позволяет организовать вызов вложенных задач. Переключившись из первой задачи на вторую, программа может вновь вернуться к первой задаче, если она выполнит команду IRET. В этом случае по команде IRET произойдет обратное переключение задач. Адрес TSS для возврата команда IRET возьмёт из поля обратной связи Link текущего сегмента TSS, куда он был записан командой CALL при первом переключении задач.

Кроме того, при переключении задачи командой CALL в поле FLAGS сегмента TSS вызванной задачи устанавливается в 1 бит вложенной задачи NT. Команда JMP, если она использована для переключения задачи, сбрасывает бит NT. Формат регистра флагов для процессоров i80386 и i80486 описан в приложении. Регистр флагов FLAGS процессора i80286 - это младшее слово 32-разрядного регистра EFLAGS.

Аналогично тому, как можно вызвать подпрограмму через вентиль вызова, для вызова задачи командой CALL можно использовать вентиль задачи. Формат вентиля задачи представлен на рис. 16.

Рис. 16. Вентиль задачи.

Вентили задач, вызываемых по команде CALL, могут располагаться в таблицах GDT или LDT.

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

Это происходит потому, что при переключении задачи в сегменте TSS записывается содержимое регистров CS:IP на момент переключения задачи. Если задача была вызвана при помощи команды CALL и возврат (обратное переключение) было выполнено по команде IRET, в TSS записывается адрес CS:IP, указывающий на следующую после IRET команду. Вы можете поместить там команду безусловного перехода JMP на начало задачи и таким образом зациклить задачу. После этого вызов задачи станет похож на вызов подпрограммы.

Существует ещё одна очень интересная возможность для переключения задач - переключение задач по прерыванию.

Эту возможность можно легко реализовать, если поместить вентиль задачи в дескрипторную таблицу прерываний IDT. Например, можно сделать отдельные задачи для обработки исключений или аппаратных прерываний. В последнем случае обработчикам аппаратных прерываний не нужно использовать стек прикладных задач, так как они будут иметь свой собственный стек.

4.3. Синхронизация задач и семафоры

Прежде чем мы приведём конкретный пример простейшей мультизадачной системы, расскажем немного о применении семафоров как средств синхронизации задач.

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

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

В простейшем случае для семафора определяются операции:

  • создание семафора;
  • уничтожение семафора;
  • сброс семафора;
  • сброс семафора;
  • ожидание, пока семафор не будет установлен.

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

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

Операции сброса и установки семафора заключаются в записи, соответственно, нуля и единицы в ячейки памяти, распределённые семафорам. Единственная особенность выполнения этих операций заключается в том, что они должны быть непрерываемыми, то есть на время выполнения этих операций необходимо запретить переключение задач. Так как в нашем случае задачи переключаются по прерываниям таймера, мы на время выполнения операций сброса и установки семафора запрещаем все прерывания при помощи команды CLI.

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

4.4. Пример мультизадачного монитора

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

Эта программа состоит из нескольких модулей, составленных на языках ассемблера и Си.

Первые два файла предназначены для определения используемых констант и структур данных.

 Листинг 4. Определение констант и структур для модулей,
                 составленных на языке ассемблера.
 
 Файл tos.inc
 -----------------------------------------------------------
 
 CMOS_PORT               equ     70h
 PORT_6845               equ     63h
 COLOR_PORT      equ     3d4h
 MONO_PORT               equ     3b4h
 STATUS_PORT     equ     64h
 SHUT_DOWN               equ     0feh
 INT_MASK_PORT   equ     21h
 VIRTUAL_MODE    equ     0001
 A20_PORT                equ     0d1h
 A20_ON          equ     0dfh
 A20_OFF         equ     0ddh
 EOI                     equ     20h
 MASTER8259A     equ     20h
 SLAVE8259A      equ     0a0h
 KBD_PORT_A      equ     60h
 KBD_PORT_B      equ     61h
 
 L_SHIFT         equ     0000000000000001b
 NL_SHIFT                equ     1111111111111110b
 R_SHIFT         equ     0000000000000010b
 NR_SHIFT                equ     1111111111111101b
 
 L_CTRL          equ     0000000000000100b
 NL_CTRL         equ     1111111111111011b
 R_CTRL          equ     0000000000001000b
 NR_CTRL         equ     1111111111110111b
 
 L_ALT           equ     0000000000010000b
 NL_ALT          equ     1111111111101111b
 R_ALT           equ     0000000000100000b
 NR_ALT          equ     1111111111011111b
 
 CAPS_LOCK               equ     0000000001000000b
 SCR_LOCK                equ     0000000010000000b
 NUM_LOCK                equ     0000000100000000b
 INSERT          equ     0000001000000000b
 
 
 STRUC   idtr_struc
         idt_len dw      0
         idt_low dw      0
         idt_hi  db      0
         rsrv    db      0
 ENDS            idtr_struc
 
 Листинг 5. Определение констант и структур для модулей,
                 составленных на языке Си.
 
 Файл tos.h
 -----------------------------------------------------------
 
 #define word unsigned int
 
 // Селекторы, определённые в GDT
 
 #define CODE_SELECTOR        0x08 // сегмент кода
 #define DATA_SELECTOR        0x10 // сегмент данных
 
 #define TASK_1_SELECTOR            0x18 // задача TASK_1
 #define TASK_2_SELECTOR            0x20 // задача TASK_2
 #define MAIN_TASK_SELECTOR      0x28 // главная задача
 
 #define VID_MEM_SELECTOR          0x30 // сегмент видеопамяти
 #define IDT_SELECTOR            0x38 // талица IDT
 
 #define KEYBIN_TASK_SELECTOR    0x40 // задача ввода с клавиатуры
 #define KEYB_TASK_SELECTOR       0x48 // задача обработки
                                                   // клавиатурного прерывания
 #define FLIP_TASK_SELECTOR       0x50 // задача FLIP_TASK
 
 // Байт доступа
 
 typedef struct {
          unsigned accessed   : 1;
          unsigned read_write : 1;
          unsigned conf_exp   : 1;
          unsigned code       : 1;
          unsigned xsystem    : 1;
          unsigned dpl         : 2;
          unsigned present    : 1;
          } ACCESS;
 
 // Структура дескриптора
 
 typedef struct descriptor {
         word limit;
         word base_lo;
         unsigned char base_hi;
         unsigned char type_dpl;
         unsigned reserved;
 } descriptor;
 
 // Структура вентиля вызова, задачи, прерывания,
 // исключения
 
 typedef struct gate {
         word offset;
         word selector;
         unsigned char count;
         unsigned char type_dpl;
         word reserved;
 } gate;
 
 // Структура сегмента состояния задачи TSS
 
 typedef struct tss {
         word link;      // поле обратной связи
 
         word sp0;               // указатель стека кольца 0
         word ss0;
         word sp1;               // указатель стека кольца 1
         word ss1;
         word sp2;               // указатель стека кольца 1
         word ss2;
 
         word ip;                // регистры процессора
         word flags;
         word ax;
         word cx;
         word dx;
         word bx;
         word sp;
         word bp;
         word si;
         word di;
         word es;
         word cs;
         word ss;
         word ds;
         word ldtr;
 } tss;
 
 // Размеры сегментов и структур
 
 #define TSS_SIZE            (sizeof(tss))
 #define DESCRIPTOR_SIZE     (sizeof(descriptor))
 #define GATE_SIZE           (sizeof(gate))
 #define IDT_SIZE            (sizeof(idt))
 
 // Физические адреса видеопамяти для цветного
 // и монохромного видеоадаптеров
 
 #define COLOR_VID_MEM       0xb8000L
 #define MONO_VID_MEM        0xb0000L
 
 // Видеоржеимы
 
 #define MONO_MODE           0x07 // монохромный
 #define BW_80_MODE          0x02 // монохромный, 80 символов
 #define COLOR_80_MODE       0x03 // цветной, 80 символов
 
 // Значения для поля доступа
 
 #define TYPE_CODE_DESCR     0x18
 #define TYPE_DATA_DESCR     0x10
 #define TYPE_TSS_DESCR      0x01
 #define TYPE_CALL_GATE      0x04
 #define TYPE_TASK_GATE      0x85
 #define TYPE_INTERRUPT_GATE 0x86
 #define TYPE_TRAP_GATE      0x87
 
 #define SEG_WRITABLE        0x02
 #define SEG_READABLE        0x02
 #define SEG_PRESENT_BIT     0x80
 
 // Константы для обработки аппаратных
 // прерываний
 
 #define EOI                             0x20
 #define MASTER8259A             0x20
 #define SLAVE8259A                      0xa0
 
 // Макро для формирования физического
 // адреса из компонент сегменоного адреса
 // и смещения
 
 #define MK_LIN_ADDR(seg,off) (((unsigned long)(seg))<<4)+(word)(off)
 
 // Тип указателя на функцию типа void без параметров
 
 typedef void (func_ptr)(void);
 
 
 
 
 

Файл tos.c (листинг 6) содержит основную программу, которая инициализирует процессор для работы в защищённом режиме и запускает все задачи. С помощью функции с названием Init_And_Protected_Mode_Entry() мы попадаем в защищённый режим и выводим сообщение на экран о том, что в главной задаче установлен защищённый режим. Регистр TR загружается селектором главной задачи при помощи функции load_task_register().

Сразу после этого программа переключается на выполнение задачи TASK_1. Эта задача просто выводит сообщение о своём запуске на экран и возвращает управление главной задаче. Цель этой процедуры - продемонстрировать процесс переключения задач с помощью команды JMP.

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

Начиная с этого момента главная задача разделяет процессорное время наравне с остальными задачами. Что же она делает?

Главная задача сбрасывает семафор с номером 0, вслед за чем ожидает его установку. Этот семафор устанавливается задачей, которая занимается вводом символов с клавиатуры и выводит на экран скан-коды клавиш, а также состояние переключающих клавиш. Как только окажется нажатой клавиша ESC, задача ввода символов с клавиатуры устанавливает семафор 0, что и приводит к завершению работы главной задачи.

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

 Листинг 6. Программа мультизадачного монитора.
 
 Файл tos.c
 -----------------------------------------------------------
 
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <dos.h>
 #include <conio.h>
 #include "tos.h"
 
 // --------------------------------
 // Определения вызываемых функций
 // --------------------------------
 
 void Init_And_Protected_Mode_Entry(void);
 
 void protected_mode(unsigned long gdt_ptr, unsigned int gdt_size,
                 word cseg, word dseg);
 
 word load_task_register(word tss_selector);
 void real_mode(void);
 void jump_to_task(word tss_selector);
 void load_idtr(unsigned long idt_ptr, word idt_size);
 void Keyb_int(void);
 void    Timer_int(void);
 void Int_30h_Entry(void);
 
 extern  word kb_getch(void);
 void            enable_interrupt(void);
 
 void    task1(void);
 void    task2(void);
 void            flipflop_task(void);
 void     keyb_task(void);
 
 void init_tss(tss *t, word cs, word ds,
                         unsigned char *sp, func_ptr ip);
 
 void init_gdt_descriptor(descriptor *descr, unsigned long base,
                         word limit, unsigned char type);
 
 void  exception_0(void); //{ prg_abort(0); }
 void  exception_1(void); //{ prg_abort(1); }
 void  exception_2(void); //{ prg_abort(2); }
 void  exception_3(void); //{ prg_abort(3); }
 void  exception_4(void); //{ prg_abort(4); }
 void  exception_5(void); //{ prg_abort(5); }
 void  exception_6(void); //{ prg_abort(6); }
 void  exception_7(void); //{ prg_abort(7); }
 void  exception_8(void); //{ prg_abort(8); }
 void  exception_9(void); //{ prg_abort(9); }
 void  exception_A(void); //{ prg_abort(0xA); }
 void  exception_B(void); //{ prg_abort(0xB); }
 void  exception_C(void); //{ prg_abort(0xC); }
 void  exception_D(void); //{ prg_abort(0xD); }
 void  exception_E(void); //{ prg_abort(0xE); }
 void  exception_F(void); //{ prg_abort(0xF); }
 void  exception_10(void); //{ prg_abort(0x10); }
 void  exception_11(void); //{ prg_abort(0x11); }
 void  exception_12(void); //{ prg_abort(0x12); }
 void  exception_13(void); //{ prg_abort(0x13); }
 void  exception_14(void); //{ prg_abort(0x14); }
 void  exception_15(void); //{ prg_abort(0x15); }
 void  exception_16(void); //{ prg_abort(0x16); }
 void  exception_17(void); //{ prg_abort(0x17); }
 void  exception_18(void); //{ prg_abort(0x18); }
 void  exception_19(void); //{ prg_abort(0x19); }
 void  exception_1A(void); //{ prg_abort(0x1A); }
 void  exception_1B(void); //{ prg_abort(0x1B); }
 void  exception_1C(void); //{ prg_abort(0x1C); }
 void  exception_1D(void); //{ prg_abort(0x1D); }
 void  exception_1E(void); //{ prg_abort(0x1E); }
 void  exception_1F(void); //{ prg_abort(0x1F); }
 
 void iret0(void);
 void iret1(void);
 
 // --------------------------------------
 // Глобальная таблица дескрипторов GDT
 // --------------------------------------
 
 descriptor      gdt[11];
 
 // --------------------------------------
 // Дескрипторная таблица прерываний IDT
 // --------------------------------------
 
 gate            idt[] = {
 
 // Обработчики исключений
 
  { (word)&exception_0, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 0
  { (word)&exception_1, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 1
  { (word)&exception_2, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 2
  { (word)&exception_3, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 3
  { (word)&exception_4, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 4
  { (word)&exception_5, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 5
  { (word)&exception_6, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 6
  { (word)&exception_7, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 7
  { (word)&exception_8, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 8
  { (word)&exception_9, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 9
  { (word)&exception_A, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // A
  { (word)&exception_B, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // B
  { (word)&exception_C, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // C
  { (word)&exception_D, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // D
  { (word)&exception_E, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // E
  { (word)&exception_F, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // F
  { (word)&exception_10, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 10
  { (word)&exception_11, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 11
  { (word)&exception_12, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 12
  { (word)&exception_13, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 13
  { (word)&exception_14, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 14
  { (word)&exception_15, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 15
  { (word)&exception_16, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 16
  { (word)&exception_17, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 17
  { (word)&exception_18, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 18
  { (word)&exception_19, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 19
  { (word)&exception_1A, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 1A
  { (word)&exception_1B, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 1B
  { (word)&exception_1C, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 1C
  { (word)&exception_1D, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 1D
  { (word)&exception_1E, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 1E
  { (word)&exception_1F, CODE_SELECTOR, 0, TYPE_TRAP_GATE, 0 }, // 1F
 
 // Обработчик прерываний таймера
 
  { (word)&Timer_int, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 20
 
 // Вентиль задачи, запускающейся по прерыванию от клавиатуры
 
  { 0, KEYB_TASK_SELECTOR, 0, TYPE_TASK_GATE, 0 }, // 21
 
 // Заглушки для остальных аппаратных прерываний
 
  { (word)&iret0, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 22
  { (word)&iret0, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 23
  { (word)&iret0, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 24
  { (word)&iret0, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 25
  { (word)&iret0, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 26
  { (word)&iret0, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 27
 
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 28
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 29
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 2A
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 2B
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 2C
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 2D
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 2E
  { (word)&iret1, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 }, // 2F
 
 // Обработчик для программного прерывания, которое
 // используется для ввода с клавиатуры
 
  { (word)&Int_30h_Entry, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0 },  // 30
 
 // Вентиль задачи FLIP_TASK
 
  { 0, FLIP_TASK_SELECTOR, 0, TYPE_TASK_GATE, 0 } // 31
 
 };
 
 // -------------------------------------------
 // Сегменты TSS для различных задач
 // -------------------------------------------
 
 tss main_tss;           // TSS главной задачи
 tss task_1_tss; // TSS задачи TASK_1
 tss task_2_tss;     // TSS задачи TASK_2
 tss keyb_task_tss;  // TSS задач обслуживания
 tss keyb_tss;           //              клавиатуры
 tss flipflop_tss;       // TSS задачи FLIP_TASK
 
 // -------------------------------------------
 // Стеки для задач
 // -------------------------------------------
 
 unsigned char task_1_stack[1024];
 unsigned char task_2_stack[1024];
 unsigned char keyb_task_stack[1024];
 unsigned char keyb_stack[1024];
 unsigned char flipflop_stack[1024];
 
 word y=0;       // номер текущей строки для вывода на экран
 
 
 
 // -------------------------------------------
 // Начало программы
 // -------------------------------------------
 
 void main(void) {
 
 // Стираем экран
 
         textcolor(BLACK); textbackground(LIGHTGRAY); clrscr();
 
 // Входим в защищённый режим
 
         Init_And_Protected_Mode_Entry();
 
 // Выводим сообщение
 
         vi_hello_msg();
         y=3;
         vi_print(0, y++,
          " Установлен защищённый режим в главной задаче", 0x7f);
 
 // Загружаем регистр TR селектором главной задачи
 // т.е. задачи main()
 
         load_task_register(MAIN_TASK_SELECTOR);
 
 // Переключаемся на задачу TASK_1
 
         jump_to_task(TASK_1_SELECTOR);
 
 // После возврата в главную задачу выдаём сообщение
 
         vi_print(0, y++ ," Вернулись в главную задачу", 0x7f);
         y++;
 
 // Запускаем планировщик задач
 
         vi_print(0, y++ ," Запущен планировщик задач", 0x70);
         enable_interrupt(); // разрешаем прерывание таймера
 
 // Ожидаем установки семафора с номером 0. После того,
 // как этот семафор окажется установлен, возвращаемся
 // в реальный режим.
 
 // Семафор 0 устанавливается задачей, обрабатывающей ввод с
 // клавиатуры, которая работает независимо от
 // главной задаче.
 
         vi_print(0, y++ ," Для возврата в реальный режим нажмите ESC", 0x70);
 
         sem_clear(0); // сброс семафора 0
         sem_wait(0);  // ожидание установки семафора 0
 
 // Возврат в реальный режим, стирание экрана и
 // передача управления MS-DOS
 
         real_mode();
         textcolor(WHITE); textbackground(BLACK); clrscr();
 }
 
 // -----------------------------------
 // Функция инициализации сегмента TSS
 // -----------------------------------
 
 void init_tss(tss *t, word cs, word ds,
         unsigned char *sp, func_ptr ip) {
 
         t->cs = cs;          // селектор сегмента кода
         t->ds = ds;          // поля ds, es, ss устанавливаем
         t->es = ds;          // на сегмент данных
         t->ss = ds;
         t->ip = (word)ip;    // указатель команд
         t->sp = (word)sp;    // смещение стека
         t->bp = (word)sp;
 }
 
 // -------------------------------------------------
 // Функция инициализации дескриптора в таблице GDT
 // -------------------------------------------------
 
 void init_gdt_descriptor(descriptor *descr,
                 unsigned long base, word limit,
                 unsigned char type) {
 
 // Младшее слово базового адреса
         descr->base_lo  = (word)base;
 
 // Старший байт базового адреса
         descr->base_hi  = (unsigned char)(base >> 16);
 
 // Поле доступа дескриптора
         descr->type_dpl = type;
 
 // Предел
         descr->limit    = limit;
 
 // Зарезервированное поле, должно быть
 // сброшено в 0
         descr->reserved = 0;
 }
 
 // -----------------------------------------------
 // Инициализация всех таблиц и вход
 // в защищённый режим
 // -----------------------------------------------
 
 void Init_And_Protected_Mode_Entry(void) {
 
         union REGS r;
 
 // Инициализируем таблицу GDT, элементы с 1 по 5
 
         init_gdt_descriptor(&gdt[1], MK_LIN_ADDR(_CS, 0),
          0xffffL, TYPE_CODE_DESCR | SEG_PRESENT_BIT | SEG_READABLE);
 
         init_gdt_descriptor(&gdt[2], MK_LIN_ADDR(_DS, 0),
          0xffffL, TYPE_DATA_DESCR | SEG_PRESENT_BIT | SEG_WRITABLE);
 
         init_gdt_descriptor(&gdt[3],
          MK_LIN_ADDR(_DS, &task_1_tss),
          (unsigned long)TSS_SIZE-1, TYPE_TSS_DESCR | SEG_PRESENT_BIT);
 
         init_gdt_descriptor(&gdt[4],
          MK_LIN_ADDR(_DS, &task_2_tss),
          (unsigned long)TSS_SIZE-1, TYPE_TSS_DESCR | SEG_PRESENT_BIT);
 
         init_gdt_descriptor(&gdt[5],
          MK_LIN_ADDR(_DS, &main_tss),
          (unsigned long)TSS_SIZE-1, TYPE_TSS_DESCR | SEG_PRESENT_BIT);
 
 
 // Инициализируем TSS для задач TASK_1, TASK_2
 
         init_tss(&task_1_tss, CODE_SELECTOR, DATA_SELECTOR, task_1_stack+
                 sizeof(task_1_stack), task1);
 
         init_tss(&task_2_tss, CODE_SELECTOR, DATA_SELECTOR, task_2_stack+
                 sizeof(task_2_stack), task2);
 
 // Инициализируем элемент 6 таблицы GDT -
 // дескриптор для сегмента видеопамяти
 
 // Определяем видеорежим
         r.h.ah=15;
         int86(0x10,&r,&r);
 
 // Инициализация для монохромного режима
 
         if(r.h.al==MONO_MODE)
           init_gdt_descriptor(&gdt[6], MONO_VID_MEM,
           3999, TYPE_DATA_DESCR | SEG_PRESENT_BIT | SEG_WRITABLE);
 
 // Инициализация для цветного режима
 
         else if(r.h.al == BW_80_MODE || r.h.al == COLOR_80_MODE)
           init_gdt_descriptor(&gdt[6], COLOR_VID_MEM,
           3999, TYPE_DATA_DESCR | SEG_PRESENT_BIT | SEG_WRITABLE);
         else {
                 printf("\nИзвините, этот видеорежим недопустим.");
                 exit(-1);
         }
 
 // Инициализация элементов 7 и 8 таблицы GDT
 
         init_gdt_descriptor(&gdt[7],
                 MK_LIN_ADDR(_DS, &idt),
                 (unsigned long)IDT_SIZE-1,
                 TYPE_DATA_DESCR | SEG_PRESENT_BIT | SEG_WRITABLE);
 
         init_gdt_descriptor(&gdt[8],
                 MK_LIN_ADDR(_DS, &keyb_task_tss),
                 (unsigned long)TSS_SIZE-1,
                 TYPE_TSS_DESCR | SEG_PRESENT_BIT);
 
 // Инициализация TSS для задачи KEYB_TASK
 
         init_tss(&keyb_task_tss, CODE_SELECTOR, DATA_SELECTOR,
                 keyb_task_stack + sizeof(keyb_task_stack), keyb_task);
 
 // Инициализация элемента 9 таблицы GDT
 
         init_gdt_descriptor(&gdt[9],
                 MK_LIN_ADDR(_DS, &keyb_tss),
                 (unsigned long)TSS_SIZE-1,
                 TYPE_TSS_DESCR | SEG_PRESENT_BIT);
 
 // Инициализация TSS для задачи KEYB обработки ввода с клавиатуры
 
         init_tss(&keyb_tss, CODE_SELECTOR, DATA_SELECTOR,
                 keyb_stack + sizeof(keyb_stack), Keyb_int);
 
 
 // Инициализация элемента 10 таблицы GDT
 
         init_gdt_descriptor(&gdt[10],
                 MK_LIN_ADDR(_DS, &flipflop_tss),
                 (unsigned long)TSS_SIZE-1,
                 TYPE_TSS_DESCR | SEG_PRESENT_BIT);
 
 // Инициализация TSS для задачи FLIP_TASK
 
         init_tss(&flipflop_tss, CODE_SELECTOR, DATA_SELECTOR,
                 flipflop_stack + sizeof(flipflop_stack), flipflop_task);
 
 // Загрузка регистра IDTR
 
         load_idtr(MK_LIN_ADDR(_DS, &idt), IDT_SIZE);
 
 // Вход в защищённый режим
 
         protected_mode(MK_LIN_ADDR(_DS, &gdt), sizeof(gdt),
                         CODE_SELECTOR, DATA_SELECTOR);
 }
 
 
 
 
 

Файл tasks.c содержит тексты программ, которые будут работать в режиме разделения времени (кроме задачи TASK_1, эта задача запускается только один раз).

Задача TASK_1 (процедура task1) выдаёт сообщение о своём запуске и передаёт управление главной задаче.

Задача TASK_2 (процедура task2) попеременно выводит на экран строки "FLIP" и "FLOP", переключая попутно семафор с номером 1.

Задача FLIP_TASK (процедура flipflop_task) также попеременно выводит на экран строки "FLIP" и "FLOP", но только тогда, когда семафор с номером 1 установлен. Таким образом, задача TASK_2 управляет работой задачи FLIP_TASK.

Задача KEYB_TASK (процедура keyb_task) вводит символы с клавиатуры и выводит скан-коды нажатых клавиш, а также состояние переключающих клавиш. Как только оказывается нажатой клавиша ESC, задача устанавливает семафор с номером 0, что приводит к завершению работы главной задачи (ожидающей установки этого семафора).

 Листинг 7. Задачи, которые будут работать параллельно.
 
 Файл tasks.c
 -----------------------------------------------------------
 
 #include <stdio.h>
 #include <dos.h>
 #include <conio.h>
 #include <stdlib.h>
 #include "tos.h"
 
 word dispatcher(void);
 
 // Номер текущей строки для вывода на экран
 
 extern unsigned int y;
 
 // Задача TASK_1
 
 void task1(void) {
         while(1){
                 vi_print(0,y++," Запущена задача TASK_1, "
                                         "переходим к главной задаче", 0x70);
                 jump_to_task(MAIN_TASK_SELECTOR);
 
 // После повторного запуска этой задачи
 // снова входим в цикл.
 
         }
 }
 
 // Задача TASK_2
 
 word flipflop1 = 0;
 long delay_cnt1 = 0l;
 
 void task2(void) {
         while(1){
 
 // Периодически выводим на экран строки
 // FLIP/FLOP, каждый раз переключая
 // семафор номер 1. Этот семафор однозначно
 // соответствует выведенной на экран строке.
 
                 asm sti
                 if(delay_cnt1 > 150000l ) {
                         asm cli
                         if(flipflop1)   {
                                 vi_print(73,3," FLIP ", 0x4f);
                                 sem_clear(1);
                         }
                         else  {
                                 vi_print(73,3," FLOP ", 0x1f);
                                 sem_set(1);
                         }
                         flipflop1 ^= 1;
                         delay_cnt1 = 0l;
                         asm sti
                 }
                 delay_cnt1++;
         }
 }
 
 word flipflop = 0;
 long delay_cnt = 0l;
 
 void flipflop_task(void) {
 
 // Эта задача также периодически выводит на экран
 // строки FLIP/FLOP, но выводит строкой выше и
 // с меньшим периодом. Кроме того, эта задача
 // работает только тогда, когда установлен
 // семафор номер 1.
 
         while(1){
                 asm sti
                 if(delay_cnt > 20000l ) {
                         sem_wait(1); // ожидаем установки семафора
                         asm cli
                         if(flipflop)    vi_print(73,2," FLIP ", 0x20);
                         else            vi_print(73,2," FLOP ", 0x20);
                         flipflop ^= 1;
                         delay_cnt = 0l;
                         asm sti
                 }
                 delay_cnt++;
         }
 }
 
 word keyb_code;
 
 extern word keyb_status;
 
 void keyb_task(void) {
 
 // Эта задача вводит символы с клавиатуры
 // и отображает скан-коды нажатых клавиш
 // и состояние переключающих клавиш на экране.
 // Если нажимается клавиша ESC, задача
 // устанавливает семафор номер 0.
 // Работающая параллельно главная задача
 // ожидает установку этого семафора. Как только
 // семафор 0 окажется установлен, главная задача
 // завершает свою работу и программа возвращает
 // процессор в реальный режим, затем передаёт
 // управление MS-DOS.
 
 
         vi_print(60, 5, " Key code:   .... ", 0x20);
         vi_print(60, 6, " Key status: .... ", 0x20);
         while(1){
                 keyb_code = kb_getch();
                 vi_put_word(73, 5, keyb_code, 0x4f);
                 vi_put_word(73, 6, keyb_status, 0x4f);
                 if((keyb_code & 0x00ff) == 1) sem_set(0);
         }
 }
 
 
 
 
 

Файл semaphor.c содержит исходные тексты процедур сброса семафора, установки семафора и ожидания семафора.

В массиве semaphore[5] определено пять семафоров. Разумеется, что когда вы будете экспериментировать с программой, вы можете изменить количество доступных семафоров.

 Листинг 8. Процедуры для работы с семафорами.
 
 Файл semaphor.c
 -----------------------------------------------------------
 
 #include <stdio.h>
 #include <dos.h>
 #include <conio.h>
 #include <stdlib.h>
 #include "tos.h"
 
 // Массив из пяти семафоров
 
 word semaphore[5];
 
 // Процедура сброса семафора.
 // Параметр sem - номер сбрасываемого семафора
 
 void sem_clear(int sem) {
         asm cli
         semaphore[sem] = 0;
         asm sti
 }
 
 // Процедура установки семафора
 // Параметр sem - номер устанавливаемого семафора
 
 void sem_set(int sem) {
         asm cli
         semaphore[sem] = 1;
         asm sti
 }
 
 // Ожидание установки семафора
 // Параметр sem - номер ожидаемого семафора
 
 void sem_wait(int sem) {
         while(1) {
                 asm cli
                 if(semaphore[sem]) break; // проверяем семафор
 
                 asm sti // ожидаем установки семафора
                 asm nop
                 asm nop
         }
         asm sti
 }
 
 
 
 
 

Файл timer.c содержит обработчик аппаратного прерывания таймера, который периодически выдаёт звуковой сигнал и инициирует работу диспетчера задач. Диспетчер задач циклически перебирает селекторы TSS задач, участвующих в процессе разделения времени, возвращая селектор той задачи, которая должна стать активной. В самом конце обработки аппаратного прерывания таймера происходит переключение именно на эту задачу.

 Листинг 9. Процедуры для работы с таймером и 
                 диспетчер задач.
 
 Файл timer.c
 -----------------------------------------------------------
 
 
 #include <stdio.h>
 #include <dos.h>
 #include <conio.h>
 #include <stdlib.h>
 #include "tos.h"
 
 // -------------------------------------------
 //      Модуль обслуживания таймера
 // -------------------------------------------
 
 #define EOI 0x20
 #define MASTER8259A 0x20
 
 extern void beep(void);
 extern void flipflop_task(void);
 void Timer_int(void);
 word dispatcher(void);
 
 word    timer_cnt;
 
 // ------------------------------------------
 // Обработчик аппаратного прерывания таймера
 // ------------------------------------------
 
 void Timer_int(void) {
 
         asm pop bp
 
 // Периодически выдаём звуковой сигнал
 
         timer_cnt += 1;
         if((timer_cnt & 0xf) == 0xf) {
                 beep();
         }
 
 // Выдаём в контроллер команду конца
 // прерывания
 
         asm mov al,EOI
         asm out MASTER8259A,al
 
 // Переключаемся на следующую задачу,
 // селектор TSS которой получаем от
 // диспетчера задач dispatcher()
 
         jump_to_task(dispatcher());
         asm iret
 }
 
 // --------------------------------------
 // Диспетчер задач
 // --------------------------------------
 
 // Массив селекторов, указывающих на TSS
 // задач, участвующих в параллельной работе,
 // т.е. диспетчеризуемых задач
 
 word task_list[] = {
         MAIN_TASK_SELECTOR,
         FLIP_TASK_SELECTOR,
         KEYBIN_TASK_SELECTOR,
         TASK_2_SELECTOR
 };
 
 word current_task = 0; // текущая задача
 word max_task = 3;     // количество задач - 1
 
 // Используем простейший алгоритм диспетчеризации -
 // выполняем последовательное переключение на все
 // задачи, селекторы TSS которых находятся
 // в массиве task_list[].
 
 word dispatcher(void) {
 
         if(current_task < max_task) current_task++;
         else current_task = 0;
         return(task_list[current_task]);
 }
 
 
 
 
 

Для сокращения объёма и без того сложной программы мы не стали делать функционально полную обработку исключений, ограничившись простым аварийным завершением работы программы с выдачей номера исключения.

Исходные тексты обработчиков исключений находятся в файле except.c.

 Листинг 10. Обработка исключений.
 
 Файл except.c
 -----------------------------------------------------------
 
 #include <stdio.h>
 #include <dos.h>
 #include <conio.h>
 #include <stdlib.h>
 #include "tos.h"
 
 void prg_abort(int err);
 
 // Номер текущей строки для вывода на экран
 
 extern unsigned int y;
 
 // Обработчики исключений
 
 void  exception_0(void) { prg_abort(0); }
 void  exception_1(void) { prg_abort(1); }
 void  exception_2(void) { prg_abort(2); }
 void  exception_3(void) { prg_abort(3); }
 void  exception_4(void) { prg_abort(4); }
 void  exception_5(void) { prg_abort(5); }
 void  exception_6(void) { prg_abort(6); }
 void  exception_7(void) { prg_abort(7); }
 void  exception_8(void) { prg_abort(8); }
 void  exception_9(void) { prg_abort(9); }
 void  exception_A(void) { prg_abort(0xA); }
 void  exception_B(void) { prg_abort(0xB); }
 void  exception_C(void) { prg_abort(0xC); }
 void  exception_D(void) { prg_abort(0xD); }
 void  exception_E(void) { prg_abort(0xE); }
 void  exception_F(void) { prg_abort(0xF); }
 void  exception_10(void) { prg_abort(0x10); }
 void  exception_11(void) { prg_abort(0x11); }
 void  exception_12(void) { prg_abort(0x12); }
 void  exception_13(void) { prg_abort(0x13); }
 void  exception_14(void) { prg_abort(0x14); }
 void  exception_15(void) { prg_abort(0x15); }
 void  exception_16(void) { prg_abort(0x16); }
 void  exception_17(void) { prg_abort(0x17); }
 void  exception_18(void) { prg_abort(0x18); }
 void  exception_19(void) { prg_abort(0x19); }
 void  exception_1A(void) { prg_abort(0x1A); }
 void  exception_1B(void) { prg_abort(0x1B); }
 void  exception_1C(void) { prg_abort(0x1C); }
 void  exception_1D(void) { prg_abort(0x1D); }
 void  exception_1E(void) { prg_abort(0x1E); }
 void  exception_1F(void) { prg_abort(0x1F); }
 
 // ------------------------------
 // Аварийный выход из программы
 // ------------------------------
 
 void prg_abort(int err) {
 
         vi_print(1,y++,"!!! ---> Произошло исключение", 0xc);
 
         real_mode(); // Возвращаемся в реальный режим
 
 // В реальном режиме выводим сообщение об исключении
 
         gotoxy(1, ++y);
         cprintf(" Исключение %X, нажмите любую клавишу", err);
         getch();
 
         textcolor(WHITE);
         textbackground(BLACK);
         clrscr();
         exit(0);
 
 }
 
 
 
 
 

В файле intproc.c расположены заглушки для тех аппаратных прерываний, обработка которых сводится к простой посылке кода конца прерывания в соответствующий контроллер прерывания.

 Листинг 11. Заглушки для аппаратных прерываний.
 
 Файл intproc.c
 -----------------------------------------------------------
 
 
 #include <stdio.h>
 #include <dos.h>
 #include <conio.h>
 #include <stdlib.h>
 #include "tos.h"
 
 // Заглушки для необрабатываемых
 // аппаратных прерываний.
 
 void iret0(void) {     // первый контроллер прерываний
         asm {
                 push    ax
                 mov     al,EOI
                 out     MASTER8259A,al
                 pop     ax
                 pop bp
                 iret
         }
 }
 
 void iret1(void) {     // второй контроллер прерываний
         asm {
                 push    ax
                 mov     al,EOI
                 out     MASTER8259A,al
                 out     SLAVE8259A,al
                 pop     ax
                 pop bp
                 iret
         }
 }
 
 
 
 
 

Файл keyb.c содержит простой интерфейс для вызова программного прерывания int 30h, обеспечивающего ввод с клавиатуры.

 Листинг 12. Ввод символа с клавиатуры.
 
 Файл keyb.c
 -----------------------------------------------------------
 
 
 #include <stdio.h>
 #include <dos.h>
 #include <conio.h>
 #include <stdlib.h>
 #include "tos.h"
 
 extern word key_code;
 
 // Функция, ожидающая нажатия любой
 // клавиши и возвращающая её скан-код
 
 unsigned int kb_getch(void) {
         asm int 30h
         return(key_code);
 }
 
 
 
 
 

Обработчик аппаратного прерывания клавиатуры мы взяли практически без изменений из программы, представленной в предыдущей главе. Исходные тексты находятся в файле keyboard.asm.

 Листинг 13. Процедуры для работы с клавиатурой.
 
 Файл keyboard.asm
 -----------------------------------------------------------
 
 IDEAL
 
 MODEL SMALL
 RADIX   16
 
 P286
 include "tos.inc"
 
 ; ------------------------------------------
 ; Модуль обслуживания клавиатуры
 ; ------------------------------------------
 
 PUBLIC  _Keyb_int, _Int_30h_Entry, _key_code, _keyb_status
 EXTRN   _beep:PROC
 DATASEG
 
         _key_flag       db      0
         _key_code       dw      0
         ext_scan        db      0
         _keyb_status    dw      0
 
 CODESEG
 
 PROC    _Keyb_int        NEAR
         cli
 
         call    _beep
 
         push    ax
         mov     al, [ext_scan]
         cmp     al, 0
         jz      normal_scan1
         cmp     al, 0e1h
         jz      pause_key
 
         in      al, 60h
         cmp     al, 2ah
         jz      intkeyb_exit_1
         cmp     al, 0aah
         jz      intkeyb_exit_1
 
         mov     ah, [ext_scan]
         call    Keyb_PutQ
 
         mov     al, 0
         mov     [ext_scan], al
         jmp     intkeyb_exit
 
 pause_key:
 
         in      al, 60h
         cmp     al, 0c5h
         jz      pause_key1
         cmp     al, 45h
         jz      pause_key1
 
         jmp     intkeyb_exit
 
 pause_key1:
         mov     ah, [ext_scan]
         call    Keyb_PutQ
 
         mov     al, 0
         mov     [ext_scan], al
         jmp     intkeyb_exit
 
 
 normal_scan1:
         in      al, 60h
         cmp     al, 0feh
         jz      intkeyb_exit
         cmp     al, 0e1h
         jz      ext_key
         cmp     al, 0e0h
         jnz     normal_scan
 
 ext_key:
         mov     [ext_scan], al
         jmp     intkeyb_exit
 
 
 intkeyb_exit_1:
         mov     al, 0
         mov     [ext_scan], al
         jmp     intkeyb_exit
 
 normal_scan:
         mov     ah, 0
         call    Keyb_PutQ
 
 intkeyb_exit:
         in      al, 61h
         mov     ah, al
         or      al, 80h
         out     61h, al
         xchg    ah, al
         out     61h, al
         mov     al,EOI
         out     MASTER8259A,al
 
         pop     ax
         sti
         iret
         jmp     _Keyb_int
 ENDP    _Keyb_int
 
 
 PROC    Keyb_PutQ       NEAR
 
         push    ax
 
         cmp     ax, 002ah       ; L_SHIFT down
         jnz     @@kb1
         mov     ax, [_keyb_status]
         or      ax, L_SHIFT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb1:
         cmp     ax, 00aah       ; L_SHIFT up
         jnz     @@kb2
         mov     ax, [_keyb_status]
         and     ax, NL_SHIFT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb2:
         cmp     ax, 0036h       ; R_SHIFT down
         jnz     @@kb3
         mov     ax, [_keyb_status]
         or      ax, R_SHIFT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb3:
         cmp     ax, 00b6h       ; R_SHIFT up
         jnz     @@kb4
         mov     ax, [_keyb_status]
         and     ax, NR_SHIFT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb4:
         cmp     ax, 001dh       ; L_CTRL down
         jnz     @@kb5
         mov     ax, [_keyb_status]
         or      ax, L_CTRL
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb5:
         cmp     ax, 009dh       ; L_CTRL up
         jnz     @@kb6
         mov     ax, [_keyb_status]
         and     ax, NL_CTRL
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb6:
         cmp     ax, 0e01dh      ; R_CTRL down
         jnz     @@kb7
         mov     ax, [_keyb_status]
         or      ax, R_CTRL
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb7:
         cmp     ax, 0e09dh      ; R_CTRL up
         jnz     @@kb8
         mov     ax, [_keyb_status]
         and     ax, NR_CTRL
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb8:
         cmp     ax, 0038h       ; L_ALT down
         jnz     @@kb9
         mov     ax, [_keyb_status]
         or      ax, L_ALT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb9:
         cmp     ax, 00b8h       ; L_ALT up
         jnz     @@kb10
         mov     ax, [_keyb_status]
         and     ax, NL_ALT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb10:
         cmp     ax, 0e038h      ; R_ALT down
         jnz     @@kb11
         mov     ax, [_keyb_status]
         or      ax, R_ALT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb11:
         cmp     ax, 0e0b8h      ; R_ALT up
         jnz     @@kb12
         mov     ax, [_keyb_status]
         and     ax, NR_ALT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb12:
         cmp     ax, 003ah       ; CAPS_LOCK up
         jnz     @@kb13
         mov     ax, [_keyb_status]
         xor     ax, CAPS_LOCK
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb13:
         cmp     ax, 00bah       ; CAPS_LOCK down
         jnz     @@kb14
         jmp     keyb_putq_exit
 @@kb14:
         cmp     ax, 0046h       ; SCR_LOCK up
         jnz     @@kb15
         mov     ax, [_keyb_status]
         xor     ax, SCR_LOCK
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb15:
         cmp     ax, 00c6h       ; SCR_LOCK down
         jnz     @@kb16
         jmp     keyb_putq_exit
 @@kb16:
         cmp     ax, 0045h       ; NUM_LOCK up
         jnz     @@kb17
         mov     ax, [_keyb_status]
         xor     ax, NUM_LOCK
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb17:
         cmp     ax, 00c5h       ; NUM_LOCK down
         jnz     @@kb18
         jmp     keyb_putq_exit
 @@kb18:
         cmp     ax, 0e052h      ; INSERT up
         jnz     @@kb19
         mov     ax, [_keyb_status]
         xor     ax, INSERT
         mov     [_keyb_status], ax
         jmp     keyb_putq_exit
 @@kb19:
         cmp     ax, 0e0d2h      ; INSERT down
         jnz     @@kb20
         jmp     keyb_putq_exit
 @@kb20:
 
         test    ax, 0080h
         jnz     keyb_putq_exit
 
         mov     [_key_code], ax
 
         mov     al, 0ffh
         mov     [_key_flag], al
 keyb_putq_exit:
         pop     ax
         ret
 ENDP    Keyb_PutQ
 
 ; Обработчик программного прерывания
 ; для ввода с клавиатуры. По своим функциям
 ; напоминает прерывание INT 16 реального
 ; режима.
 
 
 PROC    _Int_30h_Entry   NEAR
         push   ax dx
 
 ; Ожидаем прерывание от клавиатуры
 
 keyb_int_wait:
         sti
         nop
         nop
         cli
 
 ; Проверяем флаг, который устанавливается
 ; обработчиком аппаратного прерывания клавиатуры
 
         mov     al, [_key_flag]
         cmp     al, 0
         jz      keyb_int_wait
 
 ; Сбрасываем флаг после прихода прерывания
 
         mov     al, 0
         mov     [_key_flag], al
         sti
         pop     dx ax
         iret
 ENDP   _Int_30h_Entry
 
 END
 
 
 
 
 

Файл screen.c содержит процедуры, необходимые для вывода информации на экран дисплея. Работа этих процедур основана на непосредственной записи данных в видеопамять.

 Листинг 14. Процедуры для работы с видеоадаптером.
 
 Файл screen.c
 -----------------------------------------------------------
 
 #include <stdio.h>
 #include <dos.h>
 #include <conio.h>
 #include <stdlib.h>
 #include "tos.h"
 
 void vi_putch(unsigned int x, unsigned int y ,char c, char attr);
 
 char hex_tabl[] = "0123456789ABCDEF";
 
 // Вывод байта на экран, координаты (x,y),
 // выводится шестнадцатеричное представление
 // байта chr с экранными атрибутами attr.
 
 void vi_put_byte(unsigned int x,
                 unsigned int y, unsigned char chr, char attr) {
         unsigned char temp;
 
         temp = hex_tabl[(chr & 0xf0) >> 4];
         vi_putch(x, y, temp, attr);
 
         temp = hex_tabl[chr & 0xf];
         vi_putch(x+1, y, temp, attr);
 }
 // Вывод слова на экран, координаты (x,y),
 // выводится шестнадцатеричное представление
 // слова chr с экранными атрибутами attr.
 
 void vi_put_word(unsigned int x,
         unsigned int y, word chr, char attr) {
 
         vi_put_byte(x, y, (chr & 0xff00) >> 8, attr);
         vi_put_byte(x+2, y, chr & 0xff, attr);
 
 }
 
 // Вывод символа c на экран, координаты - (x,y),
 // атрибут выводимого символа - attr
 
 void vi_putch(unsigned int x,
         unsigned int y ,char c, char attr) {
 
         register unsigned int offset;
         char far *vid_ptr;
 
         offset=(y*160) + (x*2);
         vid_ptr=MK_FP(VID_MEM_SELECTOR, offset);
         *vid_ptr++=c; *vid_ptr=attr;
 }
 
 // Вывод строки s на экран, координаты - (x,y),
 // атрибут выводимой строки - attr
 
 void vi_print(unsigned int x,
         unsigned int y, char *s, char attr) {
         while(*s) vi_putch(x++, y, *s++, attr);
 }
 
 // Вывод стоки сообщения о запуске программы
 
 void vi_hello_msg(void) {
 
         vi_print(0, 0,
                 " Protected mode monitor *TINY/OS*, "
                 "v.1.2 for CPU 80286  ¦ © Frolov A.V., 1992 ", 0x30);
 
 }
 
 
 
 
 

Последний файл - tossyst.asm - содержит уже знакомые вам процедуры для входа в защищённый режим и возврата обратно в реальный режим. Обратите внимание на процедуры _load_task_register и _jump_to_task, выполняющие загрузку регистра задачи TR и переключение на другую задачу соответственно.

 Листинг 15. Процедуры для инициализации, перехода в
                 защищённый режим и возврата в реальный режим,
                 для загрузки регистра TR и переключения задач.
 
 Файл tossyst.asm
 -----------------------------------------------------------
 
 IDEAL
 MODEL SMALL
 RADIX   16
 P286
 
         DATASEG
 
         include "tos.inc"
 
         PUBLIC  _beep
 
 ; Область памяти для инициализации IDTR
 
         idtr                    idtr_struc <,,,0>
 
 ; Область памяти для инициализации GDTR
 
         gdt_ptr         dw  (8*15)-1  ; размер GDT, 15 элементов
         gdt_ptr2                dw  ?
         gdt_ptr4                dw  ?
 
 ; Область памяти для записи селектора задачи,
 ; на которую будет происходить переключение
 
         new_task                dw  00h
         new_select       dw  00h
 
 ; Область памяти для хранения регистров,
 ; используется для возврата в реальный режим
 
         real_ss         dw      ?
         real_sp         dw      ?
         real_es         dw      ?
 
         protect_sel     dw      ?
 
         init_tss                dw      ?
 
 CODESEG
 
         PUBLIC  _real_mode,_protected_mode,_jump_to_task
         PUBLIC  _load_task_register, _load_idtr, _enable_interrupt
 
 ; -------------------------------------------------------------------
 ; Процедура для переключения в защищённый режим.
 ; Прототип для вызова:
 ;  void protected_mode(unsigned long gdt_ptr, unsigned int gdt_size,
 ;                unsigned int cseg, unsigned int dseg)
 ; -------------------------------------------------------------------
 
 PROC _protected_mode NEAR
                 push    bp
                 mov     bp,sp
 
 ; Параметр gdt_ptr
 
                 mov     ax,[bp+4]           ; мл. слово адреса GDT
                 mov     dx,[bp+6]                   ; ст. слово адреса GDT
 
                 mov     [gdt_ptr4], dx      ; запоминаем адрес GDT
                 mov     [gdt_ptr2], ax
 
 ; Параметр gdt_size
 
                 mov     ax,[bp+8]                   ; получаем размер GDT
                 mov     [gdt_ptr], ax       ; и запоминаем его
 
 ; Параметры cseg и dseg
 
                 mov     ax,[bp+10d]         ; получаем селектор сегмента кода
                 mov     dx,[bp+12d]         ; получаем селектор сегмента данных
                 mov     [cs:p_mode_select], ax      ; запоминаем для команды
                 mov        [protect_sel], dx          ; перехода far jmp
 
 ; Подготовка к возврату в реальный режим
 
                 push            ds                              ; готовим адрес возврата
                 mov             ax,40h                  ; из защищённого режима
                 mov             ds,ax
                 mov             [WORD 67h],OFFSET shutdown_return
                 mov             [WORD 69h],cs
                 pop             ds
 
 ; Запрещаем и маскируем все прерывания
 
                 cli
                 in              al, INT_MASK_PORT
                 and             al, 0ffh
                 out             INT_MASK_PORT, al
 
 ; Записываем код возврата в CMOS-память
 
                 mov             al,8f
                 out             CMOS_PORT,al
                 jmp             delay1
 delay1:
                 mov             al,5
                 out             CMOS_PORT+1,al
 
                 call    enable_a20              ; открываем линию A20
 
                 mov             [real_ss],ss    ; запоминаем регистры SS и ES
                 mov             [real_es],es
 
 ; Перепрограммируем контроллер прерываний
 ; для работы в защищённом режиме
 
                 mov             dx,MASTER8259A
                 mov             ah,20
                 call            set_int_ctrlr
                 mov             dx,SLAVE8259A
                 mov             ah,28
                 call            set_int_ctrlr
 
 ; Загружаем регистры IDTR и GDTR
 
                 lidt            [FWORD idtr]
                 lgdt       [QWORD gdt_ptr]
 
                 mov             ax, 0001h       ; переключаем процессор
                 lmsw            ax              ; в защищённый режим
 
 ;               jmp     far flush
                 db      0eah
                 dw      OFFSET flush
 p_mode_select   dw      ?
 
 LABEL   flush   FAR
 
                 mov      dx, [protect_sel]
                 mov         ss, dx
                 mov         ds, dx
                 mov         es, dx
 
 ; Обнуляем содержимое регистра LDTR
 
                 mov             ax, 0
                 lldt            ax
 
                 pop         bp
                 ret
 ENDP _protected_mode
 
 ; ----------------------------------------------------
 ; Возврат в реальный режим.
 ; Прототип для вызова
 ;   void real_mode();
 ; ----------------------------------------------------
 
 PROC _real_mode   NEAR
 
 ; Сброс процессора
 
                 cli
                 mov             [real_sp], sp
                 mov             al, SHUT_DOWN
                 out             STATUS_PORT, al
 
 rmode_wait:
                 hlt
                 jmp             rmode_wait
 
 LABEL   shutdown_return FAR
 
 ; Вернулись в реальный режим
 
                 mov             ax, DGROUP
                 mov             ds, ax
 
 assume  ds:DGROUP
 
                 mov     ss,[real_ss]
                 mov     sp,[real_sp]
 
                 in      al, INT_MASK_PORT
                 and     al, 0
                 out     INT_MASK_PORT, al
 
                 call    disable_a20
 
                 mov     ax, DGROUP
                 mov     ds, ax
                 mov     ss, ax
                 mov     es, ax
 
                 mov     ax,000dh
                 out     CMOS_PORT,al
                 sti
 
                 ret
 ENDP _real_mode
 
 ; -------------------------------------------------------
 ; Загрузка регистра TR.
 ; Прототип для вызова:
 ;   void load_task_register(unsigned int tss_selector);
 ; -------------------------------------------------------
 
 PROC _load_task_register  NEAR
                 push    bp
                 mov     bp,sp
                 ltr       [bp+4] ; селектор для текущей задачи
                 pop     bp
                 ret
 ENDP _load_task_register
 
 ; -------------------------------------------------------
 ; Переключение на задачу.
 ; Прототип для вызова:
 ;   void jump_to_task(unsigned int tss_selector);
 ; -------------------------------------------------------
 
 PROC _jump_to_task   NEAR
                 push    bp
                 mov     bp,sp
                 mov     ax,[bp+4]        ; получаем селектор
                                                         ; новой задачи
                 mov     [new_select],ax  ; запоминаем его
 
                 jmp     [DWORD new_task] ; переключаемся на
                                                         ; новую задачу
                 pop     bp
                 ret
 ENDP _jump_to_task
 
 ; ------------------------------
 ; Открываем линию A20
 ; ------------------------------
 
 PROC    enable_a20      NEAR
         push    ax
         mov     al, A20_PORT
         out     STATUS_PORT, al
         mov     al, A20_ON
         out     KBD_PORT_A, al
         pop     ax
         ret
 ENDP    enable_a20
 
 ; ------------------------------
 ; Закрываем линию A20
 ; ------------------------------
 
 PROC    disable_a20     NEAR
         push    ax
         mov     al, A20_PORT
         out     STATUS_PORT, al
         mov     al ,A20_OFF
         out     KBD_PORT_A, al
         pop     ax
         ret
 ENDP    disable_a20
 
 ; -----------------------------------------------------------
 ; Готовим структуру для загрузки регистра IDTR
 ; Прототип для вызова функции:
 ; void load_idtr(unsigned long idt_ptr, word idt_size);
 ; -----------------------------------------------------------
 
 PROC _load_idtr NEAR
                 push    bp
 
                 mov    bp,sp
                 mov    ax,[bp+4] ; мл. слово адреса IDT
                 mov    dx,[bp+6] ; ст. слово адреса IDT
                 mov       bx, OFFSET idtr
 
 ; Запоминаем адрес IDTR в структуре
 
                 mov         [(idtr_struc bx).idt_low], ax
                 mov         [(idtr_struc bx).idt_hi], dl
 
 ; Получаем предел IDT и запоминаем его в структуре
 
                 mov             ax, [bp+8]
                 mov        [(idtr_struc bx).idt_len], ax
 
                 pop         bp
                 ret
 ENDP _load_idtr
 
 ; ----------------------------------
 ; Установка контроллера прерываний
 ; ----------------------------------
 
 PROC    set_int_ctrlr   NEAR
 
         mov     al, 11
         out     dx, al
         jmp     SHORT $+2
         mov     al, ah
         inc     dx
         out     dx, al
         jmp     SHORT $+2
         mov     al, 4
         out     dx, al
         jmp     SHORT $+2
         mov     al, 1
         out     dx, al
         jmp     SHORT $+2
         mov     al, 0ffh
         out     dx, al
         dec     dx
         ret
 ENDP    set_int_ctrlr
 
 ; --------------------------
 ; Выдача звукового сигнала
 ; --------------------------
 
 PROC    _beep   NEAR
 
         push    ax bx cx
 
         in      al,KBD_PORT_B
         push    ax
         mov     cx,80
 
 beep0:
 
         push    cx
         and     al,11111100b
         out     KBD_PORT_B,al
         mov     cx,60
 
 idle1:
 
         loop    idle1
         or      al,00000010b
         out     KBD_PORT_B,al
         mov     cx,60
 
 idle2:
 
         loop    idle2
         pop     cx
         loop    beep0
 
         pop     ax
         out     KBD_PORT_B,al
 
         pop     cx bx ax
         ret
 
 ENDP    _beep
 
 ; -------------------------------
 ; Задержка выполнения программы
 ; -------------------------------
 
 PROC    _pause          NEAR
 
         push    cx
         mov     cx,10
 
 ploop0:
 
         push    cx
         xor     cx,cx
 
 ploop1:
 
         loop    ploop1
         pop     cx
         loop    ploop0
 
         pop     cx
         ret
 
 ENDP    _pause
 
 ; -----------------------
 ; Размаскирование прерываний
 ; -----------------------
 
 PROC    _enable_interrupt NEAR
 
                 in              al, INT_MASK_PORT
                 and             al, 0fch
                 out             INT_MASK_PORT, al
 
                 sti
                 ret
 ENDP    _enable_interrupt
 
         end
 
Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье