DOS-extender для компилятора Borland C++
DOS-extender для компилятора Borland C++ 3.1, защищенный режим процессора 80286, организация многозадачной работы процессора
1. Введение.
Операционная система MS DOS, не смотря на свое моральное устаревание, все еще довольно часто находит применение на парке старых ПК, а значит, все еще существует необходимость создания программ для нее.
К сожалению, написание программ в реальном режиме процессоров архитектуры Intel x86 осложнено отсутствием возможности использовать в программе оперативную память объемом свыше пресловутых 640 килобайт, а реально свыше 500-620 килобайт. Это ограничение к сожалению преследует MS DOS и аналогичные ей ОС других производителей, начиная с того момента, как горячо любимый в околокомпьютерных кругах Билл Гейтс заявил, что 640 килобайт достаточно для всех возможных задач ПК. Преодоление барьера 640 килобайт в новых версиях MS DOS усложнялось необходимостью совместимости с старыми программами, которые жизненно необходимо было поддерживать. Программирование защищенного режима процессора и расширенной памяти требовало от программистов недюжинных знаний архитектуры процессоров Intel и достаточно трудоемкого программирования.
1.1 Уровни программной поддержки защищенного режима.
Инженерная мысль не стоит на месте, особенно в такой области, как программирование. Задача программной поддержки защищённого режима и поддержки работы с расширенной памятью получила не одно, а сразу несколько решений. Этими решениями стали так называемые уровни программной поддержки защищённого режима и поддержки работы с расширенной памятью:
- интерфейс BIOS;
- интерфейс драйвера HIMEM.SYS;
- интерфейс EMS/VCPI;
- интерфейс DPMI;
- расширители DOS (DOS-экстендеры).
1.1.1 Интерфейс BIOS.
Интерфейсом самого низкого уровня является интерфейс BIOS, предоставляемый программам в виде нескольких функций прерывания BIOS INT 15h. Интерфейс BIOS позволяет программе перевести процессор из реального режима в защищённый, переслать блок памяти из стандартной памяти в расширенную или из расширенной в стандартную. Этим все его возможности и ограничиваются. Интерфейс BIOS используется для старта мультизадачных операционных систем защищённого режима (таких, как OS/2) или в старых программах, работающих с расширенной памятью в защищённом режиме (например, СУБД ORACLE версии 5.1).
1.1.2 интерфейс драйвера HIMEM.SYS.
С помощью функций, предоставляемых этим драйвером, программа может выполнять различные действия с блоками расширенной памяти, а также управлять адресной линией A20. Основное различие между способом работы с расширенной памятью драйвера HIMEM.SYS и интерфейсом прерывания BIOS INT 15h заключается в том, что первый выполняет выделение программе и внутренний учёт блоков расширенной памяти, а второй рассматривает всю расширенную память как один непрерывный участок. Однако драйвер HIMEM.SYS не открывает для программ доступ к защищённому режиму. Он полностью работает в реальном режиме, а для обращения к расширенной памяти использует либо недокументированную машинную команду LOADALL (если используется процессор 80286), либо возможности процессора 80386, который позволяет адресовать расширенную память в реальном режиме (при соответствующей инициализации системных регистров и таблиц).
1.1.3 интерфейс EMS/VCPI.
Используя трансляцию страниц, некоторые драйверы памяти (например, EMM386 или QEMM) могут эмулировать присутствие дополнительной памяти, используя расширенную память. При этом стандартный набор функций управления дополнительной памятью, реализованный в рамках прерывания INT 67h, дополнен еще несколькими функциями для работы в защищённом режиме процессора. Эти новые функции реализуют интерфейс виртуальной управляющей программы VCPI (Virtual Control Programm Interface). Они позволяют устанавливать защищённый и виртуальный режимы работы процессора, работать с расширенной памятью на уровне страниц и устанавливать специальные отладочные регистры процессора i80386. Интерфейс VCPI облегчает использование механизма трансляции страниц, освобождая программиста от необходимости работать с системными регистрами процессора.
1.1.4 интерфейс DPMI.
Интерфейс DPMI (DOS Protected Mode Interface - интерфейс защищённого режима для DOS) реализуется модулем, называющимся сервером DPMI. Этот интерфейс доступен для тех программ, которые работают на виртуальной машине WINDOWS или OS/2 версии 2.0 (позже мы обсудим некоторые детали, связанные с использованием интерфейса DPMI в WINDOWS). Интерфейс DPMI предоставляет полный набор функций для создания однозадачных программ, работающих в защищённом режиме. В этом интерфейсе имеются функции для переключения из реального режима в защищённый и обратно, для работы с локальной таблицей дескрипторов LDT, для работы с расширенной и стандартной памятью на уровне страниц, для работы с прерываниями (в том числе для вызова прерываний реального режима из защищённого режима), для работы с отладочными регистрами процессора i80386. Это наиболее развитый интерфейс из всех рассмотренных ранее.
1.1.5 расширители DOS (DOS-экстендеры).
Последний, самый высокий уровень программной поддержки защищённого режима - расширители DOS или DOS-экстендеры (DOS-extender). Они поставляются, как правило, вместе со средствами разработки программ (трансляторами) в виде библиотек и компонуются вместе с создаваемой программой в единый загрузочный модуль. DOS-экстендеры значительно облегчают использование защищённого режима и расширенной памяти в программах, предназначенных для запуска из среды MS-DOS. Программы, составленные с использованием DOS-экстендеров, внешне очень похожи на обычные программы MS-DOS, однако они получают управление, когда процессор уже находится в защищённом режиме. К формируемому с помощью DOS-экстендера загрузочному модулю добавляются процедуры, необходимые для инициализации защищённого режима. Эти процедуры первыми получают управление и выполняют начальную инициализацию таблиц GDT, LDT, IDT, содержат обработчики прерываний и исключений, систему управления виртуальной памятью и т.д.
1.2 Текущее положение дел в мире DOS-extender-ов.
Еще несколько лет назад целые фирмы зарабатывали себе на существование созданием различных модификаций DOS extender-ов. Например довольно известный externder фирмы Phar Lap. После перехода большинства пользователей в среду Win32 необходимость в DOS extender-ах резко сократилась и большинство таких фирм, не сумев сориентироваться в изменившихся условиях, прекратили свое существование.
Многие фирмы, разрабатывавшие компиляторы для DOS, включали в поставку своих сред программирования DOS-extender-ы собственной разработки. Таким примером может служить фирма Borland (ныне подразделение фирмы Corel) с ее Borland Pascal, Borland C++ и расширителем DOS RTM.
В данный момент доступно несколько DOS-extender-ов по свободной лицензии, которые могут использоваться кем угодно для любых целей. И это понятно, денег на них сейчас не заработаешь.
Примеры таких программ:
ZRDX by Sergey Belyakov
Маленький и функциональный DOS-extender для Watcom C++ и 32-х битных исполняемых файлов формата OS/2 LE. Используется в коммерческих программах, таких как антивирус AVP для DOS32.
WDOSX by Michael Tippach
Самый впечатливший меня DOS-extender. Список поддерживаемых функций просто поражает. Поддерживает все распространенные среды программирования: Visual C++ 4 и позже, Borland C++ 4 и позже, Delphi 2 и позже. При желании никто не запрещает использовать Assembler.
2. Обоснование выбора средств.
DOS-экстендеры обычно поставляются в комплекте с трансляторами, редакторами связей, отладчиками и библиотеками стандартных функций (например, библиотеками для транслятора языка Си). Код DOS-extender линкуется либо уже к готовому исполняемому файлу специальной программой (чаще), либо линковка полностью проходит при помощи программы-линкера, специально разработанного для данного компилятора.
В настоящий момент науке известны всего один DOS-extender для Borland C++ 3.1. Это программа фирмы Phar Lap, не имеющая собственного названия. Фирмы, к сожалению, давно уже нет, как и исходных текстов этого DOS-extender-а. В него входил собственная программа – линкер и набор специальных библиотек функций специально для Borland C++ 3.1, которой и проводилась окончательная сборка EXE-файла.
Написание собственной среды разработки, вроде программ-линкеров и собственных трансляторов языка Ассемблера явно выходит за переделы данного курсового проекта. Поэтому остановимся на разработке набора функций, позволяющих:
реализовать защищенный режим процессора 80286,
адресовать до 16 Мб памяти,
обрабатывать прерывания реального режима DOS
реализуем набор средств для создания параллельно выполняющихся потоков в среде DOS.
После разработки необходимых средств, напишем программу–пример с их использованием. Собственно это получится не просто программа, а некий прототип многозадачной операционной системы.
Итак, согласно заданию буду пользоваться следующими средствами разработки:
Borland C++ 3.1
Borland Turbo Assembler из поставки Borland C++ 3.1
3. Реализация работы программы в защищенном режиме процессора 80286.
3.1 Адресация защищенного режима процессора 80286.
Логический адрес в защищённом режиме (иногда используется термин "виртуальный адрес") состоит из двух 16-разрядных компонент - селектора и смещения. Селектор записывается в те же сегментные регистры, что и сегментный адрес в реальном режиме. Однако преобразование логического адреса в физический выполняется не простым сложением со сдвигом, а при помощи специальных таблиц преобразования адресов.
В первом приближении можно считать, что для процессора i80286 селектор является индексом в таблице, содержащей базовые 24-разрядные физические адреса сегментов. В процессе преобразования логического адреса в физический процессор прибавляет к базовому 24-разрядному адресу 16-разрядное смещение, т.е. вторую компоненту логического адреса (Рис. 1).
Такая схема формирования физического адреса позволяет непосредственно адресовать 16 мегабайт памяти с помощью 16-разрядных компонент логического адреса.
Таблиц дескрипторов в системе обычно присутствует от одной до нескольких десятков. Но всегда существует так называемая таблица GDT (Global Descriptor Table), в которой обычно хранится описание сегментов самой операционной системы защищенного режима 80286. Таблицы LDT (Local Descriptor Table) создаются на каждый новый запускаемый процесс в операционной системе, и в них хранится описание сегментов только одной отдельной задачи.
Таблица дескрипторов - это просто таблица преобразования адресов, содержащая базовые 24-разрядные физические адреса сегментов и некоторую другую информацию. То есть каждый элемент таблицы дескрипторов (дескриптор) содержит 24-разрядный базовый адрес сегмента и другую информацию, описывающую сегмент.
Процессор 80286 имеет специальный 5-байтный регистр защищенного режима GDTR, в котором старшие 3 байта содержат 24-разрядный физический адрес таблицы GDT, младшие два байта - длину таблицы GDT, уменьшенную на 1.
Рис. 1. Схема преобразования логического адреса в физический в защищенном режиме процессора 80286.
Перед переходом в защищённый режим программа должна создать в оперативной памяти таблицу GDT и загрузить регистр GDTR при помощи специальной команды LGDT.
Каждый элемент таблицы дескрипторов имеет следующий формат:
Общая его длина составляет 8 байт, в которых расположены следующие поля:
поле базового адреса длиной 24 бита содержит физический адрес сегмента, описываемого данным дескриптором;
поле предела содержит размер сегмента в байтах, уменьшенный на единицу;
поле доступа описывает тип сегмента (сегмент кода, сегмент данных и др.);
зарезервированное поле длиной 16 бит для процессора i80286 должно содержать нули, это поле используется процессорами i80386 и i80486 (там, в частности, хранится старший байт 32-разрядного базового адреса сегмента).
Поле доступа, занимающее в дескрипторе один байт (байт доступа) служит для классификации дескрипторов. На рис. 2 приведены форматы поля доступа для трёх типов дескрипторов - дескрипторов сегментов кода, сегментов данных и системных.
Рис. 2. Форматы поля доступа дескриптора.
Поле доступа дескриптора сегментов кода содержит битовое поле R, называемое битом разрешения чтения сегмента. Если этот бит установлен в 1, программа может считывать содержимое сегмента кода. В противном случае процессор может только выполнять этот код.
Биты P и A предназначены для организации виртуальной памяти. Их назначение будет описано в разделе, посвящённом виртуальной памяти. Сейчас отметим, что бит P называется битом присутствия сегмента в памяти. Для тех сегментов, которые находятся в физической памяти (мы будем иметь дело в основном с такими сегментами) этот бит должен быть установлен в 1.
Любая попытка программы обратиться к сегменту памяти, в дескрипторе которого бит P установлен в 0, приведёт к прерыванию.
Бит A называется битом обращения к сегменту и для всех наших программ должен быть установлен в 0.
Поле доступа дескриптора сегмента данных имеет битовые поля W и D. Поле W называется битом разрешения записи в сегмент. Если этот бит установлен в 1, наряду с чтением возможна и запись в данный сегмент. В противном случае при попытке чтения выполнение программы будет прервано.
Поле D задаёт направление расширения сегмента. Обычный сегмент данных расширяется в область старших адресов (расширение вверх). Если же в сегменте расположен стек, расширение происходит в обратном направлении - в область младших адресов (расширение вниз). Для сегментов, в которых организуются стеки, необходимо устанавливать поле D равным 1.
Рассмотрим, как таблица дескрипторов будет выглядеть на языке программирования C. (В дальнейшем где это только возможно будем применять язык С, а Ассемблер – только там, где это необходимо.):
typedef struct descriptor
{
word limit; // Предел (размер сегмента в байтах)
word base_lo; // Базовый адрес сегмента (младшее слово)
unsigned char base_hi; // Базовый адрес сегмента (старший байт)
unsigned char type_dpl; // Поле доступа дескриптора
unsigned reserved; // Зарезервированные 16 бит
} descriptor;
Данная структура описана в файле tos.h.
Инициализацию экземпляра такой структуры можно произвести при помощи функции, подобной функции init_gdt_descriptor, описанной в файле tos.c:
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 всегда (для процессоров 286)
descr->reserved = 0;
}
Например, запись в третий по счёту элемент GDT информации о сегменте данных с сегментным адресом _DS и пределом 0xffff будет выглядеть так:
init_gdt_descriptor(&gdt[2], MK_LIN_ADDR(_DS, 0), 0xffffL,
TYPE_DATA_DESCR | SEG_PRESENT_BIT | SEG_WRITABLE);
Макрос MK_LIN_ADDR определен в файле tos.h и служит для преобразования адреса реального режима формата сегмент:смещение в физический адрес:
#define MK_LIN_ADDR(seg,off) (((unsigned long)(seg))<<4)+(word)(off)
Специальный регистр процессора 286 LDTR имеет длину 16 разрядов и содержит селектор дескриптора, описывающего текущую таблицу LDT.
В данном курсовом проекте я не использую регистр LDTR и не создаю таблицы LDT, в моем варианте достаточно обойтись только одним кольцом защиты (0) процессора и только таблицей GDT.
3.2 Переход в защищенный режим процессора 80286
При переходе в защищенный режим программа совершает следующие операции:
Подготовка в оперативной памяти глобальной таблицы дескрипторов GDT. В этой таблице создаются дескрипторы для всех сегментов, которые будут нужны программе сразу после того, как она переключится в защищённый режим.
Для обеспечения возможности возврата из защищённого режима в реальный записывает адрес возврата в реальный режим в область данных BIOS по адресу 0040h:0067h, а также пишет в CMOS-память в ячейку 0Fh код 5. Этот код обеспечит после выполнения сброса процессора передачу управления по адресу, подготовленному нами в области данных BIOS по адресу 0040h:0067h.
Запрещает все маскируемые и немаскируемые прерывания.
Открывает адресную линию A20 (попробуем оперировать блоками памяти выше 1 Мб).
Запоминает в оперативной памяти содержимое сегментных регистров, которые необходимо сохранить для возврата в реальный режим, в частности, указатель стека реального режима.
Программирует контроллер прерываний для работы в защищенном режиме.
Загружает регистры IDTR и GDTR.
Необходимые функции для этого реализованы в файлах tos.c и TOSSYST.ASM:
Подготовка GDT осуществляется при помощи описанных выше функции init_gdt_descriptor() и макроса MK_LIN_ADDR().
Остальные действия, необходимые для перехода в защищенный режим, описаны в функции protected_mode() модуля TOSSYST.ASM:
Обеспечение возможности возврата в реальный режим:
push ds ; готовим адрес возврата
mov ax,40h ; из защищённого режима
mov ds,ax
mov [WORD 67h],OFFSET shutdown_return
mov [WORD 69h],cs
pop ds
Запрет прерываний:
сli
in al, INT_MASK_PORT
and al, 0ffh
out INT_MASK_PORT, al
mov al,8f
out CMOS_PORT,al
Открытие линии A20 производится вызовом функции enable_a20(), описанной в файле TOSSYST.ASM:
PROC enable_a20 NEAR
mov al,A20_PORT
out STATUS_PORT,al
mov al,A20_ON
out KBD_PORT_A,al
ret
ENDP enable_a20
Запоминаем содержимое сегментных регистров SS и ES:
mov [real_ss],ss
mov [real_es],es
Программируем при помощи функции set_int_ctrlr(), описанной в файле TOSSYST.ASM каскад контроллеров прерываний (Master и Slave) для работы в защищенном режиме (описание работы прерываний в защищенном режиме приведено ниже):
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
3.3 Возврат в реальный режим процессора.
Для того, чтобы вернуть процессор 80286 из защищённого режима в реальный, необходимо выполнить аппаратный сброс (отключение) процессора. Это реализуется в функции real_mode(), описанной в файле TOSSYST.ASM:
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
Функция disable_a20(), описанная в файле TOSSYST.ASM закрывает адресную линию 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
3.4 Обработка прерываний в защищенном режиме.
Обработка прерываний и исключений в защищённом режиме по аналогии с реальным режимом базируется на таблице прерываний. Но таблица прерываний защищённого режима является таблицей дескрипторов, которая содержит так называемые вентили прерываний, вентили исключений и вентили задач.
Таблица прерываний защищённого режима называется дескрипторной таблицей прерываний IDT (Interrupt Descriptor Table). Также как и таблицы GDT и LDT, таблица IDT содержит 8-байтовые дескрипторы. Причём это системные дескрипторы - вентили прерываний, исключений и задач. Поле TYPE вентиля прерывания содержит значение 6, а вентиля исключения - значение 7.
Формат элементов дескрипторной таблицы прерываний IDT показан на рис. 3.
Расположение определяется содержимым 5-байтового внутреннего регистра процессора IDTR. Формат регистра IDTR полностью аналогичен формату регистра GDTR, для его загрузки используется команда LIDT. Так же, как регистр GDTR содержит 24-битовый физический адрес таблицы GDT и её предел, так и регистр IDTR содержит 24-битовый физический адрес дескрипторной таблицы прерываний IDT и её предел.
Регистр IDTR программа загружает перед переходом в защищённый режим, в функции protected_mode() модуля TOSSYST.ASM при помощи вызова функции set_int_ctrlr(), описанной в файле TOSSYST.ASM.
Для обработки особых ситуаций - исключений - разработчики процессора i80286 зарезервировали 31 номер прерывания. Каждому исключению соответствует одна из функций exception_XX() из модуля EXCEPT.C. Собственно, описав реакцию программы на каждое исключение можно обрабатывать любые ошибки защищенного режима. В моем случае достаточно завершать программу при возникновении любого исключения с выдачей на экран номера возникшего исключения. Поэтому функции exception_XX() просто вызывают prg_abort(), описанной там же, и передают ей номер возникшего исключения. Функция prg_abort() переключает процессор в реальный режим, выводит сообщение с данными возникшего исключения и завершает работу программы.
Теперь разберемся с аппаратными прерываниями, которые нас не интересуют в данной программе, однако это не мешает им происходить. Для этого в модуле INTPROC.C описаны две функции заглушки iret0() и iret1(), которые собственно ничего не делают кроме того, что выдают на контроллеры команды конца прерывания. Функция iret0() относится к первому контроллеру (Master), а вторая – ко второму (Slave).
Неплохо было бы включить в программу поддержку программного прерывания 30h, чтобы можно было получать данные с клавиатуры. Это реализовано в модуле KEYBOARD.ASM, в функции Int_30h_Entry(). В IDT помещается вентиль программного прерывания, который вызывает данную функцию в момент прерывания 30h.
После запуска программа переходит в защищённый режим и размаскирует прерывания от таймера и клавиатуры. Далее она вызывает в цикле прерывание int 30h (ввод символа с клавиатуры), и выводит на экран скан-код нажатой клавиши и состояние переключающих клавиш (таких, как CapsLock, Ins, и т.д.). Если окажется нажатой клавиша ESC, программа выходит из цикла.
Обработчик аппаратного прерывания клавиатуры - процедура с именем Keyb_int из модуля KEYBOARD.ASM. После прихода прерывания она выдаёт короткий звуковой сигнал (функция beep() из модуля TOSSYST.ASM), считывает и анализирует скан-код клавиши, вызвавшей прерывание. Скан-коды классифицируются на обычные и расширенные (для 101-клавишной клавиатуры). В отличие от прерывания BIOS INT 16h, мы для простоты не стали реализовывать очередь, а ограничились записью полученного скан-кода в глобальную ячейку памяти key_code. Причём прерывания, возникающие при отпускании клавиш, игнорируются.
Запись скан-кода в ячейку key_code выполняет процедура Keyb_PutQ() из модуля KEYBOARD.ASM. После записи эта процедура устанавливает признак того, что была нажата клавиша - записывает значение 0FFh в глобальную переменную key_flag.
Программное прерывание int 30h опрашивает состояние key_flag. Если этот флаг оказывается установленным, он сбрасывается, вслед за чем обработчик int 30h записывает в регистр AX скан-код нажатой клавиши, в регистр BX - состояние переключающих клавиш на момент нажатия клавиши, код которой передан в регистре AX.
Ну и последнее, требующееся прерывание – это аппаратное прерывание таймера. Обработка этого прерывания реализована в функции Timer_int() модуля TIMER.C. Эта функция служит для переключения процессора между задачами. Более подробно я рассмотрю ее работу в следующей главе курсового проекта.
Структура элемента дескрипторной таблицы прерываний IDT описана в файле tos.inc:
STRUC idtr_struc
idt_len dw 0
idt_low dw 0
idt_hi db 0
rsrv db 0
ENDS idtr_struc
3.5 Реализация мультизадачности.
Я пошел в данном курсовом проекте самым простым способом – реализации мультизадачности через аппаратный таймер компьютера. Реализация более сложных алгоритмов явно тянет на дипломный проект.
Как известно, таймер вырабатывает прерывание IRQ0 примерно 18,2 раза в секунду. Можно использовать данный факт для переключения между задачами, выделяя каждой квант времени. Я не буду здесь реализовывать механизм приоритетов задач. Все выполняемые задачи имеют равный приоритет.
Для реализации разделения ресурсов компьютера между задачами и их взаимодействию друг с другом и средой исполнения (можно даже ее назвать операционной системой), я реализовал механизм семафоров.
В моем случае семафор представляет собой ячейку памяти, отражающая текущее состояние ресурса - свободен или занят.
Я иду еще на одно упрощение - не создаю здесь таблицы LDT для каждой задачи. Все-таки это не настоящая ОС, а ее так скажем, модель.
Настоящие многозадачные ОС квантуют время не на уровне программы, а на уровне задачи, так как каждая программа может иметь несколько параллельно выполняющихся потоков. Я не буду здесь организовывать механизм потоков. Это, я думаю, простительно, так как он не реализован полностью даже в Linux. Буду исходить из предпосылки, что одна программа равна одной задаче.
3.5.1 Контекст задачи.
Для хранения контекста неактивной в настоящей момент задачи процессор i80286 использует специальную область памяти, называемую сегментом состояния задачи TSS (Task State Segment). Формат TSS представлен на рис. 4.
Сегмент TSS адресуется процессором при помощи 16-битного регистра TR (Task Register), содержащего селектор дескриптора TSS, находящегося в глобальной таблице дескрипторов GDT (рис. 5).
Многозадачная операционная система для каждой задачи должна создавать свой TSS. Перед тем как переключиться на выполнение новой задачи, процессор сохраняет контекст старой задачи в её сегменте TSS.
Сегмент состояния задачи описан в файле tos.h:
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;
3.5.2 Переключение задач.
В качестве способа переключения между задачами выберем команду JMP. Неудобство в этом случае представляет то, что если, к примеру, задача 1 вызвала задачу 2, то вернуться к задаче 2 можно только вызвав снова команду JMP и передав ей TSS задачи 1.
Реализация альтернативного метода через команду CALL позволяет создавать механизм вложенных вызовов задач, но выглядит гораздо более трудоемким и требует организации вентилей вызова задач.
Функция переключения задач называется jump_to_task() и реализована в модуле TOSSYST.ASM:
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
Переключение задач происходит в функции Timer_int() из модуля TIMER.C. Эта функция вызывается по прерыванию таймера. Выбор какая задача получит процессор в данный момент решает диспетчер задач, организованный как функция dispatcher(), описанная в модуле TIMER.C. Диспетчер работает по самому простому алгоритму – по кругу переключает процессор между задачами.
3.5.3 Разделение ресурсов.
Разделение ресурсов для задач организовано в файле SEMAPHOR.C. Сам семафор представляет собой целое 2-х байтное число (int). В принципе можно было обойтись и одним битом, но это требует несколько более сложного кода.
Так как операционная система у меня получается ну очень крошечная, я думаю будет достаточно предположить, что максимальное количество семафоров в системе будет равно 5. Поэтому в файле SEMAPHOR.C задан статический массив из 5 семафоров:
word semaphore[5];
Работа задач с семафорами организуется при помощи 3-х функций:
sem_clear() – процедура сброса семафора,
sem_set() – процедура установки семафора,
sem_wait() – процедура ожидания семафора.
3.5.4 Задачи.
Исполняющиеся задачи организованы как просто функции, в модуле TASKS.C.
Задача task1() выполняется единократно, после чего передает управление операционной системе.
Задачи task2() и flipflop_task() работают в бесконечных циклах, рисуя на экране двигающиеся линии, тем самым обозначая свою работу. Задача flipflop_task() работает с меньшим периодом и только тогда, когда установлен семафор 1.
Задача keyb_task() вводит символы с клавиатуры и отображает скан-коды нажатых клавиш, а также состояние переключающих клавиш на экране. Если нажимается клавиша ESC, задача устанавливает семафор номер 0. Работающая параллельно главная задача ожидает установку этого семафора. Как только семафор 0 окажется установлен, главная задача завершает свою работу и программа возвращает процессор в реальный режим, затем передаёт управление MS-DOS.