[Назад] [Далее]

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

Из того, что 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, но очень быстро окажется, что для этого придется общаться напрямую с аппаратным обеспечением компьютера, то есть использовать порты ввода-вывода.


п»ї
"target=_blank><\/a>") //-->