Віртуальний драйвер для обслуговування апаратних переривань

Як вже зазначалося раніше, віртуальні драйвери служать перш за все для
віртуалізації апаратури, тобто для надання одночасно виконуваних
завданням можливості спільного використання пристроїв комп'ютера.
Вимірювальна або керуюча апаратура, що підключається до комп'ютера з метою
створення автоматизованої установки, навряд чи буде експлуатуватися в
багатозадачному режимі, однак використання для її управління віртуального
драйвера може помітно скоротити програмні витрати і зменшити час відгуку.
Розглянемо приклад віртуального драйвера, обслуговуючого переривання від описаної
в попередній статті інтерфейсної плати таймера-лічильника.


Очевидно, що до складу такого драйвера повинен входити обробник переривань
від плати. Функції цього обробника визначає програміст; в найпростішому випадку
обробник може просто прочитати дані з вихідного регістра лічильника і
замаскувати переривання. Проте застосування при цьому не дізнається про завершення
вимірювань; більш природно організувати виклик з обробника переривань
драйвера деякої функції програми, в яку можна передати прочитані
дані. Фактично ця функція буде грати роль обробника переривання
додатки, проте викликатися вона буде не самим перериванням, а обробником
драйвера.


Передбачимо наступну схему взаємодії програми та драйвера. Додаток
по команді користувача викликає драйвер і передає йому константи налаштування
таймера. Драйвер ініціалізує таймер і повертає керування в додаток,
яке продовжує своє виконання. За сигналом переривання від таймера додаток
припиняється і активізується обробник переривань, що знаходиться в
драйвері. У цьому обробнику виконуються читання даних з вихідного регістра
лічильника, виклик (через VMM) обробника переривання програми та передача в нього
лічених даних. Обробник переривань програми приймає з драйвера дані
і, завершуючи, повертає керування в VMM, який нарешті передає управління
в додаток в точку його призупинення.


Оскільки обробник переривань програми викликається з VMM і після свого
завершення знову передає управління в систему, його можливості обмежені. У
ньому, зокрема, неприпустимий виклик функції виведення інформації – MessageBox () – у
вікно повідомлення. Тому тут, як і в попередньому прикладі, виникає проблема
оповіщення головної функції WinMain () про прихід переривання. Ми вирішимо цю проблему
наступним чином: обробник переривання програми, отримавши управління,
встановлює прапор, який опитується головною функцією в кожному кроці циклу
обробки повідомлень. Головна функція, виявивши встановлене стан прапора,
посилає в додаток повідомлення користувача, яке нарівні з повідомленнями
Windows обслуговується циклом обробки повідомлення. У функції обробки цього
повідомлення вже можна виконувати будь-які дії без жодних обмежень.


При розробці програми, що реалізує описаний алгоритм, за основу взято
програмний комплекс з попередньої частини статті. У ньому використовується той же файл
ресурсів, проте програмний файл модифікований.



Додаток Windows, що взаємодіє з віртуальним драйвером обробки
апаратних переривань

