DOS-extender для компілятора Borland C + + 3.1, захищений режим процесора 80286, організація багатозадачного роботи процесора
b>
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.
4. Повні вихідні тексти програми.
>
4.1 Файл 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
4.2 Файл 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;// Зарезервовані 16 біт
) 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);
4.3 Файл TOS.H. Основний файл програми.
# include
# include
# include
# include
# 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
// ((word) & Keyb_int, CODE_SELECTOR, 0, TYPE_INTERRUPT_GATE, 0),// 21
// Вентиль завдання, що запускаються на переривання від клавіатури
(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;// номер поточного рядка для виводу на екран
// -------------------------------------------
// Початок програми
// -------------------------------------------
extern int getcpu (void);
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 ()