5.9.5. Взаимодействиемеждупроцессами

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

приблизительно так же, как работают драйверы EMS или XMS.

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

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

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

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

Попробуем создать элементарный прототип такой многозадачности в DOS (все­го с двумя нитями) и посмотрим, со сколькими проблемами придется столкнуться.

scrsvr.asm

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

; Передача управления между нитями не работает в окне DOS (Windows 95).

.model tiny .code

.386 ;   ГСП использует 32-битные регистры,

org 100h ; СОМ-программа.

start:

mov . ax,13h ;  Видеорежим 13h:

int 10h ; 320x200x256.

call      init_threads       ;  Инициализировать наш диспетчер. ;  С этого места и до вызова shutdown_threads исполняются две нити с одним и тем ; же кодом и данными, но с разными регистрами и стеками ;   (в реальной системе здесь был бы вызов fork или аналогичной функции).

x_inc

mov

push mov

push

bx,1

bp

bp, sp

1 ; Цвет (синий).

;   Поместить все локальные переменные в стек, ;  чтобы обеспечить повторную входимость. ;  Добавка к X на каждом шаге.

equ word ptr [bp-2]

push 0 ; Добавка к Y на каждом шаге.

y_inc equ word ptr [bp-4]

push 128-4 ;  Относительный адрес головы буфера line.coords.

coords_head equ word ptr [bp-6]

push 0 ;  Относительный адрес хвоста буфера line.coords.

coords.tail equ word ptr [bp-8]

sub sp,64»2 ;  line.coords - колвцевой буфер координат точек.

mov di.sp

mov cx,64

mov ax, 10 ;  Заполнитв его координатами (10, 10).

push ds

pop es

rep stosw

line_coords equ word ptr [bp-(64*2)-8]

push OAOOOh

pop es ;  ES - адрес видеопамяти.

; Основной цикл.

call display_iine ;  Изобразитв текущее состояние змейки.

; Изменить направление движения случайным образом.

push bx

mov ebx,50 ;  Вероятности смены направления 2/50.

call z_ random ;  Получитв случайное число от 0 до 49..

mov ах,word ptr x^inc

mov bx.word ptr y_inc

test dx.dx Если это число - О,

jz rot_right повернем направо,

dec dx а если 1 -

jnz exit_rot налево.

Повороты

neg ах         / налево на 90 градусов.

xchg ax,bx dY = -dX, dX = dY.

jmp short exit.rot

rot_right:

neg bx ; Направо на 90 градусов.

xchg ax.bx ;  dY = dX,  dX = dY.

exit_rot:

mov word ptr x_inc,ax ;  Записать новые значения инкрементов,

mov word ptr y_inc,bx

pop bx Восстановитв цвет в BX.

; Перемещение змейки на одну позицию вперед.

mov di,word ptr coordsjiead DI - адрес головы,

mov cx.word ptr line_coords[di] CX - строка,

mov dx.word ptr line_coorcls[di+2] DX - столбец,

add cx.word ptr y_inc Добавитв инкременты,

add dx.word ptr x_lnc

add di,4 01 - следующая точка в буфере,

and <Ji, 127       ' Если DI > 128,   DI = DI - 128.

mov     " word ptr coords„head,di Теперв голова здесв.

mov

word ptr

line_coords[di],cx

; Записать ее координаты.

mov

word ptr

line_coords[di+2], dx

 

mov

di.word

ptr coords_tail

; Прочитать адрес хвоста.

add

di,4

 

; Переместить его на одну

and

di,127

 

; позицию вперед

mov

word ptr

coords_tail,di

; и записать на место.

Пауза.

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

65 535 команд loop.

mov

loop

$

mov

cx, -1

loop

$

mov

cx, -1

loop

$

mov

ah,1

int

16h

jz

main_loop

mov

ah,0

int

16h

leave

 

call

shutdown

Если ни одна из клавиш не продолжить основной цикл. Иначе - прочитать клавишу.

была нажата,

; С этого момента у нас снова только шоу ах,3 и± Юл Ш 20п

Процедура вывода точки на экран ; СХ = строка, БХ = столбец, Вй : рисртхе!

; Освободить стек от локальных переменных.

; Выключите многозадачность.

один процесс.

; Видеорежим 3:

; 80x24.

; Конец программы.

в режиме

цвет,   Е8 = 0А000Й

 

proc near

 

 

push

di

 

 

lea

есх,[есх*4+есх]

: CX = строка x