#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <string.h>
/ / Визначення констант
# Define MI_START 100 / / Константи, використовувані
# Define MI_EXIT 101 / / для ідентифікації
# Define MI_READ 102 / / пунктів меню
/* void Cls_OnUser(HWND hwnd) */
# Define HANDLE_WM_USER (hwnd, wParam, lParam, fn) / / Макрос для обробки
((Fn) (hwnd), 0L) / / повідомлення WM_USER
/ / Прототипи функцій
void Register (HINSTANCE); / / Допоміжні
void Create (HINSTANCE); / / функції
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); / / Віконна функція
void OnCommand (HWND, int, HWND, UINT); / / Функції
void OnDestroy (HWND); / / обробки повідомлень Windows
void OnUser (HWND); / / і користувача
void InitCard (); / / Функція ініціалізації плати (через драйвер)
void GetAPIEntry (); / / Функція отримання точки входу в драйвер
void isr (int, int); / / Прототип обробника переривання програми
/ / Глобальні змінні
char szClassName [] = "MainWindow"; / / Ім'я класу головного вікна
char szTitle [] = "Управління"; / / Заголовок вікна
HWND hMainWnd; / / Дескриптор головного вікна
FARPROC VxDEntry; / / Адреса точки входу в драйвер
int unsigned data; / / Дане з драйвера
char request; / / Прапор закінчення вимірювань
/ / Головна функція WinMain
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int){
MSG msg; / / Структура для прийому повідомлень
Register (hInstance); / / Реєстрація класу головного вікна
Create (hInstance); / / Створення і показ головного вікна
/ * Більш складний цикл обробки повідомлень, в який включені
аналіз прапора request завершення вимірювань і посилка додатком
повідомлення WM_USER при установці цього прапора * /
do{
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)){
if (msg.message == WM_QUIT)return msg.wParam;
DispatchMessage(&msg);
} / / Кінець if (PeekMessage ())
if(request){
request = 0; / / Скидання прапора
PostMessage (hMainWnd, WM_USER, 0,0); / * Поставити в чергу
повідомлення WM_USER без параметрів * /
} / / Кінець if (request)
} While (1); / / Кінець do
} / / Кінець WinMain
/ / Функція Register реєстрації класу вікна
void Register(HINSTANCE hInst){
WNDCLASS wc;
memset(&wc,0,sizeof(wc));
wc.lpszClassName=szClassName;
wc.hInstance=hInst;
wc.lpfnWndProc=WndProc;
wc.lpszMenuName="Main";
wc.hCursor=LoadCursor(NULL,IDC_ARROW);
wc.hIcon=LoadIcon(NULL,IDI_APPLICATION);
wc.hbrBackground=GetStockBrush(WHITE_BRUSH);
RegisterClass(&wc);
}
/ / Функція Create створення і показу вікна
void Create(HINSTANCE hInst){
hMainWnd = CreateWindow (szClassName, szTitle, WS_OVERLAPPEDWINDOW,
10,10,200,100,HWND_DESKTOP,NULL,hInst,NULL);
ShowWindow(hMainWnd,SW_SHOWNORMAL);
}
/ / Віконна функція WndProc головного вікна
LRESULT CALLBACK WndProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch(msg){
HANDLE_MSG(hwnd,WM_COMMAND,OnCommand);
HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);
HANDLE_MSG(hwnd,WM_USER,OnUser);
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
}
}
/ / Функція OnCommand обробки повідомлень WM_COMMAND від пунктів меню
void OnCommand(HWND hwnd,int id,HWND,UINT){
switch(id){
case MI_START: / / Ініціалізація плати
InitCard();
break;
case MI_READ:
char txt [80];
wsprintf (txt, "Накопичено% d подій", data);
MessageBox (hMainWnd, txt, "Дані", MB_ICONINFORMATION);
break;
case MI_EXIT: / / Завершити додаток
DestroyWindow(hwnd);
}
}
/ / Функція OnUser обробки повідомлення WM_USER
void OnUser(HWND){
MessageBox (hMainWnd, "Виміри закінчені", "Контроль", MB_ICONINFORMATION);
}
/ / Функція OnDestroy обробки повідомлення WM_DESTROY
void OnDestroy(HWND){
PostQuitMessage(0);
}
/ / Функція InitCard ініціалізації (через драйвер) плати
void InitCard (){
GetAPIEntry (); / / Отримання адреси точки входу в драйвер
_AX = _DS; / / Передача в драйвер DS
_BX = 55; / / Канал 0
_CX = 20000; / / Канал 1; BX * CX = Тривалість інтервалу
_DX = 1000; / / Канал 2, внутрішній генератор
_SI = OFFSETOF (isr); / / Зсув обробника переривань
_DI = SELECTOROF (isr); / / Селектор обробника переривань
VxDEntry (); / / Виклик драйвера
}
/ / Функція GetAPIEntry отримання адреси точки входу в драйвер
void GetAPIEntry(){
asm{
mov AX,0x1684
mov BX,0x8000
int 0x2F
mov word ptr VxDEntry,DI
mov word ptr VxDEntry+2,ES
}
}
/ / Обробник переривань додатки. Викликається VMM і повертає керування в VMM
void isr(int segment,int dt){
_DS = Segment; / / Ініціалізіруем DS селектором, отриманим з драйвера
request + +; / / Поставимо запит на повідомлення
data = dt; / / Отримаємо з драйвера апаратні дані
}

Головна функція WinMain () виконує три характерні для неї процедури:
реєстрацію класу головного вікна, створення та показ головного вікна, цикл обробки
повідомлень. У циклі обробки повідомлень є дві принципові відмінності від
прикладу попередній частині статті: у кожному кроці циклу перевіряється стан прапора
завершення вимірювань request, і якщо прапор виявляється встановленим, то
викликається функція Windows PostMessage (), яка ставить в чергу повідомлень
наведеного вище програми наше повідомлення з кодом WM_USER. Для того щоб у
цикл обробки повідомлень включити перевірку прапора, довелося замінити в ньому
функцію GetMessage () на функцію PeekMessage (), яка, на відміну від
GetMessage (), при відсутності повідомлень в черзі повертає керування в
програму, що і дає можливість включити в цикл додаткові дії.
Однак PeekMessage () не аналізує повідомлення WM_QUIT про завершення програми,
тому <виловлювання> цього повідомлення (і завершення програми оператором
return 0 у разі його приходу) доводиться виконувати вручну. Конструкція:

do{

}while(1)

дозволяє організувати нескінченний цикл, оскільки умова продовження циклу,
аналізоване оператором while, безумовно задовольняється (константа 1 ніколи
не може стати рівною 0).


У віконній функції WndProc () фіксується прихід трьох повідомлень: WM_COMMAND від
пунктів меню, WM_DESTROY від команд завершення програми та WM_USER,
свідчить про закінчення вимірювань. Оскільки для повідомлення WM_USER в
файлі windowsx.h відсутня макрос HANDLE_WM_USER, його довелося визначити в
початку програми за допомогою оператора # define, побудувавши макрорасшіренія по
аналогією з яким-небудь з макросів виду HANDLE_повідомлення з файлу windowsx.h, хоча б з
макросом HANDLE_WM_DESTROY.


