Stdlib Process Management


UCR StdLib Process Management
12.1 - Interface
12.2 - Process Package Overview
12.3 - PrcsInit, PrcsQuit
12.3.1 - Calling Conventions and Assertions
12.3.2 - Syntax & Examples
12.4 - Fork
12.4.1 - Calling Conventions and Assertions
12.4.2 - Syntax & Examples
12.5 - Die
12.6 - Alternate Syntax
12.6.1 - Calling Conventions and Assertions
12.6.2 - Syntax & Examples
12.7 - Kill
12.7.1 - Calling Conventions and Assertions
12.7.2 - Syntax & Examples
12.8 - Sleep
12.8.1 - Calling Conventions and Assertions
12.8.2 - Syntax & Examples
12.9 - YieldCPU
12.9.1 - Calling Conventions and Assertions
12.9.2 - Syntax & Examples
12.10 - WaitSemaPh, WaitSemaphStk, WaitSemaPhL
12.10.1 - Calling Conventions and Assertions
12.10.2 - Syntax & Examples
12.10.3 - Alternate Syntax
12.11 - RlsSemaPh, RlsSemaPhStk, RlsSemaPhL
12.11.1 - Calling Conventions and Assertions
12.11.2 - Syntax & Examples
12.11.3 - Alternate Syntax
12.12 - BIOS, DOS, the Standard Library, and Reentrancy


UCR StdLib Process Management

The Process package includes several routines to support multi-tasking, threads, synchronization, and coroutines under MS-DOS.


12.1 Interface

To access the routines in the declarations package, your assembly language module must include the file "process.a" during assembly. You can accomplish this with either of the following include statements in your assembly code:




	include	process.a
or
	include	ucrlib.a

The 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.


12.2 Process Package Overview

Perhaps you've read somewhere that true multitasking is not possible under DOS. Well, the process management package in the stdlib demonstrates that true multitasking under DOS is certainly possible, although somewhat involved.

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
ENDVAR
You 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             ends

Although 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

12.3 PrcsInit, PrcsQuit

The PrcsInit and PrcsQuit routines initialize and shut down the preemptive multi-tasking facilities in the standard library. You must call PrcsInit in order to activate the multitasking system. This function, that doesn't require any parameters, patches into the PC's real time clock. This clock generates an interrupt every 55ms. The process package uses this interrupt to switch tasks. The PrcsInit function also sets up some internal data structures for the processes package including the default PCB for the main program.

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.


12.3.1 Calling Conventions and Assertions


12.3.2 Syntax & Examples

PrcsInit and PrcsQuit do not require any parameters. The following sample main program demonstrates a typical invocation:




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

12.4 Fork

Fork provides the mechanism for starting a new thread of execution in a process. Fork expects the address of a semi-initialized PCB in the ES:DI register pair. For each call to Fork there are two returns: the parent process immediately returns from the fork call. It also spawns a child process that returns later when it gets its own timeslice.

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     ends

To 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
	fork

On 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.


12.4.1 Calling Conventions and Assertions


12.4.2 Syntax & Examples

The Fork invocation requires no operands. You do pass it the address of a PCB in the ES:DI register pair. The regsp and regss fields of this PCB must be initialized with the address of the last word of a stack for the child process. The following code demonstrates a typical call to Fork:




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

12.5 Die

Die terminates the current process (the one executing the Die call). The remainder of the time slice is given to the next ready-to run process in the system's ready queue.

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).


12.6 Alternate Syntax

The Fork call allows an alternate syntax where you specify a single parameter in the operand field of the Fork invocation. If the operand is a word object, Fork assumes that it is a near pointer (relative to DS) to a PCB. In this case, Fork pushes DS, followed by the value at the address specified by the word (in the data segment), and then it calls the ForkStk routine. If the operand is a dword object, Fork assumes this is a far pointer, pushes its value onto the stack, and calls ForkStk. If the operand field contains the name of a PCB, Fork calls the ForkL routine and follows the call with the address of the PCB in the code stream.




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

12.6.1 Calling Conventions and Assertions


12.6.2 Syntax & Examples




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          ends

The 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


12.7 Kill

Kill terminates the process whose process ID is passed in the AX register. This allows a parent process to prematurely terminate a child process (i.e., terminate it before it chooses to die on its own). Kill will search the ready queue for the process with the given ID and remove it from the ready queue (effectively terminating it).

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.


12.7.1 Calling Conventions and Assertions


12.7.2 Syntax & Examples

In the following example, the child process continues doing some (busy) work after it signals the parent process that it has completed its main task. The parent process, upon seeing the signal, does some work of its own and then terminates the child process via the Kill command at some later point in the program.




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


12.8 Sleep

