Mar 94 Top 10
Volume Number: | | 10
|
Issue Number: | | 3
|
Column Tag: | | Think Top 10
|
Non-interrupt
Completion Routines
By Colen Garoutte-Carson, Symantec Technical Support, Symantec Corp.
This is a monthly column written by Symantecs Technical Support Engineers intended to provide you with information on Symantec products. Each month we cover either a specific application of tools or a Q&A list.
Non-interrupt Completion Routines
If youre using high-level Macintosh toolbox routines to manipulate files, devices, or drivers, chances are that youre program could be executing much more efficiently.
The most efficient way to do any file or driver access is asynchronously. When you write to a file with the FSWrite routine, your application, as well as any background process, is suspended while FSWrite waits for the disk to seek to the appropriate sector. Upon reaching that sector, the drive controller issues an interrupt, which prompts the CPU to start writing to the file. The time lost while waiting for the disk to seek is time that could be better spent. When you write to a file asychronously, control returns to your program immediately. The only time taken is taken after the disk interrupt has been issued, at which time the operating system does the appropriate work, and, often, waits for yet another interrupt to be issued. The process of performing an asynchronous routine is entirely transparent to your application.
All high-level file and device IO related Macintosh toolbox routines have low-level counterparts. These routines begin with the letters PB, probably because they are each passed a structure called a parameter block. Rather than passing arguments necessary to complete an operation, fields of the parameter block are assigned values, and a pointer to that parameter block is passed to the PB routine. Parameter blocks are often used both to pass information to a PB routine, and return information to the calling routine.
The second, and final argument to a PB routine is a Boolean, which is set to false for synchronous, or true for asynchronous execution. All PB routines have only these two arguments.
Different PB routines require different types, and different sizes of parameter blocks. For example, the original set of Macintosh file IO routines, designed to be used on the original flat volume architecture, require a structure called a ParamBlockRec. In C and C++, a ParamBlockRec is actually a union of a number of other parameter blocks types. In Pascal, a cased record is used. Different PB routines require variables from different union members. PBRead and PBWrite require an IOParam parameter block. PBCreate and PBDelete require a FileParam parameter block. You can either use a ParamBlockRec structure for calling all of these routines, or the exact structure a particular routine requires. You can use the same ParamBlockRec for many different consecutive PB routines because its allocated to the size of the largest member of the union. But, if you use an IOParam, you will not be able to use it with any PB routines other than those which accept IOParams. For more information on the organization of the many types of Parameter Blocks, take a look at Inside Macintosh : Files, Inside Macintosh : Devices, or the Think Reference.
All parameter blocks begin with the same basic structure, a ParamBlockHeader. The first two fields of a ParamBlockHeader, qLink and qType, are also fields of a QElem structure. QElem structures are queue element structures used internally by a driver. QElem structures and parameter blocks are interchangeable. The QElem structures that a driver is using at any given moment are probably parameter blocks, passed to it by a PB routine.
Even if you are not using asynchronous IO, calling the low level PB routines instead of their high level counterparts can save time. High level routines in turn call PB routines. Filling out the parameter blocks and calling the PB routines yourself takes more code space, but saves a little bit of CPU time. Also, sometimes there can be more flexibility built into the PB version of a routine.
There are few functional differences between using PB routines synchronously and using the high level routines, but calling PB routines asynchronously adds some new considerations to your coding. The parameter block structure you pass to a PB routines must remain valid, and not move in memory until the PB routine is completed. This means that you cannot pass a pointer to a parameter block declared locally within your function if your function may be exited before the routine is completed. This also means that you cannot reuse a parameter block until the PB routine youve used it in last is completed. Attempting to reuse a parameter block, which is currently being used by another PB routine, would modify information which may still be in use. This could cause unpredictable results.
Once you know an asynchronous routine has completed, you can use the information it returns, if any, and reuse or deallocate the parameter block. Often its desirable to chain together a series of asynchronous routines. For example, you could open a file, write to the file, then close the file, all asynchronous. The completion of each of those operations could trigger the next.
There are two ways to find out if an asynchronous routine has completed. The most often used of which is a completion routine. There is a variable within the ParamBlockHeader called ioCompletion. This variable is a ProcPtr. If this variable is non-zero, its value will be used as an address and will be jumped to automatically when the asynchronous routine is completed.
At first glance, this appears to be the perfect solution to incorporating asynchronous routines into your program. Unfortunately, the completion routine may be executed at any time, including during interrupt time, or at a point in time when your application is not the current process your computer is handling. This means that you cannot call any routines which move memory. Well over half of the Macintosh Toolbox routines move memory, including most of the fundamental ones. This also means that, at the point your completion routine is called, your routine may not have access to your applications global variables. There is a mechanism which allows you to gain access to your applications global variables, but using it means you have to take yet another matter into consideration. You could potentially be modifying a global variable which another part of your code could be in the middle of using. This could be very hazardous unless youve written your code to take sudden changes in variables into consideration.
The second way to find out if an asynchronous routine has been completed is to poll the ioResult field of its parameter block. The ioResult field, like ioCompletion, is a variable within the ParamBlockHeader structure. While the asynchronous routine is executing, this variable has a positive value. When the asynchronous routine is completed, this variable holds an error code, which is a either a negative value, or zero (noErr).
Polling the ioResult field can be a lot more difficult than using a completion routine. In order to poll an ioResult field, you must have access to the parameter block. And, in order to really let the rest of your program continue to go about its business, youd have to poll ioResult fields in your programs main loop, or at some other point in your code that gets executed repeatedly. In order to do this, youd need access to all of your parameter blocks globally. And, if you did have access to all of them globally, youd need a mechanism to find out which ones are being used, and which ones are idle. This is not very intuitive.
The best use for ioResult polling is for doing something in particular while you wait for your asynchronous routine to complete, not to make asynchronous routines transparent to the rest of your program. With ioResult polling, you could execute a routine asychronous, and use the little bit of time immediately after its started executing to update windows, etc, while you wait for it to complete. As far as the rest of your program is concerned, this might as well be considered sychronous execution.
For education purposes, were going to try to mix these two methods to develop a new means of handling asynchronous completion routines. A mechanism which we shall outline will allow you to provide a routine to be executed when the asynchronous routine is completed, yet will execute the routine during normal process time. This is done by polling the ioResult field until the asynchronous routine is complete, and then executing a specified completion routine.
This mechanism offers the best of both worlds, and the drawbacks of neither. You are not limited to non-memory moving routines, and you are able to specify an action be taken at the completion of an asynchronous routine. Because the completion routine is not executed at interrupt time, youre free to deallocate the parameter block with DisposPtr, which would otherwise be off limits.
Our non-interrupt asynchronous completion routine mechanism will maintain a linked list of installed parameter blocks, periodically search this linked list for completed routines, and remove them from the list and execute their completion routines when completed.
First, lets declare our linked list structure :
/* 1 */
typedef void (* CompletionProc)(ParmBlkPtr, long);
struct CompletionHandlerEntry {
ParmBlkPtr pb;
CompletionProc doneProc;
long refCon;
struct CompletionHandlerEntry** nextEntry;
};
typedef struct CompletionHandlerEntry CompletionHandlerEntry;
In Pascal, this would be :
/* 2 */
type
CompletionHandlerEntryHand = ^CompletionHandlerEntryPtr;
CompletionHandlerEntryPtr = ^CompletionHandlerEntry;
CompletionHandlerEntry = record
pb: ParmBlkPtr;
doneProc: ProcPtr;
refCon: longint;
nextEntry: CompletionHandlerEntryHand;
end;
Notice that in the C version of this structure weve declared a function pointer type. There is no equivalent to this in Pascal. Well discuss how the function is executed in Pascal later. Also, notice that in addition to storing the parameter block and completion routine, we are also storing a refCon variable in this structure. refCon is a commonly used name for a variable which the programmer can use for his or her own purposes. Because its a 4-byte value, you can allocate a pointer or a handle and safely store it in a refCon. This means that you can piggy-back any information you would like onto each parameter block in this completion routine mechanism.
Our program will have to maintain a linked list of these structures in order to implement this mechanism. The first entry in the list will have to be kept track of as a global variable, like so :
CompletionHandlerEntry **Asynchs;
In Pascal:
var
Asynchs: CompletionHandlerEntryHand;
When the list is empty, this variable will be NULL (NIL, or zero). When there are entries in the list, this global variable will point the first entry. The list can be walked through by following the nextEntry pointer to the next entry in the list. The last entry in the list will have a nextEntry of NULL. At the start of your program you should make sure to initialize Asynchs to NULL.
Next we need to write two routines which act on these structures. One to allow you to add a parameter block to the list, and another to scan the list for completed routines. First, the routine to add the parameter block to the list :
/* 3 */
void InstallCompletion(ParmBlkPtr pb, CompletionProc theProc,
long refCon)
{
CompletionHandlerEntry ** NewEntry;
NewEntry = (CompletionHandlerEntry **)
NewHandle(sizeof(CompletionHandlerEntry));
if (NewEntry == NULL)
{
while (pb->ioParam.ioResult > 0)
;
(*theProc)(pb, theProc);
}
else
{
(**NewEntry).pb = pb;
(**NewEntry).doneProc = theProc;
(**NewEntry).refCon = refCon;
(**NewEntry).nextEntry = Asynchs;
Asynchs = NewEntry;
}
}
In Pascal :
/* 4 */
procedure InstallCompletion (pb: univ ParmBlkPtr;
theProc: ProcPtr; refCon: longint);
var
NewEntry: CompletionHandlerEntryHand;
begin
Handle(NewEntry) :=
NewHandle(sizeof(CompletionHandlerEntry));
if NewEntry = nil then
begin
while pb^.ioResult > 0 do
;
CallCompletion(pb, theProc);
end
else
begin
NewEntry^^.pb := pb;
NewEntry^^.refCon := refCon;
NewEntry^^.doneProc := theProc;
NewEntry^^.NextOne := Asynchs;
Asynchs := NewEntry;
end;
end;
This routine will first allocate a new completion handler entry. If it fails, it will wait for the asynchronous routine to complete and then call the completion routine. This means the asynchronous routine will be performed pseudo-synchronously when memory is very low. If the allocation is successful, the completion handler entry is added to the beginning of our linked list.
There is room to expand upon this routine. It could be modified to use a default completion routine if a completion routine of NULL is passed to it. The default completion routine would probably only deallocate the parameter block. This routine could also be modified to save certain variables in the linked list structure to be restored when the completion routine is called. This could be very useful if you change your current resource file often. You could save the current resource fork when the asynchronous routine is installed, and restore it temporarily when the completion routine is called.
Next, we need a routine to scan this linked list for completed asynchronous routines, and execute their completion routines:
/* 4 */
void DoCompletions()
{
CompletionHandlerEntry **curEntry = Asynchs;
CompletionHandlerEntry **parentEntry = NULL;
while (curEntry)
{
if ((**curEntry).pb->ioParam.ioResult <= 0)
{
if (parentEntry)
(**parentEntry).nextEntry = (**curEntry).nextEntry;
else
Asynchs = (**curEntry).nextEntry;
(*(**curEntry).doneProc)
((**curEntry).pb, (**curEntry).refCon);
DisposHandle((Handle)curEntry);
curEntry = NULL;
if (parentEntry)
curEntry = (**parentEntry).nextEntry;
else
curEntry = Asynchs;
}
else
{
parentEntry = curEntry;
curEntry = (**curEntry).nextEntry;
}
}
}
In Pascal :
/* 6 */
procedure CallDoneProc (pb: ParmBlkPtr; refCon: longint;
jmpAddr: ProcPtr);
inline
$205F, $4E90;
procedure DoCompletions;
var
curEntry: CompletionHandlerEntryHand;
parentEntry: CompletionHandlerEntryHand;
begin
curEntry = Asynchs;
parentEntry = nil;
while (curEntry <> nil) do
begin
if (curEntry^^.pb^.ioResult <= 0) then
begin
if (parentEntry <> nil) then
parentEntry^^.nextEntry :=
curEntry^^.nextEntry
else
Asynchs := curEntry^^.nextEntry;
CallDoneProc(curEntry^^.pb, curEntry^^.refCon,
curEntry^^.doneProc);
DisposHandle(Handle(curEntry));
curEntry := nil;
if (parentEntry <> nil) then
curEntry := parentEntry^^.nextEntry
else
curEntry := Asynchs;
end
else
begin
parentEntry := curEntry;
curEntry := curEntry^^.nextEntry;
end;
end;
end;
Notice the inline Pascal procedure. What it does is strip the last parameter off the stack, and jump to the address in memory that it represents with the rest of the parameters intact. Although this works, and is a commonly used way to execute ProcPtrs in Pascal, the method we can use in C uses registers much more efficiently.
DoCompletions should be added to your event loop, or to some other point in your code which is executed repeatedly.
One important thing to note about this mechanism is that you cannot call InstallCompletion from within a completion routine. This is because of how the DoCompletion routine is organized. At the point your completion routine is being called, the linked list is being acted upon. Installing a new completion handler entry could cause the linked list to be incorrectly maintained. This can be fixed. Consider it an exercise to better familiarize yourself with the routines used in the mechanism. It would either be a matter of 1) keeping a list of completed asychronous routines, removing them from the linked list, then executing their completion routines, or 2) making DoCompletions execute in such a way that modifications to the linked list during the call to the completion routine are compensated for.