Фрагмент програми, що виконується при виборі користувачем пункту меню
<Пуск>, містить лише виклик функції InitCard (). У ній викликом вкладеної
функції GetAPIEntry визначається адресу API-процедури драйвера, а потім, після
заповнення ряду регістрів параметрами, переданими в драйвер, викликається ця
процедура. У драйвер передаються такі параметри: селектор сегменту даних
додатки, три константи для ініціалізації плати, а також селектор і зміщення
обробника переривань програми isr (). Передача в драйвер вмісту
сегментного регістра DS (селектора сегмента даних) необхідна тому, що при
виклику драйвером (точніше, VMM) нашої функції isr () не відновлюється
операційне середовище програми, зокрема регістр DS не вказує на поля
даних програми, які в результаті виявляються недоступними. Передавши в
драйвер вміст DS, ми зможемо повернути його назад разом з іншими даними,
переданими з драйвера в додаток, відновивши тим самим адресується
даних.


При виборі користувачем пунктів меню <Читання> або <Вихід>
виконуються ті ж дії, що і в попередньому прикладі.


У порівнянні з попереднім прикладом спростилася функція OnDestroy (). Оскільки
відновлення маски в контроллері переривань покладено тепер на драйвер, а
вихідний вектор ми в цьому варіанті програми не відновлюємо, то у функції
OnDestroy () лише викликається функція Windows PostQuitMessage (), що призводить до
завершення програми.


У обробнику переривань програми isr () після засилання в регістр DS нашого
ж селектора сегмента даних, переданого раніше в драйвер і отриманого з нього
як перший параметр функції isr (), виконується інкремент прапора request
і пересилання в змінну data другого параметра функції isr () – результату
вимірювань.


Перейдемо до розгляду програми віртуального драйвера, що входить до складу
нашого програмного комплексу.



Текст віртуального драйвера, обробного апаратні переривання

; При виклику AX = DS програми, BX = C0, CX = C1, DX = C2, DI = селектор isr, SI = зсув isr
.386p
.XLIST
include vmm.inc
include vpicd.inc
.LIST
Declare_Virtual_Device VMyD,1,0,VMyD_Control,8000h,
Undefined_Init_Order,,API_Handler
;=======================
VxD_REAL_INIT_SEG
BeginProc VMyD_Real_Init
; Текст процедури ініціалізації реального режиму (див. частину 2 цього циклу)
EndProc VMyD_Real_Init
VxD_REAL_INIT_ENDS
;======================
VxD_DATA_SEG
Data dw 0; Осередок для результату вимірювань
DSseg dw 0; Осередок для зберігання селектора програми
Segment_Callback dw 0; Селектор функції isr програми
Offset_Callback dd 0; Зміщення функції isr програми
IRQ_Handle dd 0; Дескриптор віртуального переривання
VMyD_Int13_Desc label dword; 32-бітову адресу наступній далі структури
VPICD_IRQ_Descriptor <5,, OFFSET32 VMyD_Int_13>; Структура з інформацією
; Про віртуалізованому перериванні
VxD_DATA_ENDS
;======================
VxD_CODE_SEG
BeginProc VMyD_Control
; Включимо до складу драйвера процедуру обробки системного повідомлення
; Device_Init про ініціалізації драйвера
Control_Dispatch Device_Init, VMyD_Device_Init
clc
ret
EndProc VMyD_Control
;———————-
; Процедура, що викликається при ініціалізації драйвера системою
BeginProc VMyD_Device_Init
mov EDI, OFFSET32 VMyD_Int13_Desc; Адреса структури VPICD_IRQ_Descriptor
VxDCall VPICD_Virtualize_IRQ; Віртуалізація пристрої
mov IRQ_Handle, EAX; Збережемо дескриптор віртуального IRQ
clc
ret
EndProc VMyD_Device_Init
;————————-
; API-процедура, що викликається з програми
BeginProc API_Handler
; Отримаємо параметри з програми
push [EBP.Client_DI]
pop Segment_Callback
push [EBP.Client_AX];DS
pop DSseg
movzx ESI,[EBP.Client_SI]
mov Offset_Callback,ESI
; Загальний скид
mov DX,30Ch
in AL,DX
; Размаскіруем рівень 5 у фізичному контроллері переривань
mov EAX,IRQ_Handle
VxDCall VPICD_Physically_Unmask
; Засилаємо керуючі слова по каналах
mov DX,303h
mov AL, 36h; Канал 0
out DX,AL
mov AL, 70h; Канал 1
out DX,AL
mov AL, 0B6h; Канал 2
out DX,AL
; Засилаємо константи в канали
mov DX, 300h; Канал 0
mov AX, [EBP.Client_BX]; Константа С0
out DX, AL; Молодший байт частоти
xchg AL,AH
out DX, AL; Старший байт частоти
mov DX, 301h; Канал 1
mov AX, [EBP.Client_CX]; Константа С1
out DX, AL; Молодший байт інтервалу
xchg AL,AH
out DX, AL; Старший байт інтервалу
mov DX, 302h; Канал 2
mov AX, [EBP.Client_DX]; Константа С2
out DX, AL; Молодший байт частоти
xchg AH,AL
out DX, AL; Старший байт частоти
; Встановимо прапор S2 дозволу рахунки
mov DX,30Bh
in AL,DX
ret
EndProc API_Handler
;————————-
; Процедура обробки апаратного переривання IRQ5 (вектор 13)
BeginProc VMyD_Int_13, High_Freq
; Отримаємо результат вимірювань з вихідного регістра лічильника
mov DX, 309h; Порт старшого байта
in AL, DX; Отримаємо старший байт
mov AH, AL; Відправимо його в AH
dec DX ;DX=308h
in AL, DX; Отримаємо молодший байт
mov Data, AX; Весь результат в Data
; Виконаємо завершальні дії в PIC і викличемо функцію додатка
mov EAX,IRQ_Handle
VxDCall VPICD_Phys_EOI; EOI у фізичний контролер переривань
VxDCall VPICD_Physically_Mask; Маскуємо наш рівень
; Перейдемо на синхронний рівень
mov EDX, 0; Дані відсутні
mov ESI, OFFSET32 Reflect_Int; Адреса синхронної процедури
VMMCall Call_VM_Event; Встановимо запит на її виклик з VMM
clc
ret
EndProc VMyD_Int_13
;————————-
; Процедура рівня відкладених переривань
BeginProc Reflect_Int
Push_Client_State; Виділимо місце на стеку для регістрів клієнта
VMMCall Begin_Nest_Exec; Почнемо вкладений блок виконання
mov AX, Data; Відправимо дане
VMMCall Simulate_Push; в стек клієнта
mov AX, DSseg; Відправимо отриманий раніше DS
VMMCall Simulate_Push; в стек клієнта
mov CX, Segment_Callback; Зайшли отриманий раніше адресу функції isr
mov EDX, Offset_Callback; в CS: IP клієнта, щоб після повернення з VMM
VMMCall Simulate_Far_Call; у віртуальну машину зголосилася ця функція
VMMCall Resume_Exec; Повернення з VMM в поточну віртуальну машину
VMMCall End_Nest_Exec; Завершимо вкладений блок виконання
Pop_Client_State; Звільнимо місце на стеку для регістрів клієнта
clc
ret
EndProc Reflect_Int
VxD_CODE_ENDS
end VMyD_Real_Init

