include process.a or include ucrlib.aThe process.a include file exports several symbols. The UCR Standard Library prefaces all "private" names with a dollar sign ("$"). You should not call any routine in this package that begins with this symbol unless otherwise advised. To avoid name conflicts, you should not define any symbols in your programs that begin with a dollar sign ("$"). Note that future versions of the stdlib (that remain compatible with this release) may change "private" names. To remain compatible with future releases, you must not refer to these "private" names within your programs.
Source code appearing in this chapter is current as of Version Two, Release 40. There may be minor changes between this source code and the current release.
The UCR Standard Library Process package provides a simple preemptive multitasking package for 80x86 assembly language programmers. It also provides a coroutine package and support for semaphores.
THIS IS VERY IMPORTANT, keep in mind that DOS, BIOS, and many of the routines in the standard library ARE NOT REENTRANT. Two processes executing at the (apparent) same time cannot both be executing DOS, BIOS, or the same standard library routines. It is unlikely that DOS or BIOS will ever be made reentrant, and you shouldn't ever expect the standard library to be made reentrant (far too much work). The standard library provides semaphore support through which you can control access to critical resources including DOS, BIOS, and the UCR Standard Library. If you are unfamiliar with terms like reentrancy, semaphores, synchronization, and deadlock, you should probably pick up a good text on operating systems and familiarize yourself with these terms before attempting to use this package.
This process package provides three facilities to your assembly language programs: A preemptive multitasking process manager, coroutine support, and semaphore support. There are six routines associated with the preemptive multitasking system: PRCSINIT, PRCSQUIT, FORK, KILL, DIE, and YIELDCPU. There are three routines associated with the coroutines package: COINIT, COCALL, and COCALLL (though you'll rarely refer to COCALLL directly). Finally, there are two routines to support semaphores: WAITSEMAPH and RLSSEMAPH.
The PRCSINIT and PRCSQUIT routines initialize and deinitialize the interrupt system for the multitasking system. You must call PRCSINIT prior to executing any of the preemptive multitasking routines or any of the semaphore routines (the semaphore routines make sense only in the context of preemptive multitasking). This initializes various internal variables and patches the INT 8 interrupt vector (timer interrupt) to point at an internal routine in the process manager. You must call PRCSQUIT when you are done with the preemptive multitasking system; certainly you must call it before your program terminates *FOR ANY REASON*. If you do not call PRCSQUIT, the system will probably crash shortly after you try anything else after returning to DOS since the timer interrupt will still be calling the routine left in memory when your program terminates.
The process manager patches into the 1/18th second clock on the PC. Therefore, the system will automatically perform an context switch every 55ms or so. If your application reprograms the timer chip, this may produce unexpected results. This may be particularly bothersome if you are running a TSR which plays with the timer chip. Absolutely no attempt was made to make this code robust enough to work in all cases with other code which ties into the timer interrupt. Most well-written code will work fine, but there are not guarantees.
The FORK routine lets you spawn a new process. For each call to FORK your code makes, the FORK routine returns twice- once as the parent process and once as the child process. FORK returns process ID information in the AX and BX registers so that the code immediately following the FORK can figure out if it's the parent or child process. FORK provides the basic (and only!) mechanism for starting a second process.
The KILL and DIE routines let you terminate a process. KILL lets one process terminate some other process (generally a child process). DIE lets a process terminate itself.
The YIELDCPU routine gives up the current process' time slice and lets some other process take over. Note that the system uses preemptive multitasking. You are not required to use YIELDCPU to give up your timeslice. YIELDCPU exists for those rare instances when two processes are communicating with one another and one process needs to wait until the other gets the CPU and can complete some computation. By yielding the CPU, the first process gives control to the second so it can get right to work on the computation rather than waiting for the next timer interrupt before it can begin.
The semaphore routines, WaitSemaph and RlsSemaph, let you wait on a semaphore or signal that semaphore, respectively. The PROCESS.A include file contains the definition of a semaphore type, it is
sema4 struc SemaCnt word 1 smaphrLst dword ? endsmaphrlst dword ? sema4 ends
The only field you should ever play around with is the SemaCnt field. This value is the number of processes which are allowed to be in the critical region controlled by the semaphore at one time. For most mutual exclusion problems, this value should always be one. Do not modify this value once the program starts running. The process package increments and decrements this number to keep track of the number of processes waiting to use a resource. If you want to allow two processes to share a resource at the same time, you should declare your semaphore variable as follows:
MySemaPh sema4 <2>
The process.a header file also includes a "Semaphore" type definition you can use in the VAR..ENDVAR section of your program. You could declare a semaphore variable in your VAR..ENDVAR section using code like the following:
VAR semaphore S1 semaphore S2=2 ENDVARYou execute the WaitSemaPh routine to see if a semaphore is currently busy. When you get back from the WaitSemaPh call, the resource protected by the semaphore is exclusively yours until you execute the RlsSemaPh routine. Note that when you call the WaitSemaPh routine, the specified resource may already be in use, in which case your process will be suspended until the resource is freed (and anyone waiting in line ahead of you has had their shot at the resource). If you do not call the RlsSemaPh routine to free the semaphore, any other process waiting on that resource will wait indefinitely. Also note that if you call WaitSemaPh twice on a semaphore without releasing it inbetween, your process and any other process which waits on that resource will deadlock.
While semaphores solve a large number of synchronization and mutual exclusion problems, their primary use in the UCR Standard Library is to prevent re entrancy problems in DOS, BIOS, and the Standard Library itself. For example, if you have two processes which print values to the display, attempting to run both processes concurrently will crash the system if they both attempt to print at the same time (since this will cause DOS to be reentered). A simple solution is to use a DOS semaphore as follows:
In the data segment:
DOS sema4 {}
In Process 1:
lesi DOS WaitSemaPh print db "Printed from process #1.",cr,lf,0 lesi DOS RlsSemaPh
In Process 2:
lesi DOS WaitSemaPh print db "Printed from process #2.",cr,lf,0 lesi DOS RlsSemaPh
Semaphore guarantee mutual exclusion between the WaitSemaPh and RlsSemaPh calls (for a particular semaphore variable, DOS in this case). Hence, once process #1 enters its CRITICAL REGION by executing the WaitSemaPh call, any attempt by process two to enter its critical region will cause process two to suspend execution until process one executes the RlsSemaPh routine.
Coroutines provide "simulated" multitasking where the processes themselves determine when to perform a context switch. This is quite similar to the "cooperative multitasking" systems provided by Apple, Microsoft, and others ("cooperative multitasking" is a hyped up term to hide the fact that their systems provide only multiprogramming, not multitasking). There are many advantages and disadvantages to coroutines vs. multitasking. First of all, reentrancy problems do not exist in a system using coroutines. Since you control when one process switches to another, you can make sure that such context switches do not occur in critical regions. Another advantage to coroutines is that the processes themselves can determine which other process gets the next access to the CPU. Finally, when a coroutine is executing, it gets full access to the CPU to handle a time-critical operation without fear of being preempted. On the other hand, poorly designed coroutines provide a very crude approximation to multitasking and may actually hurt the overall performance of the system.
The UCR Standard Library provides three routines to support coroutines: COINIT, COCALL, and COCALLL. Generally, you'll only see COINIT and COCALL in a program, the standard library automatically generates COCALLL calls for certain types of COCALL statements. The COINIT routine initializes the coroutine package and creates a process control block (PCB) for the currently active routine. COCALL switches context to some other process. When one process COCALLs another and that second process COCALLs the first, the first process continues execution immediately after the first COCALL instruction (so it behaves more like a return than a call). In general, you should not think of COCALL as a "call" but rather as a "switch to some other process."
You may have coroutines and multitasking active at the same time, but you should not make a COCALL to a process which is being time-sliced by the multitasking system. I won't guarantee that this *won't* work, but it seems sufficiently weird that something is bound to go wrong.
For those who are interested, the coroutine and multitasking packages maintain the state of a process in a process control block (PCB) which is the following structure:
pcb struc regsssp dword ? regip word ? regcs word ? NextProc dword ? regds word ? reges word ? regfs word ? reggs word ? regeax dword ? regebx dword ? regecx dword ? regedx dword ? regesi dword ? regedi dword ? regebp dword ? regflags word ? PrcsID word ? StartingTime dword ? StartingDate dword ? CPUTime dword ? pcb endsAlthough the process manager mostly supports the 80386 register set, there are a few details of which you should be aware. First, as the structure above shows, the processes package only maintains the L.O. 16 bits of the stack pointer. Since the H.O. bits of SP must always be zero when running in real mode (e.g., under DOS or a DOS window), this is hardly a problem. The other thing to note is that the process package only maintains the L.O. word of the flags register from one process to the next. Since the H.O. bits are generally not available for user program modification, this is also of little concern. Finally, the package does not preserve the state of the FPU. If you are using the floating point coprocessor in your applications, you will want to modify the PCB so you can save the state (note: MMX extensions also use the FPU!).
Note that the process.a package also defines a type, PCBlock, you can use in a VAR..ENDVAR section of your program. Typical uses of this declaration look like the following:
var PCBlock pcb1 ;User must initialize SSSP field. PCBlock pcb2=endstk2 ;Initializes stack field to EndStk2. . . . endvar
The PrcsQuit function restores the system timer interrupt vector. It is extremely important that you call PrcsQuit before your program terminates if you've called PrcsInit. PrcsInit points the system's timer interrupt vector at code within the process package. If you do not call PrcsQuit to restore the system interrupt vector table, the system will continue to call the process manager's timer interrupt service routine even after your program terminates. This usually crashes the system in short order. To prevent a "back door escape" you should use the try..endtry block to catch control-C and critical-error traps that would cause your program to exit without the opportunity to restore the interrupt vector.
main proc far mov ax, dseg mov es, ax mov ds, ax MemInit InitExcept InitEx24 PrcsInit try . . . Except $Break print "You hit control-C",nl jmp QuitPgm ; Perhaps a better critical error handler than this one is ; warranted, but you get the idea. Except $CritErr print "Critical error occured",nl jmp QuitPgm endtry . . . QuitPgm: PrcsQuit CleanUpEx ExitPgm main endp
The PCB that ES:DI points at when you call Fork must have its SSSP field initialized with the address of the last word of a stack for the child process. Every process in the system must have its own stack (indeed, to some people, having a stack is the definition of a process). You cannot reuse the parent's stack, you must create a block of memory specifically for the child process. The easiest way to do this is to create a new segment in your program as follows:
ChildStkSeg segment para public 'stk2' word 255 dup (?) EndChildStk word ? ChildStkSeg endsTo initialize a PCB so that the SSSP field points at the end of the Child's stack segment, you would use a declaration like the following:
ChildPCB pcb {EndChildStk}
To start a new child process, you simply load the address of the PCB for the new process into the ES:DI register pair and call the Fork procedure. The following code demonstrates how to do this:
lesi ChildPCB forkOn each call to fork there are two returns: one by the parent process (this is pically the first return, although that is not guaranteed) and the child process. Most programs that use processes will want to split the execution path immediately upon return. That is, upon returning from fork the program will generally send the child process off to some other procedure rather than continuing execution with the same instructions the parent process is executing. Therefore, there needs to be some mechanism you can use to differentiate a parent process from a child process on return from fork. The stdlib uses process IDs to accomplish this.
The Fork function returns two values in the AX and BX registers. When the parent process returns from Fork the AX register contains zero and the BX register contains the process ID of the child process. When the child process returns from fork the AX register contains the child process' ID and the BX register contains zero. Therefore, you can check the value of the AX (or BX) register to differentiate the parent and child processes upon return from Fork.
Note that the parent process will know the process ID of the child process it spawns. This gives the parent process the ability to kill (terminate) the child process at a later time. See the Kill function for more details. Child processes should not kill the parent process, this is why Fork does not return the parent's process ID to the child process.
Note: Unlike its UNIX namesake, the stdlib Fork function does not create a new memory space for the new thread. Such a concept would be foreign to DOS that doesn't support separate memory spaces. Threads in the stdlib processes package are always lightweight processes and share the same memory space as the parent process.
One exception to this rule concerns the stack. Processes (by definition) require their own unique stack. Although DOS doesn't support memory management or memory protection (and, therefore, the new process' stack is in the same "memory space" as the parent process), the child and parent processes do not share the same stack. In UNIX, the system makes a (virtual) copy of the parent process' stack for the child process. Therefore, if you call Fork (in UNIX) from within a procedure the child process can return from that procedure via a subroutine return instruction. This is not possible with stdlib Forks.
The stdlib Fork call does not copy data from the parent process' stack to the child process' stack. There are three reasons for not doing this: (1) It would be inefficient since few child processes need that stack information; (2) the stdlib Fork call has no way of knowing how big the parent's stack is and, in the absence of memory management hardware that supports copy on write, it would need this information to make a copy of the stack; (3) The programmer is responsible for choosing an appropriate stack size for the child process, copying the stack data would require every stack to be at least as large as the parent process' stack - this would waste memory since most processes do not require such a large stack.
The only thing that Fork pushes onto the child process' stack is the return address that returns control from the Fork function. Therefore, once Fork returns with AX=0 (i.e., to the child process) there is nothing on the stack.
Fork preserves all registers (except AX and BX) when it returns to the parent process. However, it clears all general purpose registers (except AX) when it returns to the child process. Note that Fork will preserve the values in the segment registers CS, DS, ES, FS, and GS (obviously it cannot preserve SS since SS will point at a new stack for the child process). Therefore, if you need to initialize any general purpose registers for the child process, you will need to do this immediately upon return from the Fork function (on the Child's return). Any values placed in the general purpose registers before that point are lost. Likewise, if you need the stack set up in some particular fashion (e.g., return addresses, local variables, parameters, etc.) you will need to do this upon returning from Fork.
As a general rule, most programs that use threads (processes) fork off any necessary threads in the main program when the stack is fairly clean to begin with. If you want to be able to control the execution of a threads (that is, disable a child process from executing until you want it to run), you should consider using semaphores. The section on semaphores (later in this chapter) explains how to do this.
ChildPCB pcb {EndStk2}
.
.
.
lesi ChildPCB
fork
cmp ax, 0 ;Child process?
je ParentProcess
mov ChildPrcsID, AX
jmp ChildProcess
; Parent process continues execution here.
ParentProcess:
.
.
.
ChildProcess proc
.
.
.
die ;Terminate child process.
ChildProcess endp
.
.
.
P2Sseg segment para public 'stk2'
word 256 dup (?)
EndStk2 word ?
P2Sseg ends
As a general rule, a process never returns from Die. However, if the current process is the last process currently in the ready queue, Die will either raise a $LastProcess exception (if exceptions are enabled) or it will return with the carry flag set. This typically denotes a logic error in your program (since the last process should never terminate via Die).
MyPCB pcb {MySSeg}
wPCB word MyPCB
dPCB dword MyPCB
.
.
.
; The following invocation simply calls FORK:
fork
; The following macro invocation pushes DS followed by
; the value at DS:wPCB and then calls ForkStk:
fork wPCB
; The following macro invocation pushes the double word
; at address DS:dPCB and then calls ForkStk:
fork dPCB
; The following invocation emits the assembly code:
;
; Forkl
; dword PCB
fork MyPCB
var PCBlock ChildPCB=EndStk2 boolean Synchronize=false endvar . . . mov Synchronize, false ;Signal from child. lesi ChildPCB fork cmp ax, 0 ;Child process? je ParentProcess mov ChildPrcsID, AX jmp ChildProcess ; Parent process continues execution here. ParentProcess: ; The parent process does some calculation in parallel ; with the child process . . . ; The Parent process is now waiting for the child process ; to finish up before it proceeds (this is a synchonization ; step here). Wait4Child: cmp Synchronize, false jne Synched YieldCPU ;Give up time slice jmp Wait4Child ;Wait for the child synched: ; process to quit. . . . ; The child process computes its result, signals the main ; process that it is done (via shared memory), and then ; terminates via a call to Die. ChildProcess proc . . . ; Do whatever it is the child is supposed to be doing. . . . ; Signal to the parent process that we are done. mov Synchronize, true ; Terminate the child process. die ChildProcess endp . . . P2Sseg segment para public 'stk2' word 256 dup (?) EndStk2 word ? P2Sseg endsThe example above does not check for a possible error return from Die (assuming the parent process never attempts to call Die, it is not possible for an error to occur in this program). Were it possible for the current process to attempt to terminate itself with a call to Die and this particular process is the last process in the system, you'd probably want to use code like the following:
Die ExitPgm
Kill will raise the $NoSuchProcess exception if you attempt to kill a process whose ID is not found in the ready queue. If exceptions are not enabled, Kill will return with the carry flag set. Note that it is possible that a process might be waiting on a semaphore somewhere and, therefore, is not on the ready queue when you call Kill (see the sections on semaphores later in this chapter). Therefore, you should never expect that Kill will return success unless you know the process has to be waiting in the ready queue.
If you pass Kill the process ID of the currently active process, Kill behaves just like Die.
ChildPCB pcb {EndStk2}
Synchronize byte 0
ChildPrcsID word ?
.
.
.
mov Synchronize, 0 ;Signal from child.
lesi ChildPCB
fork
cmp ax, 0 ;Child process?
je ParentProcess
mov ChildPrcsID, AX
jmp ChildProcess
; Parent process continues execution here.
ParentProcess:
; The parent process does some calculation in parallel
; with the child process
.
.
.
; The Parent process is now waiting for the child process
; to finish up before it proceeds (this is a synchonization
; step here).
Wait4Child: cmp Synchronize, 0
jne Synched
YieldCPU ;Give up time slice
jmp Wait4Child ;Wait for the child
; process to finish.
Synched: .
. ;Do some more work.
.
mov ax, ChildPrcsID ;Terminate the child
Kill ; process now.
.
.
.
; The child process computes its result, signals the main
; process that it is done (via shared memory), and then
; terminates via a call to Die.
ChildProcess proc
.
.
.
; Do whatever it is the child is supposed to be doing.
.
.
.
; Signal to the parent process that we are done.
mov Synchronize, 1
; Do some other useful stuff while waiting to Die:
Lp: .
.
.
jmp Lp
ChildProcess endp
.
.
.
P2Sseg segment para public 'stk2'
word 256 dup (?)
EndStk2 word ?
P2Sseg ends
There are several constants defined in the Process.a include file that you may find useful. These constants are
Passing the TicksPerxxx constants in the EAX register to the Sleep procedure delays for approximately the time period specified (the nearest appropriate integer value in timer ticks). As a general rule, you should only use these constants to delay for the time they specify. You should not, for example, attempt to delay for 100 seconds using a code sequence like the following:
mov eax, TicksPerSec*100 SleepTo help reduce the truncation error problem there is one additional text equ of interest: tick. It takes the following form:
tick textequ <1193180/65535> mov eax, 100*Tick sleepOne problem with the Tick constant is that you are realistically limited to just under 10 hours (since 10 * 60 * 60 * Tick exceeds 2**32 and MASM is limited to 32-bit arithmetic). For times greater than 10 hours, you can multiply by the TicksPerHour, TicksPerDay, and/or TicksPerWeek constants, or hand calculate the value of "seconds * 1193180/65535" yourself (if you need the most accuracy).
Note that most PC's real time clock chips are not amazingly accurate. It probably is not uncommon for a PC's clock to lose or gain several seconds each month. Therefore, attempting to fine tune really large delay constants (perhaps beyond a day, but certainly beyond a week) is foolish.
While a process is sleeping, it is still on the ready queue. This is important because one process can only kill another process if they are both on the ready queue. One theoretical drawback to leaving the process on the ready queue is that it would take up some CPU cycles on every time quantum it receives. This is not a problem with Sleep, however, because all it will do when its time slice comes around is check to see if the timer has expired and then yield the CPU to another process if this is not the case. This actually takes less system time than having a daemon managing several timers from various sleeping processes.
.xlist
include ucrlib.a
includelib ucrlib.lib
.list
sseg2 segment para public 'stack2'
stk2 word 256 dup (?)
endstk2 word ?
sseg2 ends
var
MyPCB pcb {endstk2}
Synchronize word ?
endvar
cseg segment para public 'code'
assume cs:cseg, ds:dseg
MyProc proc far
mov eax, 10*Tick ;Wait for 10 seconds
Sleep
print "I'm Done",nl
mov Synchronize, 1
Die
MyProc endp
Main proc
mov ax, dseg
mov ds, ax
mov es, ax
MemInit
PrcsInit
mov Synchronize, 0
lesi MyPCB
fork
cmp ax,0
je ParentProcess
call MyProc
nop
ParentProcess:
MPLp: cmp Synchronize, 0
je MPLp
print "So am I",nl
Done: PrcsQuit
ExitPgm ;DOS macro to quit program.
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 16384 dup (?)
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg ends
end Main
If the current process is the only process in the ready queue, the YieldCPU call immediately returns. There is no error return value if this occurs.
Warning: whatever process takes over after a YieldCPU call gets only the remainder of the current time slice. This can produce some unfair scheduling problems. For example, if there are two processes in the ready queue and one process runs until its time quantum is almost up and then executes a YieldCPU call, the new process will get a very short time slice. If the first process, when it gets the CPU back a little while later, repeats this process, the second process will get very little CPU time while the first process will hog most of the CPU cycles. The stdlib process package does not consider this. It is up to you to make sure this degenerate case does not occur.
ChildPrcsID word 0
ChildPCB pcb {EndStk2}
Synchronize byte 0
.
.
.
mov Synchronize, 0 ;Signal from child.
lesi ChildPCB
fork
cmp ax, 0 ;Child process?
je ParentProcess
mov ChildPrcsID, AX
jmp ChildProcess
; Parent process continues execution here.
ParentProcess:
; The parent process does some calculation in parallel
; with the child process
.
.
.
; The Parent process is now waiting for the child process
; to finish up before it proceeds (this is a synchonization
; step here). Note how the parent process, immediately after
; determining that the child process has not terminated, calls
; YieldCPU to give up its time slice. Since the parent process
; is simply waiting on the child, the parent immediately gives
; up its time slice so the child can resume and finish its
; task sooner.
Wait4Child: cmp Synchronize, 0
jne Synched
YieldCPU ;Give up time slice
jmp Wait4Child ;Wait for the child
; process to quit.
.
.
.
; The child process computes its result, signals the main
; process that it is done (via shared memory), and then
; terminates via a call to Die.
ChildProcess proc
.
.
.
; Do whatever it is the child is supposed to be doing.
.
.
.
; Signal to the parent process that we are done.
mov Synchronize, 1
; Terminate the child process.
die
ChildProcess endp
.
.
.
P2Sseg segment para public 'stk2'
word 256 dup (?)
EndStk2 word ?
P2Sseg ends
Note that while a process is waiting on a semaphore queue it is not on the ready queue. This may seem obvious, but there are some implications that are not so obvious. In particular, if a process is not on the ready queue, then some other process will not be able to successfully kill it using the Kill call.
By default, stdlib semaphores allow only one process access to a protected region at one time. If you need to allow some fixed number of processes in a particular region, you can initialize the SemaCnt field of the semphore data structure. For a description of how to do this, see the overview earlier in this chapter.
The WaitSemaPhStk routine expects the address of the PCB on the stack. The WaitSemaPhL routine expects to find the double word address of the PCB in the code stream immediately following the call to WaitSemaPhL.
Sema4 semaphore {}
.
.
.
lesi Sema4
WaitSemaPh
.
.
.
push seg Sema4
push offset Sema4
WaitSemaPhStk
.
.
.
WaitSemaPhL
dword Sema4
.
.
.
Sema4 semaphore {}
wSema4 word ?
dSema4 dword ?
.
.
.
WaitSemaPh ;Just calls WaitSemaph
.
.
.
WaitSemaph wSema4 ;Pushes DS, wSema4, and then
. ; calls WaitSemaPhStk
.
.
WaitSemaPh dSema4 ;Push dSema4 onto the stack
. ; and then calls WaitSemaPhStk.
.
.
WaitSemaPh Sema4 ;Calls WaitSemaPhL and follows
; the call with a
; dword Sema4
; statement.
Sema4 semaphore {}
.
.
.
lesi Sema4
WaitSemaPh
.
. ;In here, only the current process has
. ; access to the protected resource.
.
lesi Sema4
RlsSemaPh
.
.
.
lesi Seam4
WaitSema4
.
.
.
push seg Sema4 ;Relese Sema4 back to the
push offset Sema4 ; system.
RlsSemaPhStk
.
.
.
lesi Seam4
WaitSema4
.
.
.
RlsSemaPhL
dword Sema4
.
.
.
Sema4 semaphore {}
wSema4 word ?
dSema4 dword ?
.
.
.
RlsSemaPh ;Just calls RlsSemaph
.
.
.
RlsSemaph wSema4 ;Pushes DS, wSema4, and then
. ; calls RlsSemaPhStk
.
.
RlsSemaPh dSema4 ;Push dSema4 onto the stack
. ; and then calls RlsSemaPhStk.
.
.
RlsSemaPh Sema4 ;Calls RlsSemaPhL and follows
; the call with a
; dword Sema4
; statement.
Discussing reentrancy problems is well beyond the scope of this text (see any good O/S text or "The Art of Assembly Language Programming"). However, it is worth pointing out that just although DOS/BIOS/stdlib is not reentrant it is still possible for two processes to make calls to these pieces of code. The trick is to ensure that only one such call is taking place at any one given time. This is known as serializing access to a given resource (the resource being DOS, BIOS, or the stdlib).
You can use semaphores to serialize access to a given resource. If one process waits on a semaphore and is given access to the resource, a second process that waits on the same semaphore will be blocked (i.e., put to sleep) until the first process releases the semaphore. As long as all processes obey the rules and wait on a semaphore before using the associated resource, and release the semaphore when they are done with the resource, there will be no reentrancy problems. The following example demonstrates this:
var Semaphore protect endvar . . . WaitSemaPh Protect print "This is protected.",nl RlsSemaPh Protect . . . WaitSemaPh Protect mov cx, 1000 malloc RlsSemaPh Protect jc BadMalloc ;RlsSemaPh preserves C. . . .