Ken has been designing real-time embedded software for the last several years and is currently a software engineer with Intel. He can be reached at kenneth_gibson@ccm.jf.intel.com.
Multithreaded applications, programs capable of executing more than one section of code concurrently, can solve a number of fprogramming problems, including those found in simulation and real-time device control. Languages like Ada address this by providing support for concurrent processing in their language definition. However, most languages (including C++) do not provide built-in support for the execution of multiple threads.
This article presents a class library that lets you implement a program as a set of concurrent threads. The multitasking class library is written to run under DOS and is built with Microsoft C++ 7.0. In the library, I define a Thread class that can be allocated for each thread of execution. A Scheduler object is defined to schedule the processor for thread execution. In addition, I provide a semaphore class for thread synchronization and a queue class which can be used for interthread communications. To illustrate how you use the class library, I'm providing a sample program electronically (see "Availability," page 3).
Although designed for DOS, the multitasking class library is not difficult to port to other systems. By adding the proper processor initialization and using a locator program, a ROMable image can be created for use on embedded processors in place of a real-time executive.
I wanted the library to be easy to use, flexible, and portable. As such, concurrent processing is achieved by allocating instances of the thread class and specifying a main() function in the constructor for each one. Flexibility is enhanced through a priority-based, preemptive scheduler.
I chose counted semaphores for thread synchronization because they are flexible and can be used to implement higher-level abstractions, such as monitors and pipes. Furthermore, semaphores are designed so that they can be signaled from interrupt service routines. Thus, high-priority threads can preempt lower-priority ones for fast response to external events.
Portability is more difficult because of the processor- and compiler-specific requirements of swapping processor context between threads. However, I've isolated nonportable code to a few assembly functions in one assembly module (Listing Three, page 98), with processor-specific definitions contained in one header file (Listing Four, page 98).
The first class in the library is a doubly linked queue. Queues are integral to the operation of multitasking classes and are used by all of the other classes. Many existing queue classes are available, but I've included my own so that my class library can be used by itself.
The Dlque class is defined in Listing One, page 96. A Dlque object contains a private forward and backward link to other Dlque objects, and public member functions for standard operations to add items, remove items, and check for an empty queue. Additional member functions include Delink(), which removes an item from the middle of a Dlque, and Peak() which looks at the item on the head of a Dlque without removing it.
The doubly linked queue class is defined so that both the queue head and the items placed on the queue are Dlque objects: The constructor for a Dlque object simply sets both forward and backward pointers to point to Dlque. Figure 1 shows an empty Dlque head. After items have been added to the queue, the flink fields each point to the next item on the queue, and the blinks point to the previous item. The exceptions are the blink on the head, which points to the last item on the queue, and the flink on the last item, which points back to the head. Figure 2 shows a queue with two items.
The Add() member function appends a new Dlque object to the end of a queue (see Listing Two, page 96). Since the Dlque object to be added is not on a queue, the constructor has initialized both flink and blink to point to the new item. Add() first sets the blink of the new item to the blink of the head. If the queue is empty, the blink in the head points to itself. Otherwise, it points to the current end of the queue. Add() sets the flink of the new item to point back to the queue head, because this item is added at the end of the queue. Add() then sets the flink of the Dlque object pointed to by the blink of the head to point to the new item being inserted. This could be the flink of either the head or the previous last item on the queue. Finally, Add() sets the blink of the queue head to point to the new item.
Remove() removes the first item on a queue. It first checks for an empty queue and returns NULL if it finds one. Otherwise, it gets the pointer to the first item from the flink in the head. Next, it updates the flink of the head to point to the item that was pointed to by the flink of the item being removed. This is either the next item on the queue, or a pointer back to the queue head (if only one item was on the queue). Remove() then sets the blink of the next item in the queue to the blink of the item being removed. If the queue had only one item on it, the head is left with its blink and flink pointing to the head itself. Finally, Remove() updates the flink and blink of the removed item to point to itself.
The Delink() member function is used to remove a Dlque object from the middle of a queue. It sets the flink of the previous item on the queue (referenced by the blink) to the flink of the item being removed, and the blink of the next item on the queue to the blink of the item being removed.
The Scheduler object can be viewed as the kernel of a multitasking application that uses the class library presented with this article. Although users do not use the Scheduler class directly, an instance of a Scheduler object is created by the class library for use by the thread and semaphore objects in an application. As Listing One shows, the scheduler contains a table of all threads in the application; the prioritized ready list; and member functions to set up threads at initialization time, select which threads to execute, and swap processor context between threads. The scheduler does this through a combination of portable C++ member functions and calls to processor-specific assembly functions.
When the scheduler is created, its constructor initializes its thread table (which contains all threads in the application) to be initially empty. It initializes its pointer to the free stack space available for allocation to individual threads. Each thread must have its own region of free stack, which the scheduler must allocate from the application's stack space. The CurStackBase member variable is initialized using the processor-specific InitStackBase() in its constructor. This function simply returns the current value of the stack pointer. As a thread is created, its constructor calls the scheduler's GetStackSpace() member function to reserve stack space and obtain an initial stack pointer. Assuming the stack grows toward low memory, GetStackSpace() subtracts the thread's requested stack space from CurStackBase to allocate a region of free stack for this new thread and leaves CurStackBase pointing to the new beginning of free application stack. GetStackSpace() then returns the previous value of CurStackSpace as the base of the new thread's stack. Figure 3 shows the stack configuration for an application that has created two threads.
GetStackSpace() also enforces a minimum stack size for each thread, because although a thread may not allocate any stack variables, on many processors interrupts push data onto the application's stack using the stack pointer at the time of the interrupt. If there's not enough free stack space to accommodate this data, the stack pointer will cross into the adjacent thread's stack and corrupt its data.
Finally, the constructor creates the NULL thread, which guarantees that when the scheduler reschedules the threads on the processor, the NULL thread will be ready to run. This can happen when all of the application threads are blocked, waiting on some event. The NULL thread executes an idle loop and runs at a priority level below any application threads, allowing one to preempt it when it's ready to run.
The AddThread() member function adds new threads into the scheduler. It first searches the thread table for an empty entry and, if one is found, enters a pointer to the new thread. It then calls the new thread's MakeReady() member function to set the thread's state to READY and enters the new thread on the ready list at the specified priority level. The ready list is implemented as an array of Dlque objects. Each Dlque holds the ready threads for one priority level; AddReady() just adds the thread to the appropriate Dlque.
The Resched() member function selects threads in the application for execution. It searches the array of ready queues in priority order for a nonempty queue. The NULL thread ensures that at least one ready thread can be scheduled. After selecting a thread, Resched() checks whether this new thread is the same as the last current thread. If so, the processor is already running in the correct thread's context, and Resched() simply returns. If not, Resched() calls the new thread's MakeCurrent() member function, which marks the new thread's state as CURRENT, points the CurrentThread pointer to the new thread, and calls the ContextSwitch() member function. ContextSwitch() makes an inline call to the assembly-language AsmContextSwitch(), which does the processor-specific work of swapping context between the old and new threads.
AsmContextSwitch() takes as parameters pointers to the old and new thread's pregs structures, processor-specific structures containing the registers that must be part of a thread's saved context; see Listing Four. AsmContextSwitch() gets the pointer to the old thread's saved-register area into an internal register after saving the value of that register on the stack. Since this function is always entered through a function call, registers not conserved across function calls need not be saved. AsmContextSwitch() saves those that must be conserved into the saved-register area. The current values of the stack and instruction pointers are not saved, however. Instead, the return address is taken off the stack and saved as the instruction pointer, and the stack pointer is incremented so that when this thread is rescheduled, the context will be restored as if it had just returned to Resched().
Next, Resched() gets the pointer to the new thread's saved registers and restores all of the registers except the instruction pointer and the register pointing to the saved-register area. The final steps are to push the saved instruction pointer onto the stack, restore the register currently pointing to the saved-register structure, and execute a return instruction that pops the saved instruction pointer off the stack and begins executing in the new thread. Listing Three provides an example of AsmContextSwitch() for the Intel architecture.
Pause(), the next member function, allows a thread to voluntarily relinquish control of the processor to other threads of the same priority. Pause() puts the calling thread back on the end of the ready list and calls ReSched().
The last function in the scheduler is StartMultiTasking(), which is called at initialization and transforms a single-threaded application to a set of threads, each running in its own context. StartMultiTasking() first sets up the scheduler such that the NULL thread appears to be the current thread by removing it from the ready list, setting its state to CURRENT, and pointing the scheduler's CurrentThread member variable to point to the NULL thread. StartMultiTasking() calls Pause(), which calls Resched() to select the highest-priority thread for execution and call ContextSwitch(). This saves the current context as the NULL thread's context and begins executing the selected thread. If the NULL thread is later rescheduled, it returns from the call to Resched() in StartMultiTasking() and executes the next statement in this function, which is an infinite loop.
The Scheduler class is not instantiated by users of the class library. Instead, the library creates a Scheduler object during initialization. The library must guarantee that only one instance of the scheduler is created and that its constructor is executed before users can create any thread objects. Allocating one instance of the scheduler in a .cpp module and referencing it as an extern from a header file won't work because C++ does not guarantee the execution order of constructors for objects statically allocated in different modules. In that case, a class-library user could statically allocate thread objects, and their constructors could be executed before the constructor for the scheduler.
This is addressed by the SchedulerInit helper class in Listing One. The header file that defines the multitasking classes allocates a static instance of SchedulerInit in each module that includes it. SchedulerInit contains a static count variable that is initialized to 0 at compile time. The constructor for SchedulerInit increments this count each time it is called and only creates an instance of the scheduler when the count is 0. It also initializes the static pointer Scheduler::InstancePtr to point to this single instance of the scheduler so that other classes can reference it.
The Thread class in Listing One contains the context of a thread of execution within the application. Private member variables include the Dlque object that places the thread on the ready list or on semaphores, and the processor-specific pregs structure for saving the thread's processor state when the thread must block. A thread also stores its current state in its private area. Figure 4 shows the allowable states and state transitions for a thread. A thread can either be CURRENT, BLOCKED, or READY. A READY thread waits on the ready list and will transition to the CURRENT state when it becomes the highest-priority thread to run. A currently running thread can transition to the BLOCKED state by waiting on a semaphore or back to READY if preempted by a higher-priority thread, or if it signals a semaphore with a waiting thread of equal or higher priority. A BLOCKED thread waiting on a semaphore will return to the READY state when the semaphore is signaled and the thread is at the head of the waiter's list.
Public member functions on threads include functions to set the current state of the thread as well as the constructor. The thread constructor takes a pointer to a main() function as a parameter; optional parameters may be provided to specify the amount of stack space and the priority. The constructor allocates a region of free stack space from the scheduler, gets its initial stack pointer, and places a pointer to the static ThreadRet() on the thread's stack so that if the thread ever returns from its main() function, it will return to ThreadRet().
The thread constructor also calls the assembly-language InitPregs() to get its initial saved-processor registers. These are initialized so that the first time the thread is scheduled, it begins executing at its main() function. An InitPregs() for the Intel architecture is in Listing Three. Finally, the constructor sets the thread state to READY and enters the thread into the scheduler's ready list to await execution.
A semaphore is defined as a subclass of a Dlque since one of its primary functions is to queue waiting threads. Otherwise, it is a straightforward implementation of a counted semaphore. The constructor for a semaphore allows the option to specify a nonzero initial count: If none is provided, it defaults to 0. Wait() first checks for a nonzero count. If nonzero, the count is decremented and Wait() returns to the caller. If 0, then the calling thread must be blocked. It changes the calling thread to the blocked state, queues it on the list of waiting threads, and calls the scheduler's Resched() to switch to another thread.
The semaphore's Signal() first checks for any waiting threads. If none are present, it increments the count and returns to the caller. If threads are waiting, Signal() removes the next waiting thread from the list, readies it to run, and returns it to the ready list. Signal() then checks to see if this is now the highest-priority thread and if so, returns the calling thread to the ready list and tells the scheduler to perform a context switch.
When using the class library, application programmers provide the main-
level functions for the threads that they create. The class library provides the main() for the application where it initiates concurrent processing among the threads. When main() is executed, the scheduler's constructor will have already executed. The scheduler requires that at least one thread be statically allocated so that it will be initialized and entered into the ready list when main() is called. As shown in Listing Two, main() calls Scheduler::StartMultiTasking() to begin executing in a thread context.
The multitasking class library presented here allows C++ programmers to write programs as a set of concurrent threads. It does so using thread and semaphore classes and a scheduler object. While this is particularly relevant for real-time system designers, it can also be a valuable addition to any C++ programmer's toolbox.
// threads.h -- Multitasking class definitions
#ifndef THREADS_H
#define THREADS_H
#include "specific.h"
#define TRUE 1
#define FALSE 0
typedef void (*vfptr)();
class Thread;
class Semaphore;
// Stack size values
#define MIN_STACK 0x400 // Minimum stack size per thread
#define NULL_STACK 0x400 // Space for NULL thread
#define INIT_STACK 0x080 // Space for scheduler initialization
#define DEFAULT_STACK 0x400 // Default size
// Values for thread states
#define THREAD_UNUSED 0 // Thread Table entry unused
#define THREAD_READY 1 // Thread is ready to run
#define THREAD_CURRENT 2 // Thread is currently running
#define THREAD_BLOCKED 3 // Blocked on a sem or timer
// Thread priorities
#define LOWEST_PRIORITY 4
#define HIGHEST_PRIORITY 0
#define NULL_PRIORITY (LOWEST_PRIORITY+1)
// Max number of threads to allow
#define MAX_THREADS 8
#define MAX_THREADID (MAX_THREADS-1)
// Doubly Linked Queue class
class Dlque
{
private:
Dlque *flink; // Forward Link
Dlque *blink; // Backward Link
public:
int Empty(); // Check for empty queue
void Add( Dlque *Queue ); // Add to the back
Dlque *Remove(); // Remove from the front
void Delink(); // Remove from the middle
Dlque *Peak(); // Look at front without removing.
Dlque() { flink = blink = this; }
~Dlque() {}
};
// The SCHEDULER class
class Scheduler
{
private:
Thread *ThreadTab[MAX_THREADS];
Dlque ReadyList[NULL_PRIORITY+1];
char *CurStackBase;
Thread *NullThread;
void ContextSwitch( pregs *OldRegs, pregs *NewRegs )
{ asmContextSwitch( OldRegs, NewRegs ); }
public:
static Scheduler *InstancePtr; // Ptr to one and only instance
Thread *CurrentThread; // Current thread
char *GetStackSpace( unsigned Size );
void ReSched(); // Reschedule threads
void AddReady( Thread *pThread );
void RemoveReady( Thread *pThread );
char AddThread( Thread *pThread );
void Pause();
void StartMultiTasking();
Scheduler();
~Scheduler() {}
};
// Scheduler initialization class. Insures that only one instance
// of the Scheduler is created no matter how many modules include
// threads.h. Also insures that it is created before any threads.
class SchedulerInit
{
private:
static int count; // Compile time initialized to 0
public:
SchedulerInit() { if( count++ == 0 )
Scheduler::InstancePtr = new Scheduler; }
~SchedulerInit() { if( --count == 0 )
delete Scheduler::InstancePtr; }
};
static SchedulerInit SchedInit;
// THREAD class
class Thread
{
private:
friend class Scheduler;
friend class Semaphore;
Dlque Queue; // For putting threads on Queues
pregs Regs; // Processor specific saved registers
char State; // Current thread state
static void ThreadRet(); // Called if a thread returns from main
public:
char id; // Thread ID
unsigned Priority;
void MakeReady() { State = THREAD_READY; }
void MakeCurrent() { State = THREAD_CURRENT; }
void MakeBlocked() { State = THREAD_BLOCKED; }
Thread( vfptr MainRtn,
unsigned Priority=LOWEST_PRIORITY,
unsigned StackSpace=DEFAULT_STACK );
~Thread() {}
};
// SEMAPHORE class
class Semaphore : Dlque
{
private:
short count;
public:
void Wait();
void Signal();
Semaphore( short InitCount=0 );
~Semaphore() {}
};
#endif // THREADS_H
// threads.cpp -- Implementation of Multitasking Classes
#include <stdio.h>
#include <stdlib.h>
#include "threads.h"
#define TRUE 1
#define FALSE 0
// Static count of SchedulerInit object that have been created.
int SchedulerInit::count = 0;
// Pointer to the one instance of the scheduler.
Scheduler *Scheduler::InstancePtr;
// Dlque::Empty -- Returns TRUE if the Dlque is empty.
inline int Dlque::Empty()
{
return( flink == this );
}
// Dlque::Add -- Adds an item to the end of a doubly linked queue.
void Dlque::Add( Dlque *Queue )
{
Queue->blink = blink;
Queue->flink = this;
blink->flink = Queue;
blink = Queue;
}
// Dlque::Remove -- Removes item at the head of the dlque. NULL if empty.
Dlque *Dlque::Remove()
{
Dlque *Item;
if( Empty() ) {
return( NULL );
}
Item = flink;
flink = Item->flink;
Item->flink->blink = Item->blink;
Item->flink = Item->blink = Item;
return( Item );
}
// Dlque::Delink -- Delinks an item from the middle of a dlque.
void Dlque::Delink()
{
blink->flink = flink;
flink->blink = blink;
flink = blink = this;
}
// Dlque::Peak -- Returns a pointer to the first item without removing it.
Dlque *Dlque::Peak()
{
if( Empty() ) {
return( NULL );
}
return( flink );
}
// Scheduler Constructor
Scheduler::Scheduler()
{
short i;
InstancePtr = this;
// Initialize the Thread Table
for( i=0; i<MAX_THREADS; ++i ) {
ThreadTab[i] = NULL;
}
// Initialize System Stack Base to the current stack pointer
CurStackBase = InitStackBase();
// Allocate space for scheduler initialization
CurStackBase -= INIT_STACK;
// Create the NULL Thread.
NullThread = new Thread( NULL, NULL_PRIORITY, NULL_STACK );
}
// GetStackSpace -- Used by new threads to get their initial SP
char *Scheduler::GetStackSpace( unsigned Size )
{
char *Base;
if ( Size < MIN_STACK ) {
Size = MIN_STACK;
}
Base = CurStackBase;
CurStackBase -= Size; // Assume stack grows toward low mem.
return Base;
}
// Scheduler::AddThread -- Add a new thread into the Scheduler
char Scheduler::AddThread( Thread *pThread )
{
register char id;
for( id=0; id<MAX_THREADS; ++id ) {
if( ThreadTab[id] == NULL ) {
break;
}
}
if( id == MAX_THREADS ) {
return( FALSE );
}
ThreadTab[id] = pThread;
pThread->MakeReady(); // Tell new thread to make itself READY
AddReady( pThread ); // Add to ready list in the scheduler
return( TRUE );
}
// AddReady -- Add the given thread to the ReadyList
inline void Scheduler::AddReady( Thread *pThread )
{
ReadyList[pThread->Priority].Add( &pThread->Queue );
}
// RemoveReady -- Remove the specified thread from the ready list.
inline void Scheduler::RemoveReady( Thread *pThread )
{
pThread->Queue.Delink();
}
// Scheduler::ReSched -- Picks next ready thread and calls ContextSwitch to
// perform the context switch to the new thread.
void Scheduler::ReSched()
{
Thread *OldThread;
Thread *NewThread;
unsigned Priority;
for( Priority=0; Priority<=NULL_PRIORITY; ++Priority ) {
if( !ReadyList[Priority].Empty() ) {
NewThread = (Thread *)ReadyList[Priority].Remove();
break;
}
}
// If calling thread is still ready and is the highest
// priority ready thread, just return
if( NewThread == CurrentThread ) {
CurrentThread->MakeCurrent();
return;
}
OldThread = CurrentThread;
CurrentThread = NewThread;
CurrentThread->MakeCurrent();
ContextSwitch( &OldThread->Regs, &CurrentThread->Regs );
}
// Scheduler::Pause -- Checks for any ready threads that are equal or higher
// priority than the calling thread. If so, reshcedules.
void Scheduler::Pause()
{
short SavedPS;
SavedPS = DisableInt();
CurrentThread->MakeReady(); // Switch from Current to Ready
AddReady( CurrentThread ); // Caller back on end of ReadyList
ReSched(); // Run new highest priority thread
EnableInt( SavedPS );
}
// StartMultiTasking -- Perform transformation from a single threaded
// application to a set of threads running in individual contexts. This
// is done by first setting up the system variables to look like the Null
// thread is the current thread. Then, call Pause() which will cause the
// context of this routine to be saved as the Null thread's context. When
// the Null thread is rescheduled, the CPU will return to this routine.
// Rest of this routine then becomes the loop that runs in the Null thread.
void Scheduler::StartMultiTasking()
{
RemoveReady( NullThread );
CurrentThread = NullThread;
NullThread->MakeCurrent();
Pause();
while( TRUE ); // Loop in the NULL thread
}
// Thread::Thread -- Creates a new thread based on the specified params.
Thread::Thread( vfptr MainRtn,
unsigned TaskPriority, unsigned StackSpace )
:Queue()
{
short *StackPtr;
short RegContents;
// Set up the initial stack so that if the main routine for this
// thread returns for some reason, it returns to ThreadRet
StackPtr
= (short*)Scheduler::InstancePtr->GetStackSpace(StackSpace);
*StackPtr = (short)Thread::ThreadRet;
// Call processor/compiler specific routine to initialize
// the saved processor registers.
InitPregs( &this->Regs, (short)StackPtr, (short)MainRtn );
Priority = TaskPriority;
Scheduler::InstancePtr->AddThread( this );
MakeReady(); // Set our state to READY
}
// ThreadRet -- Routine that is placed on each thread's stack as the return
// address in case the 'main' routine ever returns.
void Thread::ThreadRet()
{
#ifdef _DEBUG
printf( "A Thread returned from main()\n" );
#endif
exit( 1 );
}
// Semaphore::Semaphaore -- Constructor for objects of the class semaphore.
Semaphore::Semaphore( short InitCount )
{
count = InitCount;
}
// Semaphore::Wait -- Queue a thread as a waiter on a semaphore
void Semaphore::Wait()
{
short SavedPS;
SavedPS = DisableInt();
if( count ) // No need to block waiter
{
--count;
}
else // Waiter must block
{
Scheduler::InstancePtr->CurrentThread->MakeBlocked();
Add( &Scheduler::InstancePtr->CurrentThread->Queue );
Scheduler::InstancePtr->ReSched();
}
EnableInt( SavedPS );
}
// Semaphore::Signal -- Signal a semaphore
void Semaphore::Signal()
{
short SavedPS;
Thread *Waiter;
SavedPS = DisableInt();
if( Empty() ) // No waiters to reschedule
{
++count;
}
else // There are blocked waiters
{
Waiter = (Thread*)Remove(); // Get next waiter
Waiter->MakeReady(); // Make it ready
Scheduler::InstancePtr->AddReady( Waiter );
if( Waiter->Priority <
Scheduler::InstancePtr->CurrentThread->Priority ) {
Scheduler::InstancePtr->CurrentThread->MakeReady();
Scheduler::InstancePtr->AddReady(
Scheduler::InstancePtr->CurrentThread );
Scheduler::InstancePtr->ReSched();
}
}
EnableInt( SavedPS );
}
// main()
void main()
{
Scheduler::InstancePtr->StartMultiTasking();
}
; Intel Architecture specific routines.
.MODEL small
.CODE ; Create C compatible CS
; Offsets into the saved register area for each register
AX_OFST = 0
BX_OFST = 2
CX_OFST = 4
DX_OFST = 6
BP_OFST = 8
SI_OFST = 10
DI_OFST = 12
DS_OFST = 14
SS_OFST = 16
ES_OFST = 18
PSW_OFST= 20
PC_OFST = 22
SP_OFST = 24
INIT_PSW = 0200h ;Thread's initial Processor Status Word
; Return the current stack pointer. This will be used as a reference for
; assigning the stack base for each thread.
; C Prototype: char *InitStackBase( void );
PUBLIC _InitStackBase
_InitStackBase PROC
mov ax, sp
sub ax, 2 ;Where it will be after return
ret
_InitStackBase ENDP
; asmContextSwitch - Switches processor context between two threads
; C Prototype: void asmContextSwitch( pregs *OldRegs, pregs *NewRegs );
; 1. Assume SMALL or COMPACT memory model. Don't save and restore CODE,
; STACK, or DATA SEGMENTS. These always stay the same.
; 2. Assume Microsoft and Borland C calling conventions. This routine will
; always be 'called' and the registers AX, BX, CX, DX do not need to be
; preserved across procedure calls and are not saved and restored here.
PUBLIC _asmContextSwitch
_asmContextSwitch PROC
; Currently have: SP -> Return Address
; SP+2 -> Old reg save area pointer
; SP+4 -> New reg save area pointer
push si ;Save old task's SI
push bp ;And BP
mov bp, sp ;Get back to the base of the stack frame
add bp, 4
mov si, [bp+2] ;Get pointer to old register save area
pop [si+BP_OFST] ;Save old process's BP in save area
pop [si+SI_OFST] ;and SI
mov [si+DI_OFST], di ;and rest of the regs that must be saved
mov [si+ES_OFST], es
pushf ;Push PSW onto the stack
pop [si+PSW_OFST] ;then pop into save area
; Save the return address as the saved PC and increment the SP before
; saving so context will be restored as if just returned to ReSched
mov bx, [bp] ;Get return address off the stack
mov [si+PC_OFST], bx
mov bx, sp ;Increment SP
add bx, 2
mov [si+SP_OFST], bx ;and save
mov si, [bp+4] ;Get new process's saved regs
mov bp, [si+BP_OFST] ;and restore registers
mov di, [si+DI_OFST]
mov es, [si+ES_OFST]
push [si+PSW_OFST] ;Push new PSW onto the stack
popf ;then pop into PSW
mov sp, [si+SP_OFST] ;Switch to new stack
; Push the saved PC on the stack to be restored when RET is executed
push [si+PC_OFST]
mov si, [si+SI_OFST] ;Finally, restore SI
ret
_asmContextSwitch ENDP
; InitPregs -- Sets the initial saved processor register for a new thread.
; C Prototype: void InitPregs(pregs* pRegs,short InitStack,short MainRoutine );
PUBLIC _InitPregs
_InitPregs PROC
push si
push bp
mov bp, sp
add bp, 4
mov si, [bp+2] ;Get pointer to pregs
; Assume SMALL or COMPACT memory model and set the
; initial segments the same as the current ones
mov [si+SS_OFST], ss
mov [si+DS_OFST], ds
mov [si+ES_OFST], es
mov [si+PSW_OFST], INIT_PSW
mov ax, [bp+4]
mov [si+SP_OFST], ax ;Stackbase
mov ax, [bp+6]
mov [si+PC_OFST], ax ;Main Routine
pop bp
pop si
ret
_InitPregs ENDP
; DisableInt - Disables Interrupts and returns current Processor Status Word
; C Prototype: short DisableInt( void );
PUBLIC _DisableInt
_DisableInt PROC
pushf
pop ax
cli
ret
_DisableInt ENDP
; EnableInt - Enables interrupts IF enabled in saved Processor Status Word
; C Prototype: void EnableInt( short );
PUBLIC _EnableInt
_EnableInt PROC
push bp
mov bp, sp
mov ax, [bp+4] ;Get saved Processor Status Word
and ax, 0200h ;If Interrupts were enabled
jz NoEnable
sti ;then re-enable them
NoEnable:
pop bp
ret
_EnableInt ENDP
END
// specific.h -- Processor and compiler specific definitions
#ifndef SPECIFIC_H
#define SPECIFIC_H
// Intel processor saved register area.
struct pregs
{
short ax; // Offset 0
short bx; // 2
short cx; // 4
short dx; // 6
short bp; // 8
short si; // 10
short di; // 12
short ds; // 14
short ss; // 16
short es; // 18
short psw; // 20
short pc; // 22
short sp; // 24
};
// Processor specific routines in specific.asm
extern "C" void asmContextSwitch( pregs*, pregs* );
extern "C" void InitPregs( pregs*, short, short );
extern "C" char *InitStackBase( void );
extern "C" short DisableInt( void );
extern "C" void EnableInt( short );
#endif
Copyright © 1994, Dr. Dobb's Journal