У полях даних драйвера зарезервовані ряд осередків для тимчасового зберігання
отриманих з програми параметрів, а також результату вимірювань. Окремо
варто осередок IRQ_Handle, в якій зберігається дескриптор віртуального переривання.
Цей дескриптор призначається системою на етапі ініціалізації драйвера і залишається
незмінним на весь час його життя, тобто до перезавантаження комп'ютера.


Макрос VPICD_IRQ_Descriptor дозволяє описати в полях даних структуру з
інформацією про віртуалізованому перериванні. Обов'язковими елементами цієї
структури є номер рівня віртуалізуемого переривання та адресу обробника
апаратного переривання (VMyD_Int_13 в нашому випадку), що включається до складу
драйвера. Для того щоб макроси віртуального контролера переривань були
доступні асемблеру, до програми необхідно підключити (оператором include) файл
VPICD.INC.


Віртуалізація переривання здійснюється на етапі ініціалізації драйвера. До
Досі ми в явній формі не використовували процедуру VMyD_Control, в якій
обробляються системні повідомлення Windows. У розглянутому драйвері до складу
цієї процедури за допомогою макросу Control_Dispatch включена процедура
VMyD_Device_Init (ім'я довільно), яка буде викликана при отриманні
драйвером системного повідомлення Device_Init. Для обробки більшого числа
повідомлень Windows в процедуру VMyD_Control слід включити по макросу
Control_Dispatch на кожне оброблюваний повідомлення (із зазначенням імен повідомлення
та процедури його обробки).


Процедура VMyD_Device_Init містить виклик функції віртуального контролера
переривань (VPICD) VPICD_Virtualize_IRQ. Ця функція здійснює віртуалізацію
вказаного рівня переривань і повертає дескриптор віртуального переривання,
який зберігається нами у клітинці IRQ_Handle з метою подальшого використання.
Функція VPICD_Virtualize_IRQ фактично встановлює в системі наш обробник
переривань, ім'я якого включено нами в структуру VPICD_IRQ_Descriptor. Починаючи
з цього моменту апаратні переривання IRQ5 будуть викликати за замовчуванням, не
обробник цього рівня, що знаходиться в VPICD, а наш обробник. Правда, для
цього треба размаскіровать рівень 5 в контроллері переривань, чого ми ще не
зробили.


При виклику драйвера з програми управління передається API-процедурі
драйвера API_Handler. У ній перш за все беруться з структури клієнта
передані в драйвер параметри. Оскільки ці параметри (вміст регістрів
клієнта) зберігаються в стеку рівня 0, то є в пам'яті, їх не можна безпосередньо
перенести в осередку даних драйвера. Для перенесення параметрів в деяких випадках
ми використовували стек, в інших – регістри загального призначення.


Виконавши команду загального скидання програмованої плати, слід размаскіровать
переривання у фізичному (не віртуальному) контроллері переривань. Ця операція
здійснюється викликом функції віртуального контролера VPICD_Physically_Unmask
із зазначенням їй як параметр в регістрі EAX дескриптора віртуального
переривання. Далі виконується вже розглянута в попередній частині статті
процедура ініціалізації плати (причому значення констант С0, С1 і С2 витягуються
зі структури клієнта). Після завершення API-процедури управління повертається в
додаток до надходження апаратного переривання.


Апаратне переривання віртуалізованому нами рівня через дескриптор таблиці
переривань IDT з номером 55h активізує обробник переривань, що входить до
склад VPICD, який, виконавши деякі підготовчі дії (у
Зокрема, сформувавши на стеку рівня 0 структуру клієнта), передає управління
безпосередньо нашому драйверу, а саме процедурі обробки апаратного
переривання VMyD_Int_13. Системні витрати цього переходу складають близько 40
команд процесора, тобто час від моменту надходження переривання до виконання
першої команди нашого обробника складе на комп'ютері середнього швидкодії
10 … 15 мкс.


У процедурі VMyD_Int_13 після виконання змістовної частини (у нашому випадку
– Читання і запам'ятовування результату вимірювань) необхідно надіслати в контролер
переривань команду EOI, як це належить робити в кінці будь-якого обробника
апаратного переривання. Для віртуалізованому переривання це дію
виконується за допомогою функції VPICD_Phys_EOI, єдиним параметром якої
є дескриптор переривання, збережений нами у клітинці IRQ_Handle. Останньою
операцією є виклик функції VPICD_Physically_Mask, за допомогою якої
маскується рівень 5 у фізичному контроллері переривань.


Слід зазначити, що назви функцій VPICD можуть бути оманливими. Функція
VPICD_Phys_EOI в дійсності не розблокує контролер переривань, а
размаскірует наш рівень в регістрі маски фізичного контролера (чого ми,
між іншим, не замовляли!). Що ж до команди EOI, то вона була послана
в контролер по ходу виконання фрагмента VPICD ще до переходу на наш
обробник (згадані вище 40 команд). Тим не менш виклик функції
VPICD_Phys_EOI в кінці обробника переривань обов'язковий. Якщо нею знехтувати, то
операційна система буде поводитися так само, як якщо б у контролер не
була послана команда EOI: перше переривання обробляється нормально, але все
наступні – блокуються. Так відбувається тому, що при відсутності виклику
функції VPICD_Phys_EOI порушується робота функції VPICD_Physically_Unmask,
яка виконується у нас на етапі ініціалізації. Ця функція, виконавши аналіз
системних полів і виявивши, що попереднє переривання не завершилося викликом
VPICD_Phys_EOI, обходить ті свої рядки, в яких у порту 21h встановлюється 0 біт нашого рівня
переривань. У результаті цей рівень залишається замаскованим і переривання не
проходять.


Якщо обробник переривань, включений у драйвер, виконує тільки
обслуговування апаратури, то на цьому його програма може бути завершена. Однак
ми хочемо сповістити про переривання додаток, викликавши одну з його функцій. VMM
передбачає таку можливість (так зване вкладене виконання VM), але
для її реалізації слід перш за все перейти з асинхронного рівня на
синхронний.


Проблема полягає в тому, що VMM є нереентерабельной програмою.
Якщо перехід у віртуальний драйвер здійснюється синхронним чином, викликом з
поточного додатка, то, хоча цей перехід відбувається за участю VMM і, так
сказати, через нього, в активізованою процедурі віртуального драйвера допустимо
виклик всіх функцій VMM. Якщо ж перехід в драйвер стався асинхронно, в
результаті апаратного переривання, то стан VMM в цей момент невідомо і в
процедурі драйвера допустимо виклик лише невеликого набору функцій, що відносяться до
категорії асинхронних. До них, зокрема, відносяться всі функції VPICD, а також
ті функції VMM, за допомогою яких програма переводиться на синхронний рівень
(Його іноді називають рівнем відкладених переривань). У довіднику, що входить до
склад DDK, зазначено, які функції є асинхронними, і на цю
характеристику функцій слід звертати увагу.


Ідея переходу на рівень відкладених переривань полягає в тому, що в
обробнику апаратних переривань за допомогою однієї із спеціально призначених
для цього асинхронних функцій VMM встановлюється запит на виклик
callback-функції (функції зворотного виклику). Ця функція буде викликана
засобами VMM в той момент, коли перехід на неї не порушить працездатність
VMM. Вся ця процедура носить назву <обробка події>.


У поняття <подія> входить не тільки callback-функція, але й набір
умов, за яких вона може бути викликана або якими повинен супроводжуватися
її виклик. Так, наприклад, можна вказати, що callback-функцію можна викликати
тільки поза критичної секції або що виклик callback-функції повинен
супроводжуватися підвищенням пріоритету її виконання. Крім того, при установці
події можна визначити дану (подвійне слово), яке буде передано в
callback-функцію при її виклику. У складі VMM є цілий ряд функцій
установки подій, що розрізняються умовами їх обробки, наприклад
Call_When_Idle, Call_When_Not_Critical, Call_Restricted_Event,
Schedule_Global_Event, Schedule_Thread_Event та ін Необхідно підкреслити, що
момент фактичного виклику callback-функції заздалегідь визначити неможливо. Вона
може бути викликана негайно або через деякий час, коли будуть
задоволені поставлені умови.


У нашому випадку спеціальні умови відсутні і перехід на синхронний
рівень можна виконати за допомогою простої функції Call_VM_Event, в якості
параметра якій вказується 32-бітове зсув функції зворотного виклику,
розташовується в тексті віртуального драйвера. У розглянутому прикладі ця
функція названа Reflect_Int.


Команда ret, якою закінчується обробник переривань віртуального
драйвера, передає управління VMM, який у зручний для нього момент часу
викликає функцію Reflect_Int (реально до виклику може пройти 50 … 200 мкс). У
цієї функції викликом Push_Client_State вихідна структура клієнта ще раз
зберігається на стеку рівня 0, після чого функцією Begin_Nest_Exec відкривається
блок вкладеного виконання. Всередині цього блоку можна, по-перше, організувати
перехід на певну функцію додатка, а по-друге, створити умови для
передачі їй необхідних параметрів. Передача параметрів здійснюється в
відповідно до встановленого інтерфейсом використовуваної мови програмування.
Оскільки наш додаток написано на мові Сі, для його функцій діють
правила цієї мови: параметри передаються функції через стек, причому
розташування параметрів в стеку повинно відповідати їх перерахування до
прототипі і заголовку функції, тобто в глибині стека повинен знаходитися
останній параметр, а на вершині стека – перший (у функціях типу
<Паскаль>, зокрема в усіх системних функціях Windows, діє
зворотний порядок передачі параметрів).


Приміщення параметрів в стек поточної VM здійснюється функцією VMM
Simulate_Push, яка може проштовхувати в стек як одинарні, так і подвійні
слова. У нашому випадку в стек поміщаються два слова – результат вимірювань і
селектор сегменту даних програми.


Наступна операція – підготовка виклику необхідної функції програми. Ця
операція здійснюється за допомогою функції VMM Simulate_Far_Call, яка
поміщає передані їй в якості параметрів селектор і зміщення необхідної
функції додатку в поля структури клієнта Client_CS і Client_IP. У результаті,
коли VMM, передаючи управління додатком, зніме з стека структуру клієнта і
виконає перехід за рештою в стеку значень Client_CS і Client_IP, то в
регістрах CS: IP виявиться адресу цікавить нас функції, яка і почне
негайно виконуватися. Для того щоб не втратити те місце в додатку, на
якому відбулася його припинення через прихід переривання, поточний зміст
полів Client_CS і Client_IP зберігається у створеній перед цим копії структури
клієнта.


Нарешті, викликом Resume_Exec управління передається в додаток. Ще раз
підкреслимо, що цей виклик функції програми є вкладеним у VMM і що
можливості викликається функції дуже обмежені. Фактично вона працює в
чужої для програми операційному середовищі. Зокрема, як уже зазначалося,
вміст сегментних регістрів (крім CS) не відповідає сегментам
додатки. Для того щоб функція isr () могла звернутися до глобальних
змінним додатки (адресуються через регістр DS), ми передаємо їй селектор
сегмента даних програми.


Повернемося ненадовго до тексту програми. Функція isr (), яку ми викликаємо з
драйвера, має наступний вигляд:

void isr(int segment,int dt){

}

Оскільки ми в драйвері проштовхнули в стек спочатку дані Data, а потім
селектор DSseg, вони розташувалися в стеку програми в правильному з точки зору
цієї функції порядку, тому вона може звертатися до своїх локальним змінним
segment і dt, як якщо б була викликана звичайним чином оператором:


isr(DSseg,Data);


Після завершення функції isr () управління повертається в VMM, а з нього в
драйвер на команду, наступну за викликом Resume_Exec. Цей перехід може
вимагати пари сотень команд і декількох десятків мікросекунд.


Відкладена процедура драйвера завершується очевидними викликами End_Nest_Exec –
закінчення вкладеного блоку виконання та Pop_Client_State – відновлення
структури клієнта.


Описана методика організації взаємодії обробника апаратних
переривань, включеного до складу драйвера, і самого додатка щодо
складна і тим не менш не забезпечує необхідні функціональні можливості
обробнику переривань програми, в якому забороняється виклик функцій Windows.
Для того щоб по сигналу переривання вивести на екран результати вимірювань, нам
довелося створювати спеціальний цикл обробки повідомлень з постійним опитуванням
прапора request. Установка прапора обробником переривань програми приводила до
виконання функції PostMessage () і посилці у додаток повідомлення WM_USER, в
відповідь на яке ми вже могли виконувати будь-які програмні дії без всяких
обмежень.


Помітного спрощення програми можна домогтися, організувавши посилку в
додаток повідомлення WM_USER безпосередньо з обробника переривань драйвера,
точніше з рівня відкладених переривань. У цьому випадку відпадає необхідність у
передачу драйверу будь-яких даних, крім дескриптора того вікна програми, в
яке надсилається повідомлення WM_USER, в даному випадку – дескриптора головного
вікна. Скорочується і текст процедури відкладених переривань Reflect_Int.
Додаток Windows також спрощується: відпадає необхідність у поділі функцій
обробки переривань між функцією Isr (), яка працює, по суті, на рівні
відкладених переривань, і функцією OnUser (), що виконується вже на звичайному рівні
завдання. Оскільки результат вимірювань легко передати з драйвера в додаток у
Як параметр повідомлення WM_USER, зникає необхідність у пункті
<Читання> в меню програми.


Розглянемо зміни, які вносяться при використанні такого методу.



Додаток Windows, обробляє апаратні переривання

/ / Оператори препроцесора # define і # include

# Define HANDLE_WM_USER (hwnd, wParam, lParam, fn) / / Макрос для обробки
((Fn) (hwnd, wParam), 0L) / / повідомлення WM_USER
/ / Прототипи функцій

void OnUser (HWND, WPARAM); / / Сигнатура функції змінилася
/ / Глобальні змінні
HWND hMainWnd; / / Дескриптор головного вікна

/ / Головна функція WinMain
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int){

while (GetMessage (& msg, NULL, 0,0)) / / Звичайний цикл
DispatchMessage (& msg); / / обробки повідомлень
return 0;
} / / Кінець WinMain
/ / Функція Register реєстрації класу вікна

/ / Функція Create створення і показу вікна
void Create(HINSTANCE hInst){
hMainWnd = CreateWindow (szClassName, szTitle, WS_OVERLAPPEDWINDOW,
10,10,200,100,HWND_DESKTOP,NULL,hInst,NULL);
ShowWindow(hMainWnd,SW_SHOWNORMAL);
}
/ / Віконна функція WndProc головного вікна
LRESULT CALLBACK WndProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch(msg){
HANDLE_MSG(hwnd,WM_COMMAND,OnCommand);
HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);
HANDLE_MSG(hwnd,WM_USER,OnUser);
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
}
}
/ / Функція OnCommand обробки повідомлень WM_COMMAND від команд меню
void OnCommand(HWND hwnd,int id,HWND,UINT){
switch(id){
case MI_START: / / Ініціалізація плати
InitCard();
break;
case MI_EXIT: / / Завершити додаток
DestroyWindow(hwnd);
}
}
/ / Функція OnUser обробки повідомлення WM_USER
void OnUser(HWND,WPARAM wParam){
char txt [80];
wsprintf (txt, "Виміри закінченіНакопичено% d подій ", (UINT) wParam);
MessageBox (hMainWnd, txt, "Контроль", MB_ICONINFORMATION);
}
/ / Функція OnDestroy обробки повідомлення WM_DESTROY