The Sleep function puts a process to sleep for the number of clock ticks specified in the AX register. Each clock tick is 1/18.2 seconds (approximately 55 msec). The process will continue execution at some point after the specified time period elapses. Note that this may not be after exactly the number of clock ticks you specify since the process is sitting in the ready queue and must wait its turn like any other process. If the timer expires and there are still two processes ahead of the newly awoken process, there will be an additional delay of two clock ticks before the process actually starts execution.

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
	Sleep

To 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
	sleep

One 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.


12.8.1 Calling Conventions and Assertions


12.8.2 Syntax & Examples




                .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


12.9 YieldCPU

The YieldCPU call cause the current process to give up the remainder of its time slice. The next available process in the ready queue gets the remainder of the current process' time slice. This call is especially useful when one process needs to wait for results from another process.

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.


12.9.1 Calling Conventions and Assertions


12.9.2 Syntax & Examples

The following example demonstrates how to use YieldCPU to prevent the main program from wasting CPU cycles (while waiting for the child process to catch up) that could be put to better use by the child process.




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

12.10 WaitSemaPh, WaitSemaphStk, WaitSemaPhL

WaitSemaPh and RlsSemaPh protect critical regions in a multitasking environment. WaitSemaPh expects you to pass the address of a semaphore variable in ES:DI. If that particular semaphore is not currently in use, WaitSemaPh marks the semaphore "in use" and immediately returns. If the semaphore is already in use, the WaitSemaPh queues up the current process on a waiting queue and lets some other process start running. Once a process is done with the resource protected by a semaphore, it must call RlsSemaPh to release the semaphore back to the system. If any processes are waiting on that semaphore, the call to RlsSemaPh will activate the first such process. Note that a process must not make two successive calls to WaitSemaPh on a particular semaphore variable without calling RlsSemaPh between the calls. Doing so will cause a deadlock.

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.


12.10.1 Calling Conventions and Assertions


12.10.2 Syntax & Examples

The following short code segments demonstrate each of the WaitSemaPhx calls:




Sema4           semaphore       {}
                 .
                 .
                 .
                lesi    Sema4
                WaitSemaPh
                 .
                 .
                 .
                push    seg Sema4
                push    offset Sema4
                WaitSemaPhStk
                 .
                 .
                 .
                WaitSemaPhL
                dword   Sema4
                 .
                 .
                 .

12.10.3 Alternate Syntax

The WaitSemaPh macro provides an alternate syntax that allows parameters in the operand field of the WaitSemaPh invocation. If the operand is a word object, WaitSemaPh assumes that it is a near pointer (relative to DS) to a semaphore. In this case, WaitSemaPh pushes DS, followed by the value at the address specified by the word (in the data segment), and then it calls the WaitSemaPhStk routine. If the operand is a dword object, WaitSemaPh assumes this is a far pointer, pushes its value onto the stack, and calls WaitSemaPhStk. If the operand field contains the name of a semaphore, WaitSemaPh calls the WaitSemaPhL routine and follows the call with the address of the semaphore in the code stream.




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.

12.11 RlsSemaPh, RlsSemaPhStk, RlsSemaPhL

The RlsSemaPh calls complement the WaitSemaPh functions. They inform the stdlib semaphore package that a particular process is done using a resource protected by the semaphore. If there was another process waiting on that semaphore, the RlsSemaPh function removes that process from the queue (or the first such process) and places it on the ready queue. Note that this does not give the CPU to the process removed from the queue - RlsSemaPh returns directly to the calling process (assuming its time quantum doesn't expire during the return operation). At some later time the process RlsSemaPh removes from the queue will get its next time slice.


12.11.1 Calling Conventions and Assertions


12.11.2 Syntax & Examples

The following short code segments demonstrate each of the WaitSemaPhx calls:




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
                 .
                 .
                 .

12.11.3 Alternate Syntax

The RlsSemaPh macro provides an alternate syntax that allows parameters in the operand field of the RlsSemaPh invocation. If the operand is a word object, RlsSemaPh assumes that it is a near pointer (relative to DS) to a semaphore. In this case, RlsSemaPh pushes DS, followed by the value at the address specified by the word (in the data segment), and then it calls the RlsSemaPhStk routine. If the operand is a dword object, RlsSemaPh assumes this is a far pointer, pushes its value onto the stack, and calls RlsSemaPhStk. If the operand field contains the name of a semaphore, RlsSemaPh calls the RlsSemaPhL routine and follows the call with the address of the semaphore in the code stream.




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.

12.12 BIOS, DOS, the Standard Library, and Reentrancy

As you've undoubted read at one point or another, DOS is not reentrant (this is why multitasking is so difficult in DOS). Most BIOS routines are not reentrant either. Finally, many of the Standard Library routines are not reentrant as well. This means that two threads of execution cannot both be calling DOS, BIOS, or a Standard Library routine concurrently. Doing so may produce unpredictable results, it may even crash the system.

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.
         .
         .
         .