[comp.lang.c++] DDJ C++ multitasking kernel source

MacUserLabs@cup.portal.com (Stephan - Somogyi) (05/18/89)

I'm posting the entirety of the C++ multitasking kernel from DDJ's Feb
'89 issue due to popular request. I talked to DDJ and they had no
objections.

This is posted in comp.lang.c++ since all the requests that I am aware
of have originated here and there does not appear to be a
comp.sources.c++. Please do not flame me for posting here, since this
choice is deliberate and, IMHO, justified.

<- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ->
Stephan Somogyi                           AppleLink: X1058  
Software Engineer                         BIX: mulabs       
MacUser                                   CIS: 72511,16
950 Tower Lane, 18th Floor                GEnie, MacNET: MULABS
Foster City, CA 94404                     FAX: (415) 378-5675

...sun!cup.portal.com!MacUserLabs or MacUserLabs@cup.portal.com

If fun is outlawed, only outlaws will have fun.

Any opinions expressed above are mine.

- - - - -

_A C++ Multitasking Kernel_
by Tom Green

[LISTING ONE]


/********************************************/
/*		TASK.HPP                    */
/*		Tom Green                   */
/********************************************/
/* this file contains classes needed to use multi-tasking kernel */
/* include this file in your source code and then link with */
/* task.cpp and timer.asm */

/* this is used when a task is initialized */
/* this is a pointer to a function */
typedef void (*func_ptr)(void);
/* this is used for interrupt handler to call old interrupt handler */
/* this is a far pointer to a function */
typedef void (far *far_func_ptr)(void);

/* this is how the registers will look on the stack for a task */
/* after they have been saved for a task switch. the sp and ss */
/* registers will point to this when a task is started from the */
/* interrupt handler or save_image */

typedef struct task_image{
	unsigned int bp;
	unsigned int di;
	unsigned int si;
	unsigned int ds;
	unsigned int es;
	unsigned int dx;
	unsigned int cx;
	unsigned int bx;
	unsigned int ax;
	unsigned int ip;
	unsigned int cs;
	unsigned int flags;
}task_image;

/* a task object. contains information needed by task_control object */
/* to do task switching and a pointer to the task's workspace (stack) */

class task{
	private:
		friend class task_control;  // task_control object needs access
		friend class signal;        // signal needs access to next_task
		task_image far *stack_ptr;  // task stack ("image") pointer
		unsigned char task_state;   // task state flag
		unsigned char *workspace;   // address of allocated task stack
		task *next_task;            // pointer to next task in queue
	public:
		task(func_ptr func,unsigned int workspace_size); // constructor
		~task();											// destructor
};


/* this is a queue for tasks */
/* it is called signal so user can define a signal for task communication */

class signal{
	private:
		friend class task_control;   // task_control needs access
		task *head;
		task *tail;
		task *get_task_q(void);	     // get next task off of queue
		void put_task_q(task *tptr); // append task to queue
	public:
		signal(void){head=tail=0;};  // constructor
};




/* task_control object */
/* routines and methods to interface with and control tasks */
/* this object will initialize and restore interrupt vectors, */
/* keep track of timer ticks, and switch execution between the */
/* task objects */

class task_control{
	private:
		signal ready_q;			// queue of tasks ready to run
		task *current_task;		// current active task
		task_image far *old_stack_ptr;	// return to this stack when done
		unsigned int task_running;	// task switching enabled flag
		unsigned long timer_ticks;	// 18.2 ticks/second
		unsigned int task_lock;		// lock out task switching
		task_image far *task_switch(task_image far *stk_ptr,
										unsigned int flag,
										signal *sig);
	public:
		task_control(void);                // constructor
		void add_new_task(task *new_task); // add new task object to ready q
		void start_tasks(void);	           // start switching tasks on ready_q
		void stop_tasks(void){task_running=0;};
		unsigned long get_timer_ticks(void){return(timer_ticks);};
		void lock(void){task_lock=1;};	    // current task can not be switched
		void unlock(void){task_lock=0;};    // allow task switching
		void send(signal *sig);             // put task from sig q on ready q
		void wait(signal *sig);             // put task on sig q
		void block(void);                   // task allows next to run
};

[LISTING TWO]

/********************************************/
/*		TASK.CPP                    */
/*		by Tom Green	            */					
/********************************************/

/* this file implements the methods used by task_control and task */
/* objects */

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <int.h>
#include "task.hpp"

/* task states */
#define	TASK_INACTIVE	0
#define	TASK_ACTIVE 	1
#define TASK_READY	2
#define	TASK_WAITING	3
#define TASK_ERROR	0xff

/* flags for interface routines */
#define	TASK_TIMER_INTR	0
#define	TASK_SEND		1
#define	TASK_WAIT		2
#define TASK_BLOCK		3

/* system timer interrupt or "timer tick" */
#define TIMER_INT		8

/* routines we need from timer.asm */
unsigned int getCS(void);
extern void timer_handler(void);
extern void save_image(unsigned int flag,signal *sig);

/* global for timer_handler to call old interrupt routine */
far_func_ptr old_timer_handler;

/* this is really ugly. */
/* when constructor for task_control object is called we save the */
/* this pointer for task switch routine to call our task_control object */
/* task_switch. this means we can only have 1 task_control object. sorry */
task_control *gl_tptr;

/* constructor for a new task. workspace will be the stack space for */
/* the task. when the timer interrupt happens the tasks "image" */
/* is saved on the stack for use later and the task_image *stack_ptr */
/* will point to this image */

task::task(func_ptr func,unsigned int workspace_size)
{
	task_image *ptr;
	
	/* get stack or "workspace" for task */
	if((workspace=(unsigned char *)malloc(workspace_size))==NULL){
		task_state=TASK_ERROR;	// do not let this one run
		return;
	}

	/* now we must set up the starting "image" of the task registers */
	/* ptr will point to the register image to begin task */
	ptr=(task_image *)(workspace+workspace_size-sizeof(task_image));

	/* now save the pointer to the register image */
	stack_ptr=MK_FP(getDS(),(unsigned int)ptr);

	ptr->ip=(unsigned int)func; // offset of pointer to task code
	ptr->cs=getCS();            // segment of pointer to task, compiler bug
	ptr->ds=getDS();
	ptr->flags=0x200;	    // flags, interrupts on
	task_state=TASK_INACTIVE;   // task is inactive
	next_task=0;

/* destructor for a task object */

task::~task(void)
{
	free(workspace);
}

/* get the next task off of a task queue */

task *signal::get_task_q(void)
{
	task *temp;
	
	temp=head;
	if(head)
		head=head->next_task;
	return(temp);
}

/* append a task to the end of a task queue */

void signal::put_task_q(task *tptr)
{
	if(head)
		tail->next_task=tptr;
	else
		head=tptr;
	tail=tptr;
	tptr->next_task=0;
}

/* constructor for task_control */
/* inits private stuff for task control */

task_control::task_control(void)
{
	gl_tptr=this;
	task_running=0;
	current_task=0;
	timer_ticks=0L;
	task_lock=0;
}

/* adds a task to the task ready_q */
/* call this routine after creating a task object */

void task_control::add_new_task(task *new_task)
{
	if(new_task->task_state!=TASK_ERROR){
		new_task->task_state=TASK_READY;
		ready_q.put_task_q(new_task);
	}
}

/* call to start up tasks after you have created some */
/* and added them to the ready_q */

void task_control::start_tasks(void)
{
	unsigned int offset,segment;

	task_running=1;
	/* get address of old timer interrupt handler */
	int_getvector(TIMER_INT,&offset,&segment);
	old_timer_handler=(far_func_ptr)(MK_FP(segment,offset));
	/* set up our new timer interrupt handler */
	int_setvector(TIMER_INT,(unsigned int)timer_handler,getCS());
	/* tasks will now start running */
	while(task_running)
		; // do nothing, trick to wait for tasks to start up
	/* falls through to here when multi-tasking is turned off */
}

/* gets the next task off of sig queue and puts it */
/* on the ready_q. this suspends operation of the calling */
/* task which is also put on the ready queue */

void task_control::send(signal *sig)
{
	save_image(TASK_SEND,sig);
}

/* puts the calling task on the sig queue to wait for a signal */

void task_control::wait(signal *sig)
{
	save_image(TASK_WAIT,sig);
}

/* this causes the current task to be placed on the ready queue */
/* and a switch to the next task on the ready_q */

void task_control::block(void)
{
	save_image(TASK_BLOCK,(signal *)0);
}

/* this routine is called to do a task switch. it is */
/* passed a task_image far * to the current stack or task "image". */
/* also pass a flag (described above) and a signal pointer if needed. */
/* a task_image * to the "image" of the next task is returned */

task_image far *task_control::task_switch(task_image far *stk_ptr,
												signal *sig)
{
	task_image far *temp;
	task *tptr;

	if(flag==TASK_TIMER_INTR)  // increment clock if it is a timer interrupt
		timer_ticks++;

	/* this saves a pointer to stack when we first start multi-tasking */
	/* allows us to return to where start_tasks was called */
	if(!current_task){  // no current task so save state for restoring
		old_stack_ptr=stk_ptr;	// save stack pointer
		current_task=ready_q.get_task_q(); // set up a current task
	}

	/* we have an active task, so do task switch if we can */
	if(current_task->task_state==TASK_ACTIVE){
		current_task->stack_ptr=stk_ptr;  // save stack pointer
		current_task->task_state=TASK_READY; // task is ready to go
		/* do not allow task switching if tasks are locked and */
		/* it is timer interrupt */
		if(!task_lock || flag!=TASK_TIMER_INTR){
			/* check and see what caused task_switch to be called */
			switch(flag){
				case TASK_WAIT:
					current_task->task_state=TASK_WAITING;
					sig->put_task_q(current_task);
					break;
				case TASK_SEND:
					if((tptr=sig->get_task_q())!=0)
						ready_q.put_task_q(tptr);
					// fall through
				case TASK_BLOCK:
				case TASK_TIMER_INTR:
					current_task->task_state=TASK_READY;
					/* put old task on ready queue */
					ready_q.put_task_q(current_task);
					break;
			}
			/* get next task to go */
			current_task=ready_q.get_task_q();
		}
	}

	/* if we are still multi-tasking, get task ready to run */
	if(task_running){
		current_task->task_state=TASK_ACTIVE;
		temp=current_task->stack_ptr;	// get stack pointer of task
	}
	/* multi-tasking has stopped, get ready to return where we started */
	else{					// someone called stop_tasks
		int_setvector(TIMER_INT,FP_OFF(old_timer_handler),
						FP_SEG(old_timer_handler));
		temp=old_stack_ptr;	// get back original stack
	}
	/* return far pointer to stack_image to do task switch */
	return(temp);
}

[LISTING THREE]

;*****************************************************************************
;	TIMER.ASM
;	by Tom Green
;	Timer interrupt handler
;	Timer interrupt handler calls original handler first and then calls the
;	task_control object task switcher. a pointer to the stack "image"
;	of the new task is returned by the task switcher.
;	getCS
;	returns current code segment
;	save_image
;	saves "image" of task as if interrupt had happened and then calls the
;	task_control object task switcher. a pointer to the stack "image"
;	of the new task is returned by the task switcher.
;*****************************************************************************

	.MODEL SMALL
	.8086
	
	.DATA

extrn _old_timer_handler:dword
extrn _gl_tptr:word
extrn __task_control_task_switch:near

	.CODE

;*****************************************************************************
;	unsigned int getCS(void) - returns current code segment.
;	this is needed because of compiler bug. when a function is cast
;	to a far function, and you try to get the segment, DS is returned
;	instead of CS.
;*****************************************************************************
_getCS	proc	near
public	_getCS
		mov		ax,cs
		ret
_getCS	endp
		
;*****************************************************************************
;	timer_handler - this replaces the MS-DOS timer tick interrupt (8H).
;	this routine saves everything on stack, calls original timer interrupt
;	handler, and then calls task_control object task switcher.
;*****************************************************************************
_timer_handler	proc	near
public	_timer_handler
		push	ax				;save everything
		push	bx
		push	cx
		push	dx
		push	es
		push	ds
		push	si
		push	di
		push	bp
		mov		bp,dgroup
		mov	ds,bp	        ;get our data segment back
		pushf
		call	dword ptr dgroup:_old_timer_handler  ;call original handler
		sti
		xor		dx,dx
		mov		ax,ss
		mov		bx,sp
		push	dx	        ;push 0 for last 2 parameters
		push	dx		;meaning timer interrupt
		push	ax
		push	bx
		mov		dx,_gl_tptr   ;push hidden pointer for C++ object
		push	dx
;stack is now set up for call to task_control object task_switch
		cli                   ;turn off interrupts for task switch
		call	__task_control_task_switch
;no need to clean up the stack because it will change
		sti
		mov		ss,dx	;new ss returned in dx
		mov		sp,ax	;new sp returned in ax
		pop		bp	;restore registers
		pop		di
		pop		si
		pop		ds
		pop		es
		pop		dx
		pop		cx
		pop		bx
		pop		ax
		iret
_timer_handler	endp

;*****************************************************************************
;	void save_image(unsigned int flag,signal *sig) - send, wait, block
;	etc. all call through here to save the "image" of the task. this
;	code simulates what will happen with an interrupt by saving the task
;	image on the stack. the flag passed is passed on to the task_control
;	object task switcher so it knows if it was called by the timer
;	interrupt handler, send, wait, block, etc. the second parameter
;	is a signal pointer which is used by send and wait and is passed
;	through to the task switcher.
;*****************************************************************************
_save_image		proc	near
public	_save_image
;since this is a C call we can destroy some registers (ax bx cx dx),
;so now we will set up the stack as if an interrupt call had happened.
;leave parameters on stack, because calling routine will adjust on
;return. bx and cx will have the parameters that were passed.
		pop		ax	;get return address offset on stack
		pop		bx	;get first parameter off stack
		pop		cx	;get second parameter off stack
		push	cx		;put them back on stack
		push	bx
		pushf			;save flags for iret
		mov		dx,cs	;get code segment
		push	dx		;save code segment for return address
		push	ax		;push saved return address offset
		push	ax		;save everything
		push	bx
		push	cx
		push	dx
		push	es
		push	ds
		push	si
		push	di
		push	bp
		sti
		mov		ax,sp	;stack pointer parameter
		push	cx		;second parameter passed
		push	bx		;first parameter passed
		mov		bx,ss
		push	bx		;far pointer to stack, parameter passed
		push	ax
		mov		ax,_gl_tptr  ;push hidden pointer for C++ object
		push	ax
;stack is now set up for call to task_control object task_switch
		cli			  ;turn off interrupts for task switch
		call	__task_control_task_switch
;no need to clean up the stack because it will change
		sti
		mov		ss,dx		;new ss returned in dx
		mov		sp,ax		;new sp returned in ax
		pop		bp		;restore registers
		pop		di
		pop		si
		pop		ds
		pop		es
		pop		dx
		pop		cx
		pop		bx
		pop		ax
		iret
_save_image		endp

		end

[LISTING FOUR]

/********************************************/
/*		TASKDEMO.HPP                */
/*		by Tom Green                */
/********************************************/

/* this file is a demonstration of how to use the C++ multi-tasking */
/* kernel. 5 tasks are run and the various means of task switching */
/* and communication are shown */

/* you must have the Zortech C++ compiler version 1.5 and linker and */
/* Microsoft MASM 5.xx to compile this code. */
/* type "ztc taskdemo task timer" and the ztc.com will take */
/* care of compiling, assembling, and linking */

#include <stdio.h>
#include <disp.h>
#include "task.hpp"

void task0(void);
void task1(void);
void task2(void);
void task3(void);
void task4(void);

/* our task_control object (just 1 please) */
task_control tasker;

void main(void)
{
	/* task objects */
	task t0((func_ptr)task0,1024);
	task t1((func_ptr)task1,1024);
	task t2((func_ptr)task2,1024);
	task t3((func_ptr)task3,1024);
	task t4((func_ptr)task4,1024);

	/* add task objects to our task_control object ready q */
	tasker.add_new_task(&t0);
	tasker.add_new_task(&t1);
	tasker.add_new_task(&t2);
	tasker.add_new_task(&t3);
	tasker.add_new_task(&t4);

	/* use zortech display package */
	disp_open();	
	disp_move(0,0);
	disp_eeop();

	/* start tasks up and wait for them to finish */
	tasker.start_tasks();

	disp_move(0,0);
	disp_eeop();
	disp_close();
}

static unsigned long counter[]={0L,0L,0L,0L,0L};
static signal sig;

/* task 0 prints the values of the counters for the other 4 tasks. */
/* lock is used to prevent task switching while the screen is being */
/* updated. when the task is finished, block is called to transfer */
/* control to the next task on the ready q */

void task0(void)
{
	while(1){
		/* disable task switching */
		tasker.lock();
		disp_move(5,10);
		disp_printf("Task 1  %lx",counter[1]);
		disp_move(5,50);
		disp_printf("Task 2  %lx",counter[2]);
		disp_move(15,10);
		disp_printf("Task 3  %lx",counter[3]);
		disp_move(15,50);
		disp_printf("Task 4  %lx",counter[4]);
		/* if key pressed then stop the kernel and return */
		if(kbhit())
			tasker.stop_tasks();
		/* re-enable task switching */
		tasker.unlock();
		/* let next task run */
		tasker.block();
	}
}

/* tasks 1 and 2 just update counters. these tasks will run until */
/* a timer interrupt occurs, so they get a very large chunk of time */
/* to run, so the counters increase rapidly */

void task1(void)
{
	while(1){
		counter[1]++;
	}
}

void task2(void)
{
	while(1){
		counter[2]++;
	}
}

/* task 3 waits for a signal from task 4 each time the counter is */
/* incremented. when a task waits, it is put on a signal q and the */
/* next task on the ready q is run. this means task 3 and 4 counters */
/* will increment very slowly. in task 4 when a signal is sent, the */
/* task signal q is checked for a task to put on the ready q. the task */
/* sending the signal is then placed on the ready q */

void task3(void)
{
	while(1){
		counter[3]++;
		/* wait for a signal from task 4 */
		tasker.wait(&sig);
	}
}

void task4(void)
{
	while(1){
		counter[4]++;
		/* send signal to task 3 */
		tasker.send(&sig);
	}
}