/ / Функція InitCard ініціалізації (через драйвер) плати
void InitCard (){
GetAPIEntry (); / / Отримання адреси точки входу в драйвер
_BX = 55; / / Канал 0
_CX = 20000; / / Канал 1; BX * CX = Тривалість інтервалу
_DX = 1000; / / Канал 2, внутрішній генератор
_DI = (UINT) hMainWnd; / / Дескриптор головного вікна
VxDEntry (); / / Виклик драйвера
}
/ / Функція GetAPIEntry отримання адреси точки входу в драйвер
void GetAPIEntry()

У наведеному вище тексті файлу. CPP детально показані тільки змінені
ділянки програми.


Змінилося визначення макросу HANDLE_WM_USER для обробки повідомлення
WM_USER, яке ми пошлемо в додаток з драйвера: функція обробки цього
повідомлення fn (у додатку вона носить назву OnUser ()) приймає два параметри
– Hwnd і wParam. Через параметр повідомлення wParam в додаток буде переданий
результат вимірювань. При необхідності передавати в додаток більший обсяг
даних можна було розширити склад параметрів функції третім параметром lParam.


Цикл обробки повідомлень істотно спростився і набув форми, звичайну для
простих додатків Windows.


У функції OnCommand () видалений фрагмент, пов'язаний з пунктом меню
<Читання> (ідентифікатор MI_READ), оскільки в цьому пункті вже немає
необхідності.


