|
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 |
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" on page 1129), 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
.
|
Table of Content | Chapter Nineteen (Part 11) |
Chapter Nineteen: Processes,
Coroutines and Concurrency (Part 10)
29 SEP 1996