WaitSemaph
and RlsSemaph
. These functions wait and signal a semaphore, respectively. These routines mesh with the process management facilities, making it easy to implement synchronization using semaphores in your programs.semaphore struct SemaCnt word 1 smaphrLst dword ? endsmaphrLst dword ? semaphore ends
The SemaCnt
field determines how many more processes can share a resource (if positive), or how many processes are currently waiting for the resource (if negative). By default, this field is initialized to the value one. This allows one process at a time to use the resource protected by the semaphore. Each time a process waits on a semaphore, it decrements this field. If the decremented result is positive or zero, the wait operation immediately returns. If the decremented result is negative, then the wait operation moves the current process' pcb
from the run queue to the semaphore queue defined by the smaphrLst
and endsmaphrLst
fields in the structure above.
Most of the time you will use the default value of one for the SemaCnt
field. There are some occasions, though, when you might want to allow more than one process access to some resource. For example, suppose you've developed a multiplayer game that communicates between different machines using the serial communications port or a network adapter card. You might have an area in the game which has room for only two players at a time. For example, players could be racing to a particular "transporter" room in an alien space ship, but there is room for only two players in the transporter room at a time. By initializing the semaphore variable to two, rather than one, the wait operation would allow two players to continue at one time rather than just one. When the third player attempts to enter the transporter room, the WaitSemaph
function would block the player from entering the room until one of the other players left (perhaps by "transporting out" of the room).
To use the WaitSemaph
or RlsSemaph
function is very easy; just load the es:di register pair with the address of desired semaphore variable and issue the appropriate function call. RlsSemaph
always returns immediately (assuming a timer interrupt doesn't occur while in RlsSemaph
), the WaitSemaph
call returns when the semaphore will allow access to the resource it protects. Examples of these two calls appear in the next section.
Like the Standard Library coroutine and process packages, the semaphore package only preserves the 16 bit register set of the 80x86 CPU. If you want to use the 32 bit register set of the 80386 and later processors, you will need to modify the source code for the WaitSemaph
and RlsSemaph
functions. The code you need to change is almost identical to the code in the coroutine and process packages, so this is nearly a trivial change. Do keep in mind, though, that you will need to change this code if you use any 32 bit facilities of the 80386 and later processors.
DOSsmaph
variable and issue appropriate WaitSemaph
and RlsSemaph
calls around the call to DOS. The following sample code demonstrates how to do this.; MULTIDOS.ASM ; ; This program demonstrates how to use semaphores to protect DOS calls. .xlist include stdlib.a includelib stdlib.lib .list dseg segment para public 'data' DOSsmaph semaphore {} ; Macros to wait and release the DOS semaphore: DOSWait macro push es push di lesi DOSsmaph WaitSemaph pop di pop es endm DOSRls macro push es push di lesi DOSsmaph RlsSemaph pop di pop es endm ; PCB for our background process: BkgndPCB pcb {0,offset EndStk2, seg EndStk2} ; Data the foreground and background processes print: StrPtrs1 dword str1_a, str1_b, str1_c, str1_d, str1_e, str1_f dword str1_g, str1_h, str1_i, str1_j, str1_k, str1_l dword 0 str1_a byte "Foreground: string 'a'",cr,lf,0 str1_b byte "Foreground: string 'b'",cr,lf,0 str1_c byte "Foreground: string 'c'",cr,lf,0 str1_d byte "Foreground: string 'd'",cr,lf,0 str1_e byte "Foreground: string 'e'",cr,lf,0 str1_f byte "Foreground: string 'f'",cr,lf,0 str1_g byte "Foreground: string 'g'",cr,lf,0 str1_h byte "Foreground: string 'h'",cr,lf,0 str1_i byte "Foreground: string 'i'",cr,lf,0 str1_j byte "Foreground: string 'j'",cr,lf,0 str1_k byte "Foreground: string 'k'",cr,lf,0 str1_l byte "Foreground: string 'l'",cr,lf,0 StrPtrs2 dword str2_a, str2_b, str2_c, str2_d, str2_e, str2_f dword str2_g, str2_h, str2_i dword 0 str2_a byte "Background: string 'a'",cr,lf,0 str2_b byte "Background: string 'b'",cr,lf,0 str2_c byte "Background: string 'c'",cr,lf,0 str2_d byte "Background: string 'd'",cr,lf,0 str2_e byte "Background: string 'e'",cr,lf,0 str2_f byte "Background: string 'f'",cr,lf,0 str2_g byte "Background: string 'g'",cr,lf,0 str2_h byte "Background: string 'h'",cr,lf,0 str2_i byte "Background: string 'i'",cr,lf,0 dseg ends cseg segment para public 'code' assume cs:cseg, ds:dseg ; A replacement critical error handler. This routine calls prcsquit ; if the user decides to abort the program. CritErrMsg byte cr,lf byte "DOS Critical Error!",cr,lf byte "A)bort, R)etry, I)gnore, F)ail? $" MyInt24 proc far push dx push ds push ax push cs pop ds Int24Lp: lea dx, CritErrMsg mov ah, 9 ;DOS print string call. int 21h mov ah, 1 ;DOS read character call. int 21h and al, 5Fh ;Convert l.c. -> u.c. cmp al, 'I' ;Ignore? jne NotIgnore pop ax mov al, 0 jmp Quit24 NotIgnore: cmp al, 'r' ;Retry? jne NotRetry pop ax mov al, 1 jmp Quit24 NotRetry: cmp al, 'A' ;Abort? jne NotAbort prcsquit ;If quitting, fix INT 8. pop ax mov al, 2 jmp Quit24 NotAbort: cmp al, 'F' jne BadChar pop ax mov al, 3 Quit24: pop ds pop dx iret BadChar: mov ah, 2 mov dl, 7 ;Bell character jmp Int24Lp MyInt24 endp ; We will simply disable INT 23h (the break exception). MyInt23 proc far iret MyInt23 endp ; This background process calls DOS to print several strings to the ; screen. In the meantime, the foreground process is also printing ; strings to the screen. To prevent reentry, or at least a jumble of ; characters on the screen, this code uses semaphores to protect the ; DOS calls. Therefore, each process will print one complete line ; then release the semaphore. If the other process is waiting it will ; print its line. BackGround proc mov ax, dseg mov ds, ax lea bx, StrPtrs2 ;Array of str ptrs. PrintLoop: cmp word ptr [bx+2], 0 ;At end of pointers? je BkGndDone les di, [bx] ;Get string to print. DOSWait puts ;Calls DOS to print string. DOSRls add bx, 4 ;Point at next str ptr. jmp PrintLoop BkGndDone: die BackGround endp Main proc mov ax, dseg mov ds, ax mov es, ax meminit ; Initialize the INT 23h and INT 24h exception handler vectors. mov ax, 0 mov es, ax mov word ptr es:[24h*4], offset MyInt24 mov es:[24h*4 + 2], cs mov word ptr es:[23h*4], offset MyInt23 mov es:[23h*4 + 2], cs prcsinit ;Start multitasking system. lesi BkgndPCB ;Fire up a new process fork test ax, ax ;Parent's return? je ParentPrcs jmp BackGround ;Go do backgroun stuff. ; The parent process will print a bunch of strings at the same time ; the background process is doing this. We'll use the DOS semaphore ; to protect the call to DOS that PUTS makes. ParentPrcs: DOSWait ;Force the other process mov cx, 0 ; to wind up waiting in DlyLp0: loop DlyLp0 ; the semaphore queue by DlyLp1: loop DlyLp1 ; delay for at least one DlyLp2: loop DlyLp2 ; clock tick. DOSRls lea bx, StrPtrs1 ;Array of str ptrs. PrintLoop: cmp word ptr [bx+2], 0 ;At end of pointers? je ForeGndDone les di, [bx] ;Get string to print. DOSWait puts ;Calls DOS to print string. DOSRls add bx, 4 ;Point at next str ptr. jmp PrintLoop ForeGndDone: prcsquit Quit: ExitPgm ;DOS macro to quit program. Main endp cseg ends sseg segment para stack 'stack' ; Here is the stack for the background process we start stk2 byte 1024 dup (?) EndStk2 word ? ;Here's the stack for the main program/foreground process. stk byte 1024 dup (?) sseg ends zzzzzzseg segment para public 'zzzzzz' LastBytes db 16 dup (?) zzzzzzseg ends end Main
This program doesn't directly call DOS, but it calls the Standard Library puts
routine that does. In general, you could use a single semaphore to protect all BIOS, DOS, and Standard Library calls. However, this is not particularly efficient. For example, the Standard Library pattern matching routines make no DOS calls; therefore, waiting on the DOS semaphore to do a pattern match while some other process is making a DOS call unnecessarily delays the pattern match. There is nothing wrong with having one process do a pattern match while another is making a DOS call. Unfortunately, some Standard Library routines do make DOS calls (puts
is a good example), so you must use the DOS semaphore around such calls.
In theory, we could use separate semaphores to protect DOS, different BIOS calls, and different Standard Library calls. However, keeping track of all those semaphores within a program is a big task. Furthermore, ensuring that a call to DOS does not also invoke an unprotected BIOS routine is a difficult task. So most programmers use a single semaphore to protect all Standard Library, DOS, and BIOS calls.
WaitSemaph
and RlsSemaph
routines to implement barriers. However, it turns out to be quite simple. Suppose we were to initialize the semaphore's SemaCnt
field to zero rather than one. When the first process waits on this semaphore, the system will immediately block that process. Likewise, each additional process that waits on this semaphore will block and wait on the semaphore queue. This would normally be a disaster since there is no active process that will signal the semaphore so it will activate the blocked processes. However, if we modify the wait call so that it checks the SemaCnt
field before actually doing the wait, the nth process can skip the wait call and reactivate the other processes. Consider the following macro:barrier macro Wait4Cnt local AllHere, AllDone cmp es:[di].semaphore.SemaCnt, -(Wait4Cnt-1) jle AllHere WaitSemaph cmp es:[di].semaphore.SemaCnt, 0 je AllDone AllHere: RlsSemaph AllDone: endm
This macro expects a single parameter that should be the number of processes (including the current process) that need to be at a barrier before any of the processes can proceed. The SemaCnt
field is a negative number whose absolute value determines how many processes are currently waiting on the semaphore. If a barrier requires four processes, no process can proceed until the fourth process hits the barrier; at that time the SemaCnt
field will contain minus three. The macro above computes what the value of SemaCnt
should be if all processes are at the barrier. If SemaCnt
matches this value, it signals the semaphore that begins a chain of operations with each blocked process releasing the next. When SemaCnt
hits zero, the last blocked process does not release the semaphore since there are no other processes waiting on the queue.
It is very important to remember to initialize the SemaCnt field to zero before using semaphores for barrier synchronization in this manner. If you do not initialize SemaCnt
to zero, the WaitSemaph
call will probably not block any of the processes.
The following sample program provides a simple example of barrier synchronization using the Standard Library's semaphore package:
; BARRIER.ASM ; ; This sample program demonstrates how to use the Standard Library's ; semaphore objects to synchronize several processes at a barrier. ; This program is similar to the MULTIDOS.ASM program insofar as the ; background processes all print a set of strings. However, rather than ; using an inelegant delay loop to synchronize the foreground and background ; processes, this code uses barrier synchronization to achieve this. .xlist include stdlib.a includelib stdlib.lib .list dseg segment para public 'data' BarrierSemaph semaphore {0} ;Must init SemaCnt to zero. DOSsmaph semaphore {} ; Macros to wait and release the DOS semaphore: DOSWait macro push es push di lesi DOSsmaph WaitSemaph pop di pop es endm DOSRls macro push es push di lesi DOSsmaph RlsSemaph pop di pop es endm ; Macro to synchronize on a barrier: Barrier macro Wait4Cnt local AllHere, AllDone cmp es:[di].semaphore.SemaCnt, -(Wait4Cnt-1) jle AllHere WaitSemaph cmp es:[di].semaphore.SemaCnt, 0 jge AllDone AllHere: RlsSemaph AllDone: endm ; PCBs for our background processes: BkgndPCB2 pcb {0,offset EndStk2, seg EndStk2} BkgndPCB3 pcb {0,offset EndStk3, seg EndStk3} ; Data the foreground and background processes print: StrPtrs1 dword str1_a, str1_b, str1_c, str1_d, str1_e, str1_f dword str1_g, str1_h, str1_i, str1_j, str1_k, str1_l dword 0 str1_a byte "Foreground: string 'a'",cr,lf,0 str1_b byte "Foreground: string 'b'",cr,lf,0 str1_c byte "Foreground: string 'c'",cr,lf,0 str1_d byte "Foreground: string 'd'",cr,lf,0 str1_e byte "Foreground: string 'e'",cr,lf,0 str1_f byte "Foreground: string 'f'",cr,lf,0 str1_g byte "Foreground: string 'g'",cr,lf,0 str1_h byte "Foreground: string 'h'",cr,lf,0 str1_i byte "Foreground: string 'i'",cr,lf,0 str1_j byte "Foreground: string 'j'",cr,lf,0 str1_k byte "Foreground: string 'k'",cr,lf,0 str1_l byte "Foreground: string 'l'",cr,lf,0 StrPtrs2 dword str2_a, str2_b, str2_c, str2_d, str2_e, str2_f dword str2_g, str2_h, str2_i dword 0 str2_a byte "Background 1: string 'a'",cr,lf,0 str2_b byte "Background 1: string 'b'",cr,lf,0 str2_c byte "Background 1: string 'c'",cr,lf,0 str2_d byte "Background 1: string 'd'",cr,lf,0 str2_e byte "Background 1: string 'e'",cr,lf,0 str2_f byte "Background 1: string 'f'",cr,lf,0 str2_g byte "Background 1: string 'g'",cr,lf,0 str2_h byte "Background 1: string 'h'",cr,lf,0 str2_i byte "Background 1: string 'i'",cr,lf,0 StrPtrs3 dword str3_a, str3_b, str3_c, str3_d, str3_e, str3_f dword str3_g, str3_h, str3_i dword 0 str3_a byte "Background 2: string 'j'",cr,lf,0 str3_b byte "Background 2: string 'k'",cr,lf,0 str3_c byte "Background 2: string 'l'",cr,lf,0 str3_d byte "Background 2: string 'm'",cr,lf,0 str3_e byte "Background 2: string 'n'",cr,lf,0 str3_f byte "Background 2: string 'o'",cr,lf,0 str3_g byte "Background 2: string 'p'",cr,lf,0 str3_h byte "Background 2: string 'q'",cr,lf,0 str3_i byte "Background 2: string 'r'",cr,lf,0 dseg ends cseg segment para public 'code' assume cs:cseg, ds:dseg ; A replacement critical error handler. This routine calls prcsquit ; if the user decides to abort the program. CritErrMsg byte cr,lf byte "DOS Critical Error!",cr,lf byte "A)bort, R)etry, I)gnore, F)ail? $" MyInt24 proc far push dx push ds push ax push cs pop ds Int24Lp: lea dx, CritErrMsg mov ah, 9 ;DOS print string call. int 21h mov ah, 1 ;DOS read character call. int 21h and al, 5Fh ;Convert l.c. -> u.c. cmp al, 'I' ;Ignore? jne NotIgnore pop ax mov al, 0 jmp Quit24 NotIgnore: cmp al, 'r' ;Retry? jne NotRetry pop ax mov al, 1 jmp Quit24 NotRetry: cmp al, 'A' ;Abort? jne NotAbort prcsquit ;If quitting, fix INT 8. pop ax mov al, 2 jmp Quit24 NotAbort: cmp al, 'F' jne BadChar pop ax mov al, 3 Quit24: pop ds pop dx iret BadChar: mov ah, 2 mov dl, 7 ;Bell character jmp Int24Lp MyInt24 endp ; We will simply disable INT 23h (the break exception). MyInt23 proc far iret MyInt23 endp ; This background processes call DOS to print several strings to the ; screen. In the meantime, the foreground process is also printing ; strings to the screen. To prevent reentry, or at least a jumble of ; characters on the screen, this code uses semaphores to protect the ; DOS calls. Therefore, each process will print one complete line ; then release the semaphore. If the other process is waiting it will ; print its line. BackGround1 proc mov ax, dseg mov ds, ax ; Wait for everyone else to get ready: lesi BarrierSemaph barrier 3 ; Okay, start printing the strings: lea bx, StrPtrs2 ;Array of str ptrs. PrintLoop: cmp word ptr [bx+2], 0 ;At end of pointers? je BkGndDone les di, [bx] ;Get string to print. DOSWait puts ;Calls DOS to print string. DOSRls add bx, 4 ;Point at next str ptr. jmp PrintLoop BkGndDone: die BackGround1 endp BackGround2 proc mov ax, dseg mov ds, ax lesi BarrierSemaph barrier 3 lea bx, StrPtrs3 ;Array of str ptrs. PrintLoop: cmp word ptr [bx+2], 0 ;At end of pointers? je BkGndDone les di, [bx] ;Get string to print. DOSWait puts ;Calls DOS to print string. DOSRls add bx, 4 ;Point at next str ptr. jmp PrintLoop BkGndDone: die BackGround2 endp Main proc mov ax, dseg mov ds, ax mov es, ax meminit ; Initialize the INT 23h and INT 24h exception handler vectors. mov ax, 0 mov es, ax mov word ptr es:[24h*4], offset MyInt24 mov es:[24h*4 + 2], cs mov word ptr es:[23h*4], offset MyInt23 mov es:[23h*4 + 2], cs prcsinit ;Start multitasking system. ; Start the first background process: lesi BkgndPCB2 ;Fire up a new process fork test ax, ax ;Parent's return? je StartBG2 jmp BackGround1 ;Go do backgroun stuff. ; Start the second background process: StartBG2: lesi BkgndPCB3 ;Fire up a new process fork test ax, ax ;Parent's return? je ParentPrcs jmp BackGround2 ;Go do backgroun stuff. ; The parent process will print a bunch of strings at the same time ; the background process is doing this. We'll use the DOS semaphore ; to protect the call to DOS that PUTS makes. ParentPrcs: lesi BarrierSemaph barrier 3 lea bx, StrPtrs1 ;Array of str ptrs. PrintLoop: cmp word ptr [bx+2], 0 ;At end of pointers? je ForeGndDone les di, [bx] ;Get string to print. DOSWait puts ;Calls DOS to print string. DOSRls add bx, 4 ;Point at next str ptr. jmp PrintLoop ForeGndDone: prcsquit Quit: ExitPgm ;DOS macro to quit program. Main endp cseg ends sseg segment para stack 'stack' ; Here are the stacks for the background processes we start stk2 byte 1024 dup (?) EndStk2 word ? stk3 byte 1024 dup (?) EndStk3 word ? ;Here's the stack for the main program/foreground process. stk byte 1024 dup (?) sseg ends zzzzzzseg segment para public 'zzzzzz' LastBytes db 16 dup (?) zzzzzzseg ends end Main
Sample Output:
Background 1: string 'a' Background 1: string 'b' Background 1: string 'c' Background 1: string 'd' Background 1: string 'e' Background 1: string 'f' Foreground: string 'a' Background 1: string 'g' Background 2: string 'j' Foreground: string 'b' Background 1: string 'h' Background 2: string 'k' Foreground: string 'c' Background 1: string 'i' Background 2: string 'l' Foreground: string 'd' Background 2: string 'm' Foreground: string 'e' Background 2: string 'n' Foreground: string 'f' Background 2: string 'o' Foreground: string 'g' Background 2: string 'p' Foreground: string 'h' Background 2: string 'q' Foreground: string 'i' Background 2: string 'r' Foreground: string 'j' Foreground: string 'k' Foreground: string 'l'
Note how background process number one ran for one clock period before the other processes waited on the DOS semaphore. After this initial burst, the processes all took turns calling DOS.