У функції OnUser () параметр wParam приводиться до типу цілого без знака,
перетвориться в символьну форму і виводиться на екран у вікно повідомлення з
відповідним текстом.


Як буде видно з програми драйвера (і як, втім, має бути очевидно
для читача), при посилці з драйвера повідомлення WM_USER необхідно вказати
системі, якому вікна адресоване це повідомлення. Однак драйверу, природно,
нічого не відомо про вікна додатків; дескриптор нашого головного вікна слід
передати драйверу на етапі ініціалізації плати. Ця операція виконується в
функції InitCard (), де перед викликом драйвера в регістри BX, CX і DX засилають
константи настроювання таймера, а регістр DI є приведеним до цілого типу
дескриптором головного вікна hMainWnd.


Подивимося тепер, як зміниться програма драйвера.



Програма драйвера для обслуговування апаратних переривань


WM_USER = SPM_UM_AlwaysSchedule +400 h; Код повідомлення WM_USER
include shell.inc; Додатковий включається файл

;======================
VxD_DATA_SEG
Data dw 0,0; 32-бітова осередок з даними для передачі в додаток
hwnd dd 0; 32-бітова осередок для отримання дескриптора вікна
IRQ_Handle dd 0; Дескриптор віртуального переривання
VMyD_Int13_Desc label dword; 32-бітову адресу наступній далі структури
VPICD_IRQ_Descriptor <5,, OFFSET32 VMyD_Int_13>; Структура з даними про переривання
VxD_DATA_ENDS
;======================
VxD_CODE_SEG
BeginProc VMyD_Control

