home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
World of Shareware - Software Farm 2
/
wosw_2.zip
/
wosw_2
/
CPROG
/
MTL100JE.ZIP
/
THREADS.CPP
< prev
next >
Wrap
C/C++ Source or Header
|
1993-05-06
|
29KB
|
847 lines
//--------------------------------------------------------------------------
//
// THREADS.CPP: body of DOS multithreading library.
// Copyright (c) J.English 1993.
// Author's address: je@unix.brighton.ac.uk
//
// Permission is granted to use copy and distribute the
// information contained in this file provided that this
// copyright notice is retained intact and that any software
// or other document incorporating this file or parts thereof
// makes the source code for the library of which this file
// is a part freely available.
//
//--------------------------------------------------------------------------
//
// Note: this library is highly DOS specific and hence non-portable.
// It also involves the use of assembly language and interrupt
// functions, so it is also very compiler-specific and will need
// modification for use with compilers other than Borland C++ 3.0
// or later.
//
// Revision history:
// 1.0 March 1993 Initial coding
//
//--------------------------------------------------------------------------
#include "threads.h"
#include <dos.h>
//--------------------------------------------------------------------------
//
// Assembler sequences.
//
#define DISABLE asm { pushf; cli; } // save and disable interrupts
#define ENABLE asm { popf; } // restore interrupt state
#define PUSHAD asm { db 0x66, 0x60; } // push extended registers
#define POPAD asm { db 0x66, 0x61; } // pop extended registers
#define PUSH_FS asm { db 0x0F, 0xA0; } // push FS register
#define PUSH_GS asm { db 0x0F, 0xA8; } // push GS register
#define POP_FS asm { db 0x0F, 0xA1; } // pop FS register
#define POP_GS asm { db 0x0F, 0xA9; } // pop GS register
//--------------------------------------------------------------------------
//
// Typedefs and constants.
//
typedef void interrupt (*Handler)(...); // interrupt handler type
typedef volatile unsigned Register; // interrupt handler parameter type
const unsigned MIN_STACK = 512; // minimum stack size
const long DAY_LENGTH = 1573040L; // length of day in timer ticks
//--------------------------------------------------------------------------
//
// Global (static) variables.
//
static DOSThreadManager* current = 0; // current thread
static DOSThreadManager* ready = 0; // queue of ready threads
static DOSThreadManager* delayed = 0; // queue of delayed threads
static DOSThread* mainthread = 0; // thread for execution of "main"
static unsigned mainsetup = 0; // flag set when constructing "main"
static unsigned slicesize = 1; // length of timeslice in clock ticks
static unsigned i386; // flag set if executing on 386/486
static volatile unsigned threadcount = 0; // number of active threads
static volatile unsigned breakflag = 0; // flag set by control-break
static volatile long nextslice = 0; // tick count for next timeslice
static volatile long far* currtime = (volatile long far*) MK_FP(0x40,0x6C);
static volatile char far* midnight = (volatile char far*) MK_FP(0x40,0x70);
// timer values in BIOS data area
//--------------------------------------------------------------------------
//
// Interrupt function pointers.
//
static Handler old_timer; // original timer interrupt handler
static Handler old_dos; // original DOS services handler
static Handler old_break; // original control-break handler
static Handler old_error; // original critical error handler
//--------------------------------------------------------------------------
//
// Identify CPU.
//
// This function is needed so that on 386/486 processors the
// extended registers can be saved as part of a thread's context.
// This is necessary in case the thread is interrupted during a
// routine that relies on these registers being preserved. On
// the 386 and above, bits 12 - 14 of the flag register can be
// written to; on earlier processors, they are either always 1
// (8086) or always 0 (286). The global variable "i386" is set
// to 1 if we are executing on a 386 or above, and 0 otherwise.
//
void interrupt cputype ()
{
//--- Assume a 386 or above to start with
i386 = 1;
//--- Test for an 8086 (bits 12 - 15 of flags register always 1)
_FLAGS = 0;
if ((_FLAGS & 0xF000) == 0xF000)
i386 = 0;
//--- Test for a 286 (bits 12 - 14 of flags register always 0)
_FLAGS = 0x7000;
if ((_FLAGS & 0x7000) == 0)
i386 = 0;
}
//--------------------------------------------------------------------------
//
// Class DOSNullThread.
//
// A concrete derivation of DOSThread used for the null thread and
// the main thread. The main body just sits in an infinite loop.
// A minimal stack is allocated for the purpose.
//
class DOSNullThread : public DOSThread
{
public:
DOSNullThread () : DOSThread (MIN_STACK) { }
protected:
virtual void main ();
};
void DOSNullThread::main ()
{
for (;;) ; // do nothing
}
//--------------------------------------------------------------------------
//
// Class DOSCallMonitor.
//
// This class is a monitor which protects against re-entrant DOS
// calls. It contains the DOS interrupt handler which uses "lock"
// and "unlock" to prevent DOS being re-entered. Since interrupt
// routines must be static and thus have no "this" pointer, there
// is a single instance of this class declared which the interrupt
// handler can use when calling "lock" and "unlock".
//
class DOSCallMonitor : public DOSMonitor
{
public:
static void interrupt dos_int // handle DOS service calls
(Register, Register, Register, Register,
Register, Register, Register, Register,
Register, Register, Register, Register);
};
static DOSCallMonitor dos; // instance used by DOS handler
//--------------------------------------------------------------------------
//
// Class DOSThreadManager.
//
// This is a support class used for maintaining queues of threads.
// A queue is represented as a circular list of DOSThreadManagers.
// Each queue is headed by a DOSThreadManager with a null "thread"
// pointer, so that queues and threads can be treated in a unified
// manner -- it guarantees that queues will never be empty, and
// simplifies moving threads around. The only exception to this
// is the header for the ready queue, which has a pointer to the
// null thread instead of a null pointer. This guarantees that the
// ready queue always contains a runnable thread and that the null
// thread will only ever be executed when the ready queue contains
// no other runnable threads. The static member functions are also
// included in this class so they can access the private parts of
// the threads being controlled.
//
class DOSThreadManager
{
public:
DOSThreadManager (DOSThread* t = 0) : thread (t), critflag (0)
{ next = prev = this; }
void move (DOSThreadManager* t,
DOSThread::State s); // move to position before "t"
DOSThreadManager* next; // next entry in list
DOSThreadManager* prev; // previous entry in list
DOSThread* thread; // thread for this entry
unsigned far* stkptr; // stack pointer
unsigned long wakeup; // wakeup time in clock ticks
unsigned critflag; // flag set during critical errors
static void far start (DOSThread* t) // execute thread and then shut down
{ t->main(); t->terminate(); }
static void cleanup (); // restore interrupt vectors at exit
static void create (); // create main & null threads
static void interrupt destroy (); // destroy main & null threads
static void interrupt schedule (); // schedule next thread
static void interrupt timer_int (); // handle timer interrupts
static void interrupt break_int (); // handle control-break
static void interrupt error_int // handle critical errors
(Register, Register, Register,
Register, Register, Register,
Register, Register, Register);
};
//--------------------------------------------------------------------------
//
// Timer interrupt handler.
//
void interrupt DOSThreadManager::timer_int ()
{
//--- call old timer handler
old_timer ();
//--- move current thread to back of ready queue at end of timeslice
long now = *currtime;
if (nextslice >= DAY_LENGTH && *midnight != 0)
nextslice -= DAY_LENGTH;
if (slicesize > 0 && now >= nextslice && current == ready->next)
current->move (ready, DOSThread::READY);
//--- make threads with expired delays runnable
DOSThreadManager* t = delayed->next;
DOSThreadManager* r = ready->next;
while (t != delayed)
{ if (t->wakeup >= DAY_LENGTH && *midnight != 0)
t->wakeup -= DAY_LENGTH;
if (now < t->wakeup)
break;
t = t->next;
t->prev->move (r, DOSThread::READY);
}
//--- reschedule
DOSThreadManager::schedule ();
}
//--------------------------------------------------------------------------
//
// Control-break interrupt handler.
//
// This routine just sets a flag for polling by individual threads.
//
void interrupt DOSThreadManager::break_int ()
{
breakflag = 1;
}
//--------------------------------------------------------------------------
//
// Critical error interrupt handler.
//
// This calls the critical error handler for the current thread
// (which will always be the current one, since only one thread
// can be executing a DOS call at any one time). "Critflag"
// is set to inform the DOS interrupt handler that a critical
// error is being handled.
//
void interrupt DOSThreadManager::error_int
(Register, Register di, Register,
Register, Register, Register,
Register, Register, Register ax)
{
current->critflag = 1;
ax = current->thread->DOSerror ((ax & 0xFF00) | (di & 0x00FF)) & 0xFF;
current->critflag = 0;
}
//--------------------------------------------------------------------------
//
// DOS service interrupt handler.
//
// Since DOS is not re-entrant, this handler is required to protect
// against threads making DOS calls while there is already one in
// progress. It uses the monitor "dos" to prevent re-entrancy.
// However, if a critical error occurs, the thread's critical error
// handler may make a DOS call (but only functions 00 to 0C). In
// this case, the lock operation is bypassed (the thread must already
// be in a DOS function) but the function code is checked.
//
void interrupt DOSCallMonitor::dos_int
(Register, Register di, Register si, Register,
Register es, Register dx, Register cx, Register bx,
Register ax, Register, Register, Register flags)
{
if (current->critflag == 0)
dos.lock (); // prevent reentrance to DOS
else if (ax > 0x0C)
return; // critical error, functions > 0x0C not allowed
//--- load registers from stacked values
_FLAGS = flags;
_DI = di;
_SI = si;
_ES = es;
_DX = dx;
_CX = cx;
_BX = bx;
_AX = ax;
//--- call old DOS handler
old_dos ();
//--- store registers in stacked copies
ax = _AX;
bx = _BX;
cx = _CX;
dx = _DX;
es = _ES;
si = _SI;
di = _DI;
flags = _FLAGS;
if (current->critflag == 0)
dos.unlock (); //--- allow other threads in
}
//--------------------------------------------------------------------------
//
// DOSThreadManager::cleanup.
//
// This is called by the thread manager destructor when the last
// thread is destroyed, or by "atexit" if a quick exit happens. It
// just restores the original interrupt vectors, so if it is called
// multiple times during exit it won't matter.
//
void DOSThreadManager::cleanup ()
{
//--- unhook DOS vector by hand (can't use a DOS call to do it!)
DISABLE;
Handler far* dos = (Handler far*) MK_FP(0, 0x21*4);
*dos = old_dos;
ENABLE;
//--- unhook other vectors
setvect (0x08, old_timer);
setvect (0x23, old_break);
setvect (0x24, old_error);
}
//--------------------------------------------------------------------------
//
// DOSThreadManager::move.
//
// Move the caller's queue entry from its current position to the
// position before entry "t". This must NEVER be used to move an
// entry which marks the head of a queue! If "t" is a null pointer,
// the entry is just unlinked from its current list and left hanging.
// If the current thread is affected, a new thread is scheduled.
//
void DOSThreadManager::move (DOSThreadManager* t, DOSThread::State s)
{
DISABLE;
//--- change thread status
thread->state = s;
//--- detach thread from current queue
next->prev = prev;
prev->next = next;
//--- attach before specified position (if any)
if (t != 0)
{ next = t;
prev = t->prev;
t->prev->next = this;
t->prev = this;
}
ENABLE;
}
//--------------------------------------------------------------------------
//
// DOSThreadManager::create.
//
// Register the creation of a thread by incrementing the number of
// threads, and initialise the system if it is the first thread to
// be created.
//
void DOSThreadManager::create ()
{
if (threadcount++ == 0)
{
//--- set "i386" if the processor being used is a 386 or above
cputype ();
//--- create the delay queue
delayed = new DOSThreadManager;
//--- create the ready queue and the null thread
ready = (new DOSNullThread)->entry;
ready->move (ready, DOSThread::READY);
//--- create the main thread (heavily bodged with "mainsetup")
mainsetup = 1;
mainthread = new DOSNullThread;
mainsetup = 0;
mainthread->entry->move (ready, DOSThread::READY);
//--- save interrupt vectors
old_timer = getvect (0x08);
old_dos = getvect (0x21);
old_break = getvect (0x23);
old_error = getvect (0x24);
atexit (cleanup); // take care of sudden exits
//--- hook interrupts (DOS interrupt last!)
setvect (0x24, Handler (error_int));
setvect (0x23, Handler (break_int));
setvect (0x08, Handler (timer_int));
setvect (0x21, Handler (DOSCallMonitor::dos_int));
// DOS calls unsafe now
//--- set the thread count and the current thread
threadcount = 1;
current = mainthread->entry; // DOS calls safe again
DOSThreadManager::schedule ();
}
}
//--------------------------------------------------------------------------
//
// DOSThreadManager::destroy.
//
// This registers the destruction of a thread by decrementing the
// number of threads, and shuts down the threading mechanism if the
// last thread is being destroyed.
//
void interrupt DOSThreadManager::destroy ()
{
if (--threadcount == 0)
{
//--- unhook interrupt vectors
cleanup ();
//--- terminate main & null threads
current = 0;
ready->thread->state = DOSThread::TERMINATED;
mainthread->state = DOSThread::TERMINATED;
//--- delete main & null threads and the delay queue
delete mainthread;
delete ready->thread;
delete delayed;
}
}
//--------------------------------------------------------------------------
//
// DOSThreadManager::schedule.
//
// Save the current thread and restore another one. Do nothing
// if there is no current thread, or if the one to be scheduled
// is already the current thread.
//
void interrupt DOSThreadManager::schedule ()
{
//--- disable interrupts (original state will be restored on exit)
asm { cli; }
//--- on a 386 or above, save the extended registers
if (i386)
{ PUSH_FS;
PUSH_GS;
PUSHAD;
}
//--- switch threads if necessary
if (current != ready->next && current != 0)
{
//--- set time for end of timeslice
nextslice = *currtime + slicesize;
//--- save current thread's stack pointer
current->stkptr = (unsigned far*) MK_FP(_SS,_SP);
//--- select new current thread
current = ready->next;
//--- restore its stack (other registers will be restored on exit)
_SS = FP_SEG (current->stkptr);
_SP = FP_OFF (current->stkptr);
}
//--- on a 386 or above, restore the extended registers
if (i386)
{ POPAD;
POP_GS;
POP_FS;
}
}
//--------------------------------------------------------------------------
//
// DOSThread::DOSThread.
//
// Construct a new thread. All new threads are kept terminated
// until it is certain they can be started, and are then left in
// limbo until they are explicitly started using "run".
//
DOSThread::DOSThread (unsigned stacksize)
: stack (mainsetup ? 0 :
new char [stacksize > MIN_STACK ? stacksize : MIN_STACK]),
entry (new DOSThreadManager (this)),
state (TERMINATED)
{
//--- leave thread terminated if any allocation failures have occurred
if (!mainsetup && stack == 0)
return; // stack not allocated
if (entry == 0)
return; // thread queue entry not allocated
//--- register thread creation
DOSThreadManager::create ();
//--- initialise new thread (for all but main thread)
if (!mainsetup)
{
//--- set up stack pointer
entry->stkptr = (unsigned*)(stack + stacksize);
//--- create initial stack
asm { sti; } // ensure interrupts enabled!
*--(DOSThread**)(entry->stkptr) = this; // parameter for "start"
entry->stkptr -= 2; // dummy return address
*--(entry->stkptr) = _FLAGS; // flags
*--(entry->stkptr) = FP_SEG (&DOSThreadManager::start); // cs
*--(entry->stkptr) = FP_OFF (&DOSThreadManager::start); // ip
entry->stkptr -= 5; // ax, bx, cx, dx, es
*--(entry->stkptr) = _DS; // ds
entry->stkptr -= 2; // si, di
*--(entry->stkptr) = _BP; // bp
//--- stack extended registers on a 386 or above
if (i386)
entry->stkptr -= 18; // 8 x 32-bit regs, fs, gs
}
//--- allow thread to live (but don't move it into any queue)
state = CREATED;
}
//--------------------------------------------------------------------------
//
// DOSThread::~DOSThread.
//
// Wait for current thread to terminate, and then destroy the evidence.
//
DOSThread::~DOSThread ()
{
//--- wait for thread to terminate (normal threads only)
if (this != mainthread && this != ready->thread)
wait ();
//--- delete associated structures
delete entry;
delete stack;
//--- register thread destruction (normal threads only)
if (this != mainthread && this != ready->thread)
DOSThreadManager::destroy ();
}
//--------------------------------------------------------------------------
//
// DOSThread::wait.
//
// Wait for thread to terminate. This is needed to allow destructors
// to avoid destroying threads while they are still running.
//
void DOSThread::wait ()
{
//--- make sure a thread is not trying to wait on itself
if (this == current->thread)
return;
//--- terminate task if it hasn't started yet
if (state == CREATED)
state = TERMINATED;
//--- wait for thread to terminate
while (state != TERMINATED)
pause ();
}
//--------------------------------------------------------------------------
//
// DOSThread::userbreak.
//
// This function returns the value of the flag which indicates if
// control-break has been pressed.
//
int DOSThread::userbreak ()
{
return breakflag;
}
//--------------------------------------------------------------------------
//
// DOSThread::cancelbreak.
//
// This function resets the flag which indicates if control-break has
// been pressed. It also returns the original value of the flag.
//
int DOSThread::cancelbreak ()
{
DISABLE;
int b = breakflag;
breakflag = 0;
ENABLE;
return b;
}
//--------------------------------------------------------------------------
//
// DOSThread::run.
//
// Start a new thread running.
//
int DOSThread::run ()
{
//--- error if thread is not newly created
if (state != CREATED)
return 0;
//--- make thread ready to run and start it running
entry->move (ready->next, READY);
DOSThreadManager::schedule ();
return 1;
}
//--------------------------------------------------------------------------
//
// DOSThread::terminate.
//
// Immediately terminate a thread. The thread is detached from its
// current queue ready to be destroyed.
//
void DOSThread::terminate ()
{
entry->move (0, TERMINATED);
DOSThreadManager::schedule ();
}
//--------------------------------------------------------------------------
//
// DOSThread::delay.
//
// Delay for "n" clock ticks. The current thread is moved to the
// correct position in the "delayed" queue and will be woken up
// by the timer interrupt handler.
//
void DOSThread::delay (int n)
{
//--- don't delay if no current thread, or if tick count is non-positive
if (current == 0 || n <= 0)
return;
//--- set wake-up time
current->wakeup = *currtime + n;
//--- find correct position in delay queue
DOSThreadManager* t = delayed->next;
while (t != delayed && t->wakeup < current->wakeup)
t = t->next;
//--- put thread in delay queue and reschedule
current->move (t, DELAYED);
DOSThreadManager::schedule ();
}
//--------------------------------------------------------------------------
//
// DOSThread::pause.
//
// Move the current thread to the back of the ready queue.
//
void DOSThread::pause ()
{
//--- don't pause if no current thread
if (current == 0)
return;
//--- move current thread to back of ready queue and reschedule
current->move (ready, READY);
DOSThreadManager::schedule ();
}
//--------------------------------------------------------------------------
//
// DOSThread::timeslice.
//
// Set the timeslice. This is ignored once the first thread has
// been created.
//
void DOSThread::timeslice (unsigned n)
{
if (threadcount == 0)
slicesize = n;
}
//--------------------------------------------------------------------------
//
// Constructors and destructors for DOSMonitorQueue and DOSMonitor.
//
// These are trivial but involve knowledge of DOSThreadManager, and
// so cannot go in the header file.
//
DOSMonitorQueue::DOSMonitorQueue () : queue (new DOSThreadManager) { }
DOSMonitorQueue::~DOSMonitorQueue () { delete queue; }
DOSMonitor::DOSMonitor () : lockq (new DOSThreadManager),
lockholder (0) { }
DOSMonitor::~DOSMonitor () { delete lockq; }
//--------------------------------------------------------------------------
//
// DOSMonitor::lock.
//
// This ensures that only one thread at a time is executing in a
// monitor object, and must be called before any other monitor
// functions (suspend, resume or unlock) can be used. If another
// (non-terminated) thread already holds the lock, the current
// thread is suspended by being placed at the back of the monitor
// lock queue. When the current lock holder calls "unlock", all
// queued threads are made ready, and the thread can then attempt
// to acquire the lock again. The current thread must not be the
// current lock holder (i.e. no recursive monitor calls are allowed).
//
void DOSMonitor::lock ()
{
//--- check for errors
if (lockq == 0)
error (NEW_FAIL); // lock queue doesn't exist
if (current == 0)
error (NO_THREAD); // no current thread
if (lockholder == current)
error (LOCK_FAIL); // current thread already holds lock
DISABLE;
//--- suspend repeatedly until lock is available
while (lockholder != 0 &&
lockholder->thread->status() != DOSThread::TERMINATED)
{ current->move (lockq, DOSThread::WAITING);
DOSThreadManager::schedule ();
}
//--- make current thread the new lock holder
lockholder = current;
ENABLE;
}
//--------------------------------------------------------------------------
//
// DOSMonitor::unlock.
//
// The calling thread must be the current lock holder. The lock is
// released, and all threads requesting a lock are moved to the front
// of the ready queue.
//
void DOSMonitor::unlock ()
{
//--- check for errors
if (lockholder != current)
error (UNLOCK_FAIL); // current thread isn't the lock holder
DISABLE;
//--- release lock
lockholder = 0;
//--- make pending threads ready to run
DOSThreadManager* t = ready->next;
while (lockq->next != lockq)
lockq->next->move (t, DOSThread::READY);
//--- reschedule
DOSThreadManager::schedule ();
ENABLE;
}
//--------------------------------------------------------------------------
//
// DOSMonitor::suspend.
//
// This function allows a monitor to suspend the current thread on
// a monitor queue until another thread resumes it. The current
// thread must be the current lock holder for the monitor. The
// lock is released while the thread is suspended.
//
void DOSMonitor::suspend (DOSMonitorQueue& q)
{
//--- check for errors
if (q.queue == 0)
error (NEW_FAIL); // monitor queue doesn't exist
if (lockholder != current)
error (SUSPEND_FAIL); // current thread isn't the lock holder
//--- release lock
unlock ();
//--- suspend current thread and reschedule
current->move (q.queue, DOSThread::SUSPENDED);
DOSThreadManager::schedule ();
//--- lock the monitor again
lock ();
}
//--------------------------------------------------------------------------
//
// DOSMonitor::resume.
//
// This function allows a monitor to resume any threads suspended
// on a monitor queue. The current thread must be the current lock
// holder for the monitor.
//
void DOSMonitor::resume (DOSMonitorQueue& q)
{
//--- check for errors
if (q.queue == 0)
error (NEW_FAIL); // monitor queue doesn't exist
if (lockholder != current)
error (RESUME_FAIL); // current thread isn't the lock holder
//--- make any suspended threads wait for the lock to be released
DOSThreadManager* lq = lockq->next;
while (q.queue != q.queue->next)
q.queue->next->move (lq, DOSThread::WAITING);
}