Process Manager Routines ------------------------ The UCR Standard Library Process package provides a simple preemptive multitasking package for 8086 assembly language programmers. It also provides a coroutine package and support for semaphores. First, *AND THIS IS VERY IMPORTANT*, this package only supports the 8086, 8088, 80188, 80186, and 80286 processors operating in real mode. You will need to make some minor modifications to the process package if you wish to support the 32-bit x86 processors. The current process manager only saves 16-bit registers, not the 32-bit registers of the 80386 and later. You will, however, find it a relatively minor task to go in and modify this code to support the 386 and later processors. We will probably add support for these processors at a later date when time allows. Second, *THIS IS ALSO 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 YIELD. 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. There- fore, 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 YIELD routine gives up the current process' time slice and lets some other process take over. The semaphore routines, WaitSemaph and RlsSemaph, let you wait on a semaphore or signal that semaphore, respectively. The PROCESS.A and PROCESS.A6 include files contain the definition of a semaphore type, it is semaphore struc SemaCnt dw 1 smaphrLst dd ? endsmaphrlst dd ? semaphore 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 semaphore <2> 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 semaphore {} 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 NextProc dd ? regsp dw ? regss dw ? regip dw ? regcs dw ? regax dw ? regbx dw ? regcx dw ? regdx dw ? regsi dw ? regdi dw ? regbp dw ? regds dw ? reges dw ? regflags dw ? PrcsID dw ? StartingTime dd ? StartingDate dd ? CPUTime dd ? pcb ends As mentioned earlier, this code does not maintain the full state of the 80386 and later processors since it only saves the 16-bit register set. If you like, you can easily change the definition of the PCB, and all the code in the PROCESS.ASM file which refers to the PCB, and support full 32-bit operation. As usual, you should rarely, if ever, play around with the internal fields of a PCB. That is for the process manager to do and you could mess things up pretty back if you're not careful. Routine: prcsinit ----------------- Category: Processes Registers on entry: None Registers on return: None Flags affected: None Example of Usage: prcsinit Description: Prcsinit initializes the process manager. Note that if you make a call to the process manager, you *MUST* make a call to prcsquit before your program quits. Failure to do so will crash the system in short order. Note that you must even handle the case where the user types control-C or encounters a critical error and aborts back to DOS. This routine patches into the timer interrupt vector (INT 08) and may not work properly with code in the system which is also messing around with the timer interrupt. Include: stdlib.a or process.a Routine: prcsquit ----------------- Category: Processes Registers on entry: None Registers on return: None Flags affected: None Example of Usage: prcsquit Description: Prcsquit deinitializes the process manager. It detaches the timer interrupt from the internal routine and performs various other clean up chores. You must call this routine before your program terminates if you've called prcsinit somewhere in your program. Note that you cannot call prcsquit twice in a row, although you can call the prcsinit/prcsquit combinations as many times as you like in your code. Include: stdlib.a or process.a Routine: fork ------------- Category: Processes Registers on entry: ES:DI- Points at a PCB to hold the process info for the new process. The regss and regsp fields of this PCB must be initialized to the top of a new stack for the new process. Registers on return: AX- Returned containing zero. Child process ID. BX- Child's process ID. Returned containing zero. Flags affected: None Example of Usage: ChildPCB pcb {0, offset endstk, seg endstk} . . . lesi ChildPCB fork cmp ax, 0 jne DoChildProcess . . . ChildsStack db 1024 dup (?) EndStk dw 0 Description: Fork spawns a new process. You make a single call to fork but it returns twice- once for the parent process (the original caller to fork) and once for the child process (the new process created by fork). On entry to fork ES:DI must point at a PCB for the new process which has the REGSP and REGSS fields initialized to point at the last word in a stack set aside for the child process. *THIS IS VERY IMPORTANT!* On return, the code following the call to FORK can test to see whether the parent is returning or the child is returning by looking at the value in the AX register. AX will contain zero upon return to the parent process. It will contain a non-zero value, which is the process ID, when returning to the child process. When the parent process returns, FORK returns the process ID of the child process in the BX register. The parent process can use this value to kill the child process, should that become necessary later on. If the child needs access to the parent's process ID, the parent process should store away its process ID in a variable before calling the FORK routine. Note that with the exception of the AX, BX, SP, and SS registers, FORK preserves all register values and returns the same set of values to both the parent and child processes. In particular, it preserves the value of the DS register so the child will have access to any global variables in use by the parent process. FORK does *not* copy the parent's stack data to the child's stack. Upon return from FORK, the child's stack is empty. If you call FORK after pushing something on the stack (e.g., a return address because you've called FORK inside some other procedure), that information will not be placed on the stack of the child process. If you such information pushed on the child's stack, you will need to save SS:SP prior to calling FORK (in a global variable) and then push the data pointed at by this saved value onto the child's stack upon return. Of course, it's a whole lot easier if you simply don't count on anything being on the child's stack when you get back from FORK. In particular, don't call FORK from inside some nested routine and expect the child process to return to the caller of the routine containing FORK. Include: stdlib.a or process.a Routine: die ------------ Category: Processes Registers on entry: None Registers on return: None Flags affected: None Example of Usage: die Description: Die kills the current process. Control is transferred to some other process in the process manager's ready-to-run queue. Control never returns back to the current process. Note: if the current process is the only process running, calling DIE may crash the system. Include: stdlib.a or process.a Routine: kill ------------- Category: Processes Registers on entry: AX- Process ID of process to terminate. Registers on return: None Flags affected: None Example of Usage: mov ax, ProcessID kill Description: KILL lets one process terminate another. Typically, a parent might kill a child process, although any process which knows the process ID of some other process can kill that other process. If a process passes its own ID to KILL, the system behaves exactly as though the process called the DIE routine. If a process passes its own ID to KILL and it is the only process in the system, the system may crash. Include: stdlib.a or process.a Routine: yield -------------- Category: Processes Registers on entry: None Registers on return: None Flags affected: None Example of Usage: yield Description: YIELD voluntarily gives up the CPU. The remainder of the current time slice is given to some other process and the current process goes to the end of the process manager's ready to run queue. This call is particularly useful for passing control between two cooperating process where one process needs to wait for some action to complete and you don't feel like using semaphores to synchronize the two activies. This call is roughly equivalent to "COCALL ". Include: stdlib.a or process.a Routine: coinit --------------- Category: Processes Registers on entry: ES:DI- Points at an empty PCB for the current process. Registers on return: None Flags affected: None Example of Usage: MainProcess pcb {} . . . lesi MainProcess coinit Description: COINIT initializes the coroutine package and sets up an internal pointer to the PCB specified by ES:DI (the "current coroutine" pcb). On the next COCALL, the process state will be saved in the pcb you've specified on the COINIT call. Note that you do not have to initialize this pcb in any way, that will all be taken care of by COINIT and the COCALL. Include: stdlib.a or process.a Routine: cocall/cocalll ----------------------- Category: Processes Registers on entry: ES:DI- Points at a PCB for the new coroutine (COCALL only). CS:IP- Points at a pointer to a PCB for the new coroutine (COCALLL only). Registers on return: None Flags affected: None Example of Usage: OtherProcess pcb {----} YetAnotherProcess pcb {----} . . . lesi OtherProcess cocall . . . cocall YetAnotherProcess Description: COCALL switches context between two coroutines. There are two versions of this call, although you use COCALL to invoke both of them: COCALL and COCALLL. For COCALL, you must pass the address of a PCB in ES:DI. When calling COCALL in this fashion, the operand field of the COCALL instruction must be blank (see the example above). COCALLL expects the address of the pcb to follow in the code stream. The COCALL macro looks for an operand and, if one is present, it automatically creates the appropriate call to COCALLL and inserts the address of the PCB in the code stream (again, see the example above). Before you start a coroutine for the first time by calling COCALL, you must properly initialize the pcb for that coroutine. You must provide initial values for the regsp, regss, regip, and regcs fields of the pcb (fields two through five in the pcb structure). Regsp and regss must point at the last word of an appropriately sized stack for that coroutine; regip and regcs must point at the initial entry point for the coroutine. For example, if you want to switch between the current process and a coroutine named "CORTN", you could use the following code: MainCoRtn pcb {} CoRtnPCB pcb {0,offset CoRtnStk, seg CoRtnStk, offset CoRtn, seg CoRtn} . . . lesi MainCoRtn coinit . . . cocall CoRtnPCB . . . CoRtn proc . . . cocall MainCoRtn . . . CoRtn endp . . . db 1024 dup (?) CoRtnStk dw 0 Include: stdlib.a or process.a Routine: waitsemaph ------------------- Category: Processes Registers on entry: ES:DI- Points at a semaphore variable Registers on return: None Flags affected: None Example of Usage: DOSsemaph semaphore {} . . . lesi DOSsemaph WaitSemaPh . . . lesi DOSsemaph RlsSemaPh Description: 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. Include: stdlib.a or process.a Routine: rlssemaph ------------------ Category: Processes Registers on entry: ES:DI- Points at a semaphore variable Registers on return: None Flags affected: None Example of Usage: See WaitSemaPh Description: RlsSemaPh releases a semaphore (also known as "signalling") that the current process has aquired via a call to WaitSemaPh. Please see the WaitSemaPh explaination for more details. You should not call RlsSemaPh without first calling WaitSemaPh. Doing so may cause some inconsistencies in the system. Include: stdlib.a or process.a