EndProc VMyD_Control
;———————-
BeginProc VMyD_Device_Init

EndProc VMyD_Device_Init
;————————-
; API-процедура, що викликається з програми
; При виклику: BX = C0, CX = C1, DX = C2, DI = дескриптор головного вікна
BeginProc API_Handler
; Отримаємо параметри з програми
movzx EAX,[EBP.Client_DI]
mov hwnd,EAX
; Загальний скид

; Размаскіруем рівень 5 у фізичному контроллері переривань

; Засилаємо керуючі слова по каналах

; Засилаємо константи в канали

; Встановимо прапор S2 дозволу рахунки

ret
EndProc API_Handler
;————————-
; Процедура обробки апаратного переривання IRQ5 (вектор 13)
BeginProc VMyD_Int_13, High_Freq
; Отримаємо результат вимірювань з вихідного регістра лічильника

mov Data, AX; Результат в молодшій половині Data
; Виконаємо завершальні дії в PIC і викличемо функцію додатка
mov EAX,IRQ_Handle
VxDCall VPICD_Phys_EOI; EOI у фізичний контролер переривань
VxDCall VPICD_Physically_Mask; Маскуємо наш рівень
; Перейдемо на синхронний рівень. Це все інакше
push 0; Таймаут
push CAAFL_RING0; Подія кільця 0
push 0; Дані для передачі в процедуру відкладених переривань
push OFFSET32 Reflect_Int; викликається процедура відкладених переривань
VxDCall _SHELL_CallAtAppyTime; Викликати у "час програми"
add ESP, 4 * 4; Компенсація стека
clc
ret
EndProc VMyD_Int_13
;————————-
; Процедура рівня відкладених переривань. Це теж інакше
BeginProc Reflect_Int
push 0; Дані для функції зворотного виклику
push 0; Адреса функції зворотного виклику
push 0 ;lParam
push Data ;wParam
push WM_USER; Код повідомлення
push hwnd; Вікно-адресат
VxDCall _SHELL_PostMessage; Поставити повідомлення в чергу
add ESP, 4 * 6; Компенсація стека
clc
ret
EndProc Reflect_Int
VxD_CODE_ENDS
end VMyD_Real_Init