5.

shl

6

; CX = строка x

5 x 64 = строка

add

dx, cx

;  DX = строка x

320 + столбец =

mov

di, dx

 

 

mov

al.bl

 

 

stosb

 

Записать байт

в видеопамять.

pop

di

 

 

ret

endp

 

 

x 320. адрес.

putpixel

; Процедура display_line. '

;  Выводит на экран нашу змейку по координатам из кольцевого буфера line_coords.

display_line    proc near

mov        di.word ptr coords.tail continue_line_display: ,

ptr

je line_displayed

Начать вывод с хвоста,

Если Б1 равен адресу головы, вывод закончился.

call

add and

imp

line_displayed:

call

mov

push

mov

call

pop

ret

display_line

;

;

di, 127

short continue_line_display display_point

di.word ptr coords.tail ;

bx

bx,0 ;

display_polnt

bx

11111

.Иначе - вывести точку на экран. Установить Б1 на следующую точку.

;  И так далее.

Вывести точку в хвосте

нулевым цветом, то есть'стереть.

endp

; Процедура display.point. ;  Выводит точку из буфера line_coords

proc near

cx.word ptr line_coords[di] dx.word ptr line_coords[di+2]

putpixel

с индексом

display_point

mov mov

call

ret

display_point

Cтрока. Cтолбец. Вывод точки.

endp

Процедура z_random.

Cтандартный Вход: EBX -Выход: EDX z_random:

push

cmp

je ■ mov

zr_cont:

mul

div mov pop mov xor div

ret

конгруэнтный генератор случайных чисел (неоптимизированный). максималвное число. ■ число от 0 до ЄВХ-1.

zr_init:

push

pop mov mov . mov jmp rnd_number rnd nurober2

ebx

byte ptr zr_init_flag,0

zrjnit

eax,"zr_prev_rand

rnd_number rnd_number2

zr_prev_rand,edx

ebx

eax,edx edx,edx

ebx

0040h fs

eax,fs:[006Ch] eax

byte ptr zr_init_flag,1

zr_cont

dd 16807

dd 2147483647

.Если еще не вызывали, инициализироваться.

Иначе - умножить предыдущее

на множитель и разделить на

делитель.

Остаток -от деления - новое число.

Разделить его на максимальное и вернуть остаток в EDX.

Инициализация генератора. 0040гг. ООбСп -

счетчик прерываний таймера BIOS, он и будет первым ' случайным числом.

Множитель. Делитель.

db 0                    ; Флаг инициализации

zr prev rand     dd 0 ; Предыдущее случайное число.

;   Здесь начинается код диспетчера,   обеспечивающего многозадачность.

;  йруктура данных, в которой мы храним регистры для каждой нити. thread_struc struc

_ах                   dw ?

_bx             dw ?

_cx                   dw ?

_dx             dw ?

_si            dw ?

_di     .       dw ?

_bp                   dw ?

_sp                  dw ?

_ip                   dw ?

_flags             dw ?

thre'acLstruc ends

; Процедура

; Инициализирует обработчик прерывания 08h и заполняет структуры, описывающие ; обе нити, .

proc near

pushf

 

 

pusha

 

 

push

es

 

mov

ax,3508h

АН = 35Гр  АЬ = номер прерывания.

int

21h

;  Определить адрес обработчика.

mov

word ptr old_int08h,bx .

; Сохранить его.

mov

word ptr old_int_08h+2, es

 

mov

;

АН = 25п, АЬ = номер прерывания.

mov

dx, offset int08h_handler

Установить наш.

int

21h

 

pop

es

 

popa

; Теперь регистры

те же, что и при вызове процедуры.

popf

 

 

mov

threadl._ax,ax

Заполнить структуры

mov

;

и

mov

threadl._bx,bx

в которых хранится содержимое

mov

thread2._bx,bx

;   всех регистров  (кроме сегментных

mov

;

они в этом примере не изменяются)

mov

thread2._cx,cx

 

mov

threadl._dx,dx

 

mov

thread2._dx,dx

 

mov

threadl._si,si

 

mov

thread2._si,si •

 

mov

threadl._di,di

 

mov

thread2._di,di

 

mov

threadl._bp,bp

 

mov

thread2._bp,bp

 

mov mov

pop

mov

mov

pushf

pop

mov

mov

mov

jmp

init_threads

current thread

threadl._sp,offset thread1_stack+512 thread2.,sp,offset thread2_stack+512

