The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Nineteen (Part 9)

Table of Content

Chapter Nineteen (Part 11)

CHAPTER NINETEEN:
PROCESSES, COROUTINES AND CONCURRENCY (Part 10)
19.4.2 - The UCR Standard Library Processes Package
19.4.3 - Problems with Multitasking

19.4.2 The UCR Standard Library Processes Package

The UCR Standard Library provides six routines to let you manage threads. These routines include prcsinit, prcsquit, fork, die, kill, and yield. These functions let you initialize and shut down the threads system, start new processes, terminate processes, and voluntarily pass the CPU off to another process.

The prcsinit and prcsquit functions let you initialize and shutdown the system. The prcsinit call prepares the threads package. You must call this routine before executing any of the other five process routines. The prcsquit function shuts down the threads system in preparation for program termination. Prcsinit patches into the timer interrupt (interrupt 8). Prcsquit restores the interrupt 8 vector. It is very important that you call prcsquit before your program returns to DOS. Failure to do so will leave the int 8 vector pointing off into memory which may cause the system to crash when DOS loads the next program. Your program must patch the break and critical error exception vectors to ensure that you call prcsquit in the event of abnormal program termination. Failure to do so may crash the system if the user terminates the program with ctrl-break or an abort on an I/O error. Prcsinit and prcsquit do not require any parameters, nor do they return any values.

The fork call spawns a new process. On entry, es:di must point at a pcb for the new process. The regss and regsp fields of the pcb must contain the address of the top of the stack area for this new process. The fork call fills in the other fields of the pcb (including cs:ip).

For each call you make to fork, the fork routine returns twice, once for each thread of execution. The parent process typically returns first, but this is not certain; the child process is usually the second return from the fork call. To differentiate the two calls, fork returns two process identifiers (PIDs) in the ax and bx registers. For the parent process, fork returns with ax containing zero and bx containing the PID of the child process. For the child process, fork returns with ax containing the child's PID and bx containing zero. Note that both threads return and continuing executing the same code after the call to fork. If you want the child and parent processes to take separate paths, you would execute code like the following:

                lesi    NewPCB          ;Assume regss/regsp are initialized.
                fork
                test    ax, ax          ;Parent PID is zero at this point.
                je      ParentProcess   ;Go elsewhere if parent process.

; Child process continues execution here

The parent process should save the child's PID. You can use the PID to terminate a process at some later time.

It is important to repeat that you must initialize the regss and regsp fields in the pcb before calling fork. You must allocate storage for a stack (dynamically or statically) and point ss:sp at the last word of this stack area. Once you call fork, the process package uses whatever value that happens to be in the regss and regsp fields. If you have not initialized these values, they will probably contain zero and when the process starts it will wipe out the data at address 0:FFFE. This may crash the system at one point or another.

The die call kills the current process. If there are multiple processes running, this call transfers control to some other processes waiting to run. If the current process is the only process on the system's run queue, then this call will crash the system.

The kill call lets one process terminate another. Typically, a parent process will use this call to terminate a child process. To kill a process, simply load the ax register with the PID of the process you want to terminate and then call kill. If a process supplies its own PID to the kill function, the process terminates itself (that is, this is equivalent to a die call). If there is only one process in the run queue and that process kills itself, the system will crash.

The last multitasking management routine in the process package is the yield call. Yield voluntarily gives up the CPU. This is a direct call to the dispatcher, that will switch to another task in the run queue. Control returns after the yield call when the next time slice is given to this process. If the current process is the only one in the queue, yield immediately returns. You would normally use the yield call to free up the CPU between long I/O operations (like waiting for a keypress). This would allow other tasks to get maximum use of the CPU while your process is just spinning in a loop waiting for some I/O operation to complete.

The Standard Library multitasking routines only work with the 16 bit register set of the 80x86 family. Like the coroutine package, you will need to modify the pcb and the dispatcher code if you want to support the 32 bit register set of the 80386 and later processors. This task is relatively simple and the code is quite similar to that appearing in the section on coroutines; so there is no need to present the solution here.

19.4.3 Problems with Multitasking

When threads share code and data certain problems can develop. First of all, reentrancy becomes a problem. You cannot call a non-reentrant routine (like DOS) from two separate threads if there is ever the possibility that the non-reentrant code could be interrupted and control transferred to a second thread that reenters the same routine. Reentrancy is not the only problem, however. It is quite possible to design two routines that access shared variables and those routines misbehave depending on where the interrupts occur in the code sequence. We will explore these problems in the section on synchronization (see "Synchronization"), just be aware, for now, that these problems exist.

Note that simply turning off the interrupts (with cli) may not solve the reentrancy problem. Consider the following code:

                cli                     ;Prevent reentrancy.
                mov     ah, 3Eh         ;DOS close call.
                mov     bx, Handle
                int     21h
                sti                     ;Turn interrupts back on.

This code will not prevent DOS from being reentered because DOS (and BIOS) turn the interrupts back on! There is a solution to this problem, but it's not by using cli and sti.

Chapter Nineteen (Part 9)

Table of Content

Chapter Nineteen (Part 11)

Chapter Nineteen: Processes, Coroutines and Concurrency (Part 10)
29 SEP 1996