На початку тексту драйвера необхідно підключити ще один заголовний файл
SHELL.INC і значення константи WM_USER. У Windows ця константа має
довжину 16 біт і дорівнює 400h, проте функції _SHELL_PostMessage необхідно передати
32-бітове слово, причому сам код повідомлення WM_USER повинен знаходитися в молодшій
половині цього слова, а в старшу половину слід помістити інформацію про
диспетчеризації. У нашому випадку ця інформація виглядає як константа:
SPM_UM_AlwaysSchedule.


У сегменті даних видалені осередки для адреси функції зворотного виклику isr і
селектора DS. Осередок для результату вимірювань оголошена як два слова, оскільки
всі параметри функції Shell_PostMessage мають розмір 32 біт. Додана осередок
hwnd для отримання в неї з програми дескриптора головного вікна. Сам дескриптор
має розмір 16 біт, однак передавати його тієї ж функції Shell_PostMessage треба
у вигляді довгого слова.


На початку API-процедури зі структури клієнта (конкретно – з регістра DI)
витягується дескриптор вікна і після розширення до довгого слова поміщається в
клітинку hwnd.


Інші зміни стосуються лише способу переходу на рівень відкладених
переривань і складу процедури ReflectInt, що працює на цьому рівні.


Для переходу на синхронний рівень в даному випадку використовується системний
виклик _SHELL_CallAtAppyTime, який здійснює передачу управління зазначеної в
виклику процедурі ReflectInt у <час програми>, тобто коли
управління буде повернено з VMM в додаток. У цій процедурі вже можна
буде поставити повідомлення WM_USER в чергу повідомлень головного вікна нашого
додатки.


У процедурі рівня відкладених переривань ReflectInt після приміщення в стек
необхідних параметрів викликається системна функція _Shell_PostMessage, яка
і посилає в додаток повідомлення WM_USER. Легко побачити, що програміст повинен
в цьому випадку повністю сформувати весь склад структури повідомлення msg –
дескриптор вікна-адресата, код повідомлення, а також обидва параметри, що входять в усі
повідомлення Windows, – wParam і lParam. Параметром wParam ми в даному прикладі
користуємося для передачі в додаток результату вимірювання з осередку Data. При
необхідності можна було використовувати і lParam.


Функція зворотного виклику, адреса якої можна було вказати в числі
параметрів, викликається Windows після успішної постановки в чергу посилається
повідомлення. Ми цю функцію не використовуємо.


Для завдань управління апаратурою, що працює в режимі переривань, важливою
характеристикою є час відгуку на переривання, тобто тимчасова затримка
від моменту надходження переривання до виконання першої команди обробника. Як
ми побачили, при використанні віртуального драйвера системні витрати переходу
на прикладної обробник, включений до складу драйвера, складають близько 40
команд, на виконання яких на машині середньої продуктивності може
знадобитися 10 … 15 мкс. При використанні системи MS-DOS цих витрат не було
б зовсім, тому що в реальному режимі перехід на обробник переривань процесор
здійснює практично миттєво. Якщо ж реалізувати обробку переривань без
допомогою віртуального драйвера, як це було зроблено у попередній частині статті, то
перехід на прикладної обробник переривань зажадав би 200 … 300 команд, а
час затримки збільшилася б (на такому ж комп'ютері) до 120 … 180 мкс, тобто
більш ніж на порядок.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*