Взаимодействие между процессами
Из того, что DOS является однозадачной операционной системой, вовсе не следует, что в ней не могут существовать одновременно несколько процессов. Это только означает, что сама система не будет предоставлять никаких специальных возможностей для их одновременного выполнения, кроме возможности оставлять программы резидентными в памяти. Так, чтобы организовать общую память для нескольких процессов, надо загрузить пассивную резидентную программу, которая будет поддерживать функции выделения блока памяти (возвращающая идентификатор), определения адреса блока (по его идентификатору) и освобождения блока — приблизительно так же, как работают драйверы EMS или XMS.
Чтобы реализовать многозадачность, придется запустить активную резидентную программу, которая перехватит прерывание IRQ0 и по каждому такту системного таймера будет по очереди отбирать управление от каждого из запущенных процессов и передавать следующему. Практически никто не реализует полноценную многозадачность в 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 или аналогичной функции)
mov bx,1 ; цвет (синий) push bp mov bp,sp ; поместить все локальные переменные в стек, ; чтобы обеспечить повторную входимость push 1 ; добавка к X на каждом шаге x_inc 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 0A000h pop es ; ES - адрес видеопамяти
main_loop: ; основной цикл call display_line ; изобразить текущее состояние змейки
; изменить направление движения случайным образом push bx mov ebx,50 ; вероятность смены направления 2/50 call z_random ; получить случайное число от 0 до 49 mov ax,word ptr x_inc mov bx,word ptr y_inc test dx,dx ; если это число - 0, jz rot_right ; повернем направо, dec dx ; а если 1 - jnz exit_rot ; налево
; повороты neg ax ; налево на 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 ; восстановить цвет в ВХ
; перемещение змейки на одну позицию вперед mov di,word ptr coords_head ; DI - адрес головы mov cx,word ptr line_coords[di] ; СХ-строка mov dx,word ptr line_coords[di+2] ; DX-столбец add cx,word ptr y_inc ; добавить инкременты add dx,word ptr x_inc add di,4 ; DI - следующая точка в буфере, and di,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 для паузы, поэтому сделаем просто пустой цикл. Длину цикла ; придется изменить в зависимости от скорости процессора mov cx,-1 loop $ ; 65 535 команд 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_threads ; выключить многозадачность ; с этого момента у нас снова только один процесс mov ах,3 ; видеорежим 3 int 10h ; 80x24 int 20h ; конец программы
; процедура вывода точки на экран в режиме 13h ; СХ = строка, DX = столбец, BL = цвет, ES = A000h putpixel proc near push di lea ecx,[ecx*4+ecx] ; CX = строка * 5 shl cx,6 ; CX = строка * 5 * 64 = строка * 320 add dx,cx ; DX = строка * 320 + столбец = адрес mov di,dx mov al,bl stosb ; записать байт в видеопамять pop di ret putpixel endp
; процедура display_line ; выводит на экран нашу змейку по координатам из кольцевого буфера line_coords display_line proc near mov di,word ptr coords_tail ; начать вывод с хвоста, continue_line_display: cmp di,word ptr coords_head ; если DI равен адресу головы, je line_displayed ; вывод закончился, call display_point ; иначе - вывести точку на экран, add di,4 ; установить DI на следующую точку and di,127 jmp short continue_line_display ; и так далее line_displayed: call display_point mov di,word ptr coords_tail ; вывести точку в хвосте push bx mov bx,0 ; нулевым цветом, call display_point ; то есть стереть pop bx ret display_line endp
; процедура display_point ; выводит точку из буфера line_coords с индексом DI display_point proc near mov cx,word ptr line_coords[di] ; строка mov dx,word ptr line_coords[di+2] ; столбец call putpixel ; вывод точки ret display_point endp
; процедура z_random ; стандартный конгруэнтный генератор случайных чисел (неоптимизированный) ; ввод: ЕВХ - максимальное число ; вывод: EDX - число от 0 до ЕВХ-1 z_random: push ebx cmp byte ptr zr_init_flag,0 ; если еще не вызывали, je zr_init ; инициализироваться, mov eax,zr_prev_rand ; иначе - умножить предыдущее zr_cont: mul rnd_number ; на множитель div rnd_number2 ; и разделить на делитель, mov zr_prev_rand,edx ; остаток от деления - новое число pop ebx mov eax,edx xor edx,edx div ebx ; разделить его на максимальное ret ; и вернуть остаток в EDX zr_init: push 0040h ; инициализация генератора pop fs ; 0040h:006Ch - mov eax,fs:[006Ch] ; счетчик прерываний таймера BIOS, mov zr_prev_rand,eax ; он и будет первым случайным числом mov byte ptr zr_init_flag,1 jmp zr_cont rnd_number dd 16807 ; множитель rnd_number2 dd 2147483647 ; делитель zr_init_flag 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 ? thread_struc ends
; процедура init_threads ; инициализирует обработчик прерывания 08h и заполняет структуры, описывающие ; обе нити init_threads proc near pushf pusha push es mov ax,3508h ; AH = 35h, AL = номер прерывания int 21h ; определить адрес обработчика, mov word ptr old_int08h,bx ; сохранить его mov word ptr old_int08h+2,es mov ax,2508h ; AH = 25h, AL = номер прерывания mov dx,offset int08h_handler ; установить наш int 21h pop es popa ; теперь регистры те же, что и при вызове процедуры popf
mov thread1._ax,ax ; заполнить структуры mov thread2._ax,ax ; threadl и thread2, mov thread1._bx,bx ; в которых хранится содержимое mov thread2._bx,bx ; всех регистров (кроме сегментных - mov thread1._cx,cx ; они в этом примере не изменяются) mov thread2._cx,cx mov thread1._dx,dx mov thread2._dx.dx mov thread1._si,si mov thread2._si,si mov thread1._di,di mov thread2._di,di mov thread1._bp,bp mov thread2._bp,bp mov thread1._sp,offset thread1_stack+512 mov thread2._sp,offset thread2_stack+512 pop ax ; адрес возврата (теперь стек пуст) mov thread1._ip,ax mov thread2._ip,ax pushf pop ax ; флаги mov thread1._flags,ax mov thread2._flags,ax mov sp,thread1._sp ; установить стек нити 1 jmp word ptr thread1._ip ; и передать ей управление init_threads endp
current_thread db 1 ; номер текущей нити
; Обработчик прерывания INT08h (IRQ0) переключает нити 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 ; сохранить DI, SI mov save_si,si pushf ; и флаги ; определить, с какой нити на какую надо передать управление, cmp byte ptr current_thread,1 ; если с первой, je thread1_to_thread2 ; перейти на thread1_to_thread2, mov byte ptr current_thread,1 ; если с 2 на 1, записать ; в номер 1 mov si,offset thread1 ; и установить SI и DI mov di,offset thread2 ; на соответствующие структуры, jmp short order_selected thread1_to_thread2: ; если с 1 на 2, mov byte ptr current_thread,2 ; записать в номер нити 2 mov si,offset thread2 ; и установить SI и DI mov di,offset thread1 order_selected: ; записать все текущие регистры в структуру по адресу [DI] ; и загрузить все регистры из структуры по адресу [SI] ; начать с SI и DI: mov ax,[si]._si ; для MASM все выражения [reg]._reg надо push save_si ; заменить на (thread_struc ptr [reg])._reg 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._flags] popf ; адрес возврата pop [di._ip] ; адрес возврата из стека add sp,4 ; CS и флаги из стека - теперь он пуст ; переключить стеки mov [di._sp],sp mov sp,[si._sp] push [si._ip] ; адрес возврата в стек (уже новый) mov di,save_di ; загрузить DI и SI mov si,save_si retn ; и перейти по адресу в стеке ; управление переходит сюда, если прерывание произошло в чужом коде called_far: popf ; восстановить регистры pop bx pop ax mov bp,save_di iret ; и завершить обработчик int08h_handler endp
save_di dw ? ; переменные для временного хранения save_si dw ? ; регистров
; процедура shutdown_threads ; выключает диспетчер shutdown_threads proc near mov ax,2508h ; достаточно просто восстановить прерывание lds dx,dword ptr old_int08h int 21h ret shutdown_threads endp
; структура, описывающая первую нить thread1 thread_struc <> ; и вторую, thread2 thread_struc <> ; стек первой нити thread1_stack db 512 dup(?) ; и второй thread2_stack db 512 dup(?) end start
Как мы видим, этот пример не может работать в Windows 95 и в некоторых других случаях, когда DOS расширяют до более совершенной операционной системы. Фактически в этом примере мы именно этим и занимались — реализовывали фрагмент операционной системы, который отсутствует в DOS.
Действительно, используя механизм обработчиков прерываний, можно создать операционную систему для реального режима, аналогичную DOS, но очень быстро окажется, что для этого придется общаться напрямую с аппаратным обеспечением компьютера, то есть использовать порты ввода-вывода.
Содержание раздела