ax

threadl,_ip,ax thread2._ip,ax

ax

threadl._flags, ax thread2._flags,ax sp,threadl._sp

word ptr

endp

Адрес возврата (теперь стек пуст).

; Флаги.

Установить и передать

стек нити 1 ей управление.

db

1

; Номер текущей нити.

;  Обработчик прерывания INT08h (IRQO) ; переключает нити int08h_handler proc far

' pushf       ■ ; Сначала вызватв старый обработчик,

db        9Ah ; Код команды call far.

old_int08h dd 0 ; Адрес старого обработчика.

Определить, произошло ли прерывание в момент исполнения нашей нити или какого-то обработчика другого прерывания. Это важно, так как мы не собираемся возвращать управление чужой программе, по крайней мере сейчас. Вот почему нельзя пользоваться прерываниями для задержек в наших нитях и программа не работает в окне DOS  (Windows 95).

mov

save_di, bp

; Сохранить ВР.

mov

bp, sp

 

push

ax

 

push

bx

 

pushf

 

 

mov

ax, word ptr [bp+2]

; Прочитать сегментную'часть

mov

bx, cs

; обратного адреса.

cmp

ax,bx ■

; Сравнить ее с CS.

jne

called_far

; Если они не совпадают - выйти.

popf

 

 

pop

bx

; Иначе - восстановить регистры.

pop

ax

 

mov

bp,save_di

 

mov

save_di,di

Сохранить 01, SI

mov

save_si,si

 

pushf

 

и флаги.

лить, с

какой нити на какую надо

передать

cmp

byte ptr current_thread,1

Если с первой,

je

threadl to_thread2

; перейти на ^геааі_[о_іЬгеай2.

mov

byte ptr current_thread, 1

: Если с 2 на 1,  записать в номер

mov

si,offset threadl

; и установить SI и DI

mov

di,offset thread2

; на соответствующие структуры.

jmp

short order_selected

 

1

thread1_to_thread2: ;   Если с 1 на 2,

mov      byte ptr current_thread, 2 ;  записать в номер нити 2

;  и установить SI и

mov       di,offset threadl order_selected:

Записатв все текущие регистры в структуру по адресу [Ш] и загрузитв все регистры из структуры по адресу [81]. начатв с 81 и БІ:

mov

ax, [si]._si

push

save_si

pop

[di]._si

mov

save_si,ax

mov

ax, [si], di

push

save_di

pop

[di]._di

mov

save_di,ax

Теперь все

основные регистры

mov

[di._ax],ax

mov

ax, [si._ax]

mov

[di._bx], bx

mov

bx, [si._bx]

mov

[di._cx], cx

mov

cx, [si. cx]

mov

[di._dx],dx

mov

dx, [si._dx]

mov

[di._bp],bp

mov

bp, [si._bp]

Флаги.

 

pop

[di._flags]

push

[si.J lags]

popf

 

Адрес возврата.

pop

[di._ip]

add

sp,4

Переключить

стеки.

mov

[di._sp], sp

mov

sp,[si._sp]

push

[si._ip]

mov

di,save_di

• mov

si,save_si

retn

 

Для MASM все выражения [reg]._reg надо заменить (thread_struc ptr [reg])._reg.

Адрес возврата из стека.

и флаги из стека - теперь он пуст.

; Адрес возврата в стек (уже новый). ; Загрузить 01 и SI

; и пере*йти по адресу в стеке. ; Управление переходит сюда, если прерывание произошло в чужом коде.

called far: popf

pop bx pop ax mov       bp, savejd і ret

int08h_handler endp

; Восстановить регистры

завершить обработчик.

save_di dw ?

save_si dw ?

;  Процедура shutdowns-threads. ;   Выключает диспетчер. shutdown_threads proc near

mov        ax,2508h ;   Достаточно просто восстановить прерывание.

Ids       dx,dword ptr old_int08h

int 21h

ret

shutdown_threads endp

;  Структура,  описывающая первую нить. threadl thread_struc <>

;  И вторую.

thread2 thread^struc о

;   Стек первой нити. thread1_stack db 512 dup(?)

; И второй.

thread2_stack db 512 dup(?) end start

Как мы видим, этот пример не может работать в Windows 95 и в некоторых других случаях, когда DOS расширяют до более совершенной операционной сис­темы. Фактически в этом примере мы именно этим и занимались — реализовыва-ли фрагмент операционной системы, который отсутствует